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
Dateobjects are frequently innocuous:Datedoes not use dynamically allocated memory, so a corrupted object will not crash a program.Dateobjects are often used in contexts where a nonsensical value might go unnoticed.
Datepredatesbsls_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
Dateare 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).