As an example, consider this code (godbolt: https://godbolt.org/z/TrMrYTKG9):
struct foo {
unsigned char a, b;
};
foo make(int x) {
foo result;
if (x) {
result.a = 13;
} else {
result.b = 37;
}
return result;
}
At high enough optimization levels, the function compiles to “mov eax, 9485; ret”, which sets both a=13 and b=37 without testing the condition at all - as if both branches of the test were executed. This is perfectly reasonable because the lack of initialization means the values could already have been set that way (even if unlikely), so the compiler just goes ahead and sets them that way. It’s faster!succeeded = true; error = true; //This makes no sense
succeeded = false; error = false; //This makes no sense
Otherwise if I'm checking a response, I am generally going to check just "succeeded" or "error" and miss one of the two above states that "shouldn't happen", or if I check both it's both a lot of awkward extra code and I'm left with trying to output an error for a state that again makes no sense.
Compiler was changed to allocate storage for any referenced varibles.
Typically you'd have at least an assert (and hopefully some unit tests) to ensure that invariant (.success ^ .error == true).
But the code has just been getting by on the good graces of the previous stack contents. One random day, the app behaviour changed and left a non-zero byte that the response struct picked up and left the app in the alternate reality where .success == .error
Others have mentioned sanitizers that may expose the problem.
Microsoft's Visual C++ compiler has the RTCs/RTC1 compiler switch which fills the stack frame with a non-zero value (0xCC). Using that compiler switch would have exposed the problem.
You could also create a custom __chkstk stack probe function and have GCC/Clang use this to fill the stack as well as probing the stack. I did this years ago when there was no RTCs/RTC1 compiler option available in VC++.
The original code defined a struct with two bools that were not initialized. Therefore, when you instantiate one, the initial values of the two bools could be anything. In particular, they could be both true.
This is a bit like defining a local int and getting surprised that its initial value is not always zero. (Even if the compiler did nothing funny with UB, its initial value could be anything.)
1 - In C++, a struct is no different than a class
other than a default scope of public instead of
private.
2 - The use of braces for property initialization
in a constructor is malformed C++.
3 - C++ is not C, as the author eventually concedes:
At this point, my C developer spider senses are tingling:
is Response response; the culprit? It has to be, right? In
C, that's clear undefined behavior to read fields from
response: The C struct is not initialized.
In short, if the author employed C++ instead of trying to use C techniques, all they would have needed is a zero cost constructor definition such as: inline Response () : error (false), succeeded (false)
{
;
}And by convention, all classes derived from CBase would start their name with C, so something like CHash or CRectangle.
I think a sanitizer probably would have caught this, but IMHO this is the language's fault.
Hopefully future versions of C++ will mandate default initialization for all cases that are UB today and we can be free of this class of bug.
But re the distinction at the end of TFA — that a garbage char is slightly more OK than a garbage bool — that's also intuitive. Eight bits of garbage is always going to be at least some valid char (physically speaking), whereas it's highly unlikely that eight bits of garbage will happen to form a valid bool (there being only two valid values for bool out of those 256 possible octets).
This also relates to the (old in GCC but super new in Clang, IIUC) compiler option -fstrict-bool.