bdlt::Date Is Always Valid (Except When It Isn’t)

Undefined behavior related to BDE date and time types is a frequent source of confusion. This is highlighted by a common pattern found in client code:

 if (bdlt::DateTz::isValid(someDate, 0)) {  // Not what the author intended!

     // ...

In the example above, the author may have heard of “invalid” Date objects, has noticed that BDE systematically provides isValid methods, and assumed that isValid will somehow detect a corrupted Date.

Note

This article focuses on bdlt::Date, but it is equally true of other BDE date and time types in the bdlt package like bdlt::Datetime and bdlt::Time.

Unfortunately, isValid assumes that the program has not previously violated any function contracts or evaluated any undefined behavior, and thus all objects supplied to it are themselves internally consistent. What isValid actually does is verify whether someDate and 0 (a time zone offset) are a valid combination to construct a DateTz.

The check above is always true!

To put it another way, the call to DateTz::isValid does not verify whether function pre-conditions for Date were violated in the past, but verifies whether the arguments would themselves meet the preconditions of the bdlt::DateTz constructor.

Note

In a valid program, a bdlt::Date cannot be in a corrupted state.

The preconditions on the constructors and manipulators of bdlt::Date preclude creating a corrupted bdlt::Date (or any other BDE date and time type). Other mechanisms for creating a corrupted Date – like memset – are undefined behavior in the language. So, in theory, there is no need for any check at all. Is the original author’s concern reasonable?

A Valid Program Can’t Have a Corrupted bdlt::Date — But What About My Program?

Unfortunately, while it’s true a valid (bug-free) program cannot have a corrupted bdlt::Date, in the real world, there are many working programs that are not bug-free.

Some protections against bugs exist:

  • In appropriate build modes preconditions may catch the creation of corrupted objects.

  • Various serialization methods (like encoding and decoding BAS messages) preclude transmitting (some) corrupted date and time values.

However, corrupted bdlt::Date objects are still found with some frequency in the real world.

Here are some examples of corrupted dates:

static bdlt::Date g_globalDateVariable(2025, 7, 31);   // Might be used before construction!

int main() {
    bdlt::Date invalidInput(2025, -1 , 12);  // Will not be caught unless this is a SAFE build

    bsl::optional<bdlt::Date> nullableDate;
    doSomething(*nullableDate);              // Failed to check for an unset value

    bdlt::Date temp;
    memset(&temp, 0, sizeof(bdlt::Date));    // Invalid use of raw memory access

Date objects are particularly susceptible to corruption because:

  • Corrupted Date objects are frequently innocuous:

    • Date does not use dynamically allocated memory, so a corrupted object will not crash a program.

    • Date objects are often used in contexts where a nonsensical value might go unnoticed.

  • Date predates bsls_assert:

    • The preconditions for date and time functions were not checked for many years after their release.

    • Enabling those checks now in production software is difficult and dangerous (because of the undetected bugs).

  • The constructors for Date are easy to misuse:

    • In hindsight, a common vocabulary type needs less error-prone constructors and manipulators.

Because of this, bugs often go undetected for years in production software.

How Do I Identify Corrupted bdlt::Date Objects

1. Use APIs that Prevent Creating an Invalid Date In the First Place

Traditionally:

bdlt::Date date;
if (0 != date.setYearMonthDayIfValid(year, month, day)) {
    return -1;
}

Prompted by feedback from the OCaml community, and consistent with the C++ Guild Safety Working Group’s suggestion’s, we have recently added factory functions. For example:

bsl::optional<bdlt::Date> date = bdlt::DateUtil::fromYmd(year, month, day);
// ``date`` will be unset if ``year``, ``month``, and ``day`` do not form a valid date

2. Build and Test with Safe Asserts Enabled

Unfortunately, because of the large number of pre-existing bugs, the precondition checks for date and time types are currently only enabled in “SAFE” mode builds. A SAFE-mode build is a build performed with compiler flags that enable additional library checks.

3. In Case of Emergency, Break Glass

We cannot always determine that the memory for a Date has been corrupted. But sometimes we can come close…

This should be treated as a last resort: corrupted Date objects cannot typically be transmitted (producing encoding/decoding errors in BAS, for example), and bugs in code you maintain are better identified with SAFE-mode asserts.

However, there are contexts where finding the source of a corrupted value can be quite difficult.

In particular:

Note

We think this may sometimes be useful for library developers, where corrupted objects are being passed through a library boundary, and you may not immediately be able to identify the calling code.

For example:

void libraryFunction(const bdlt::Date& dateFromUnknownSource) {

    if (dateFromUnknownSource == bdlt::Date(9999, 12, 31)
        || dateFromUnknownSource.addDaysIfValid(0)) {
        BALL_FMT_FATAL("libraryFunction has received a corrupted Date");

        // Stop the normal flow of execution...
    }
}

This might allow more quickly identifying the callers who have created the corrupted bdlt::Date objects.

Why is this a Last Resort?

It may be tempting to use the workaround above, but it is just that: a workaround. Our concern is that a workaround will be used to mask bugs (making them even harder to find) rather than identifying and fixing them.

Consider the following example, where a developer has used the workaround to avoid processing bad trades:

int processTrade(const TradeData& trade, const bdlt::Date& date) {

    // Do stuff

    bdlt::Date tmp(date);
    if (tmp == bdlt::Date(9999, 12, 31) || tmp.addDaysIfValid(0)) {

        bsl::cout << "Invalid `Date` detected!" << bsl::endl;
        return -1;
    }

    // We're safe?
    return executeTrade(trade, date);
}

Unfortunately:

processTrade(trade, bdlt::Date(0, 0, 0));                              // `processTrade` fails with error

unsigned char corruptedMemory[4] = {0x67, 0xAA, 0x13, 0x00};
processTrade(trade, bdlt::Date(0, 0, 0) - 1);                          // `executeTrade` on 30-Dec-9999
processTrade(trade, *reinterpret_cast<bdlt::Date *>(corruptedMemory)); // `executeTrade` on 18-Aug-3529

So again, we recommend the workaround above to help identify the root cause of a bug. Typically though, unless you are receiving corrupted dates through a library interface boundary, we expect there are easier ways to identify the problem (like a SAFE-mode build).