Perhaps what you mean is, "Nothing is to be gained by relying on the language spec to initialize things to zero, and a lot is lost"; I'd agree with that.
Read a complex enough project that's meant to be used across compiler venrdos and versions, and you'll find plenty of instances where they're working around the compiler not implementing the standard.
Also, if you attended the standards committee, you would hear plenty of complaints from compiler vendors that certain things are implementable. Sometimes the committee listens and makes changes, other times they put their fingers in their ears and ignore reality.
There are also plenty of places where the standard lets the compiler make it's own decision (implementation defined behavior). You need to know what your compiler vendor(s) chose to do.
tl;dr: With a standard as complex as C++'s, the compilers very much do not just "implement the standard". Sometimes you can get away with pretending that, but others very much not.
The standard (to the extent that it is implemented) is implemented by compilers. At this point this whole thread has nothing to do with my original point, just weird one-upping all around
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!The code says that if x is true then a=13 and if it is false than b=37.
This is the case. Its just that a=13 even if x is false. A thing that the code had nothing to say about, and so the compiler is free to do.
Practically speaking, I’d argue that a compiler assuming uninitialized stack or heap memory is always equal to some arbitrary convenient constant is obviously incorrect, actively harmful, and benefits no one.
I take issue with the compiler assuming anything about the contents of that memory; it should be a black box.
The memory being uninitialised means reading it is illegal for the writer of the program. The compiler can write to it if that suits it, the program can’t see the difference without UB.
In fact the compiler can also read from it, because it knows that it has in fact initialised that memory. And the compiler is not writing a C program and is thus not bound by the strictures of the C abstract machine anyway.
> The user didn’t initialize this integer. Let’s assume it’s always 4 since that helps us optimize this division over here into a shift…
This is convenient for who exactly? Why not just treat it as a black box memory load and not do further “optimizations”?
Nobody’s stopping you from using non-optimising compilers, regardless of the strawmen you assert.
There’s a million more sensible things that the compiler could do here besides the hilariously bad codegen you see in the grandparent and sibling comments.
All I’ve heard amounts to “but it’s allowed by the spec.” I’m not arguing against that. I’m saying a spec that incentivizes this nonsense is poorly designed.
That's not what these words mean.
> There’s a million more sensible things
Again, if you don't like compilers leveraging UBs use a non-optimizing compiler.
> All I’ve heard amounts to “but it’s allowed by the spec.” I’m not arguing against that.
You literally are though. Your statements so far have all been variations of or nonsensical assertions around "why can't I read from uninitialised memory when the spec says I can't do that".
> I’m saying a spec that incentivizes this nonsense is poorly designed.
Then... don't use languages that are specified that way? It's really not that hard.
> Undef values aren't exactly constants ... they can appear to have different bit patterns at each use.
My claim is simple and narrow: compilers should internally model such values as unspecified, not actively choose convenient constants.
The comment I replied to cited an example where an undef is constant folded into the value required for a conditional to be true. Can you point to any case where that produces a real optimization benefit, as opposed to being a degenerate interaction between UB and value propagation passes?
And to be explicit: “if you don’t like it, don’t use it” is just refusing to engage, not a constructive response to this critique. These semantics aren't set in stone.
[0] https://llvm.org/doxygen/classllvm_1_1UndefValue.html#detail...
An assertion you have provided no utility or justification for.
> The comment I replied to cited an example where an undef is constant folded into the value required for a conditional to be true.
The comment you replied to did in fact not do that and it’s incredible that you misread it such.
> Can you point to any case where that produces a real optimization benefit, as opposed to being a degenerate interaction between UB and value propagation passes?
The original snippet literally folds a branch and two stores into a single store, saving CPU resources and generating tighter code.
> this critique
Critique is not what you have engaged in at any point.
The snippet is, after lowering:
if (x)
return { a = 13, b = undef }
else
return { a = undef, b = 37 }
LLVM represents this as a phi node of two aggregates: a = phi [13, then], [undef, else]
b = phi [undef, then], [37, else]
Since undef isn’t “unknown”, it’s “pick any value you like, per use”, InstCombine is allowed to instantiate each undef to whatever makes the expression simplest. This is the problem. a = 13
b = 37
The branch is eliminated, but only because LLVM assumes that those undefs will take specific arbitrary values chosen for convenience (fewer instructions).Yes, the spec permits this. But at that point the program has already violated the language contract by executing undefined behavior. The read is accidental by definition: the program makes no claim about the value. Treating that absence of meaning as permission to invent specific values is a semantic choice, and precisely what I am criticizing. This “optimization” is not a win unless you willfully ignore the program and everything but instruction count.
As for utility and justification: it’s all about user experience. A good language and compiler should preserve a clear mental model between what the programmer wrote and what runs. Silent non-local behavior changes (such as the one in the article) destroy that. Bugs should fail loudly and early, not be “optimized” away.
Imagine if the spec treated type mismatches the same way. Oops, assigned a float to an int, now it’s undef. Let’s just assume it’s always 42 since that lets us eliminate a branch. That’s obviously absurd, and this is the same category of mistake.
int random() {
return 4; // chosen by dice roll
}
Technically correct. But not really.Same for b. If x is true, b could be 37 no matter how unlikely that is.
I’ve never understood this behaviour from clang. At least stick a trap at the end so the program aborts instead of just executing random instructions?
The x and y values are funny too, because clang doesn’t even bother loading anything into esi for operator<<(unsigned int), so you get whatever the previous call left behind in that register. This means there’s no x or y variable at all, even though they’re nominally being “printed out”.
It can just leave the result totally uninitialised. That's because both code paths have undefined behaviour: whichever of result.x or result.y is not set is still copied at "return result" which is undefined behaviour, so the overall function has undefined behaviour either way.
It could even just replace the function body with abort(), or omit the implementation entirely (even the ret instruction, allowing execution to just fall through to whatever memory happens to follow). Whether any computer does that in practice is another matter.
That is incorrect, per the resolution of DR222 (partially initialized structures) at WG14:
> This DR asks the question of whether or not struct assignment is well defined when the source of the assignment is a struct, some of whose members have not been given a value. There was consensus that this should be well defined because of common usage, including the standard-specified structure struct tm.
As long as the caller doesn't read an uninitialised member, it's completely fine.
The compiler sees that foo can only be assigned in one place (that isn't called locally, but could called from other object files linked into the program) and its address never escapes. Since dereferencing a null pointer is UB, it can legally assume that `*foo` is always 42 and optimizes out the variable entirely.
Compilers can do whatever they want when they see UB, and accessing an unassigned and unassiganble (file-local) variable is UB, therefore the compiler can just decide that *foo is in fact always 42, or never 42, or sometimes 42, and all would be just as valid options for the compiler.
(I know I'm just restating the parent comment, but I had to think it through several times before understanding it myself, even after reading that.)
The way they work things out is to assume no UB happens (because otherwise your program is invalid and you would not request compiling an invalid program would you) then work from there.
That's not exactly correct. It's not that the compiler sees that there's UB and decides to do something arbitrary: it's that it sees that there's exactly one way for UB to not be triggered and so it's assuming that that's happening.
"But it's right there in the name!" Undefined behavior literally places no restrictions on the code generated or the behavior of the program. And the compiler is under no obligation to help you debug your (admittedly buggy) program. It can literally delete your program and replace it with something else that it likes.
[1] https://kristerw.blogspot.com/2017/09/why-undefined-behavior...
This is because the code is executed symbolically during optimization. It's not running on your real CPU. It's first "run" on a simulation of an abstract machine from the C spec, which doesn't have registers or even real stack to hold an actual garbage value, but it does have magic memory where bits can be set to 0, 1, or this-can-never-ever-happen.
Optimization passes ask questions like "is x unused? (so I can skip saving its register)" or "is x always equal to y? (so I can stop storing it separately)" or "is this condition using x always true? (so that I can remove the else branch)". When using the value is an undefined behavior, there's no requirement for these answers to be consistent or even correct, so the optimizer rolls with whatever seems cheapest/easiest.
With Optimizing settings on, the compiler should immediately treat unused variables as errors by default.
1. Syntactically require initialization, ie you can't write "int k;" only "int k = 0;". This is easy to do and 100% effective, but for many algorithms this has a notable performance cost to comply.
2. Semantically require initialization, the compiler must prove at least one write happens before every read. Rice's Theorem says we cannot have this unless we're willing to accept that some correct programs don't compile because the compiler couldn't see why they're correct. Safe Rust lives here. Fewer but still some programmers will hate this too because you're still losing perf in some cases to shut up the prover.
3. Redefine "immediately" as "Well, it should report the error at runtime". This has an even larger performance overhead in many cases, and of course in some applications there is no meaningful "report the error at runtime".
Now, it so happens I think option (2) is almost always the right choice, but then I would say that. If you need performance then sometimes none of those options is enough, which is why unsafe Rust is allowed to call core::mem::MaybeUninit::assume_init an unsafe function which in many cases compiles to no instructions at all, but is the specific moment when you're taking responsibility for claiming this is initialized and if you're wrong about that too fucking bad.
In languages like Go or Odin where "zero is default" for every type and you can't even opt out, this same problem (which I'd say is a bigger but less instantly fatal version of the Billion Dollar Mistake) occurs everywhere, at every API edge, and even in documentation, you just have to suck it up.
Which reminds of in a sense another option - you can have the syntactic behaviour but write it as though you don't initialize at all even though you do, which is the behaviour C++ silently has for user defined types. If we define a Goose type (in C++ a "class"), which we stubbornly don't provide any way for our users to make themselves (e.g. we make the constructors private, or we explicitly delete the constructors), and then a user writes "Goose foo;" in their C++ program it won't compile because the compiler isn't allowed to leave this foo variable uninitialized - but it also can't just construct it, so, too bad, this isn't a valid C++ program.
int foo(bool x, int* y) {
if (x) return *y;
return 0;
}
Dereferencing y would be UB. But maybe this function is called only with x=false when y is nullptr. This cannot be a compile error. So instead the compiler recognizes that certain program paths are illegal and uses that information during compilation.Retrofitting this into C++ at the language level is impossible. At least without a huge change in priorities from the committee.
-Werror -Wlet-me-stop-you-right-thereGood example of why uninitialized variables are not intuitive.
Compiler was changed to allocate storage for any referenced varibles.
And by convention, all classes derived from CBase would start their name with C, so something like CHash or CRectangle.
Also, how does CBase knows the size of its allocated memory?
https://github.com/SymbianSource/oss.FCL.sf.os.kernelhwsrv/b...
2. Initialisation of the CBase derived object to binary zeroes through a specific CBase::operator new() - this means that members, whose initial value should be zero, do not have to be initialised in the constructor. This allows safe destruction of a partially-constructed object.
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.
Then the obvious question why do we need _succeeded_ at all, if we can always check for _error_. Sometimes it can be useful, when the server doesn't know itself if the operation is succeeded (e.g. an IO/database operation timed out), so it might be succeeded, but should also show an error message to user.
Another possibility if the succeeded is not a bool, but, say, "succeeded_at" timestamp. In general, I noticed that almost always any boolean value in database can be replaced with a timestamp or an error code.
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.)
Then reading from that struct like in OP constitutes UB.
Could a language define un-initialized variables as readable garbage? Sure, but that would be a different language with different semantics, and such languages can also define declaration such that
> defining a local int and getting surprised that its initial value is not always zero.
is in fact reasonable. That is what Java and Go opted to do, for instance.
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)
{
;
}One of my s/w engineering axioms is:
Better to express intent than assume a future
reader of a solution, including myself, will
intrinsically understand the decisions made.
If this costs a few extra keystrokes when authoring an implementation, so be it.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++.
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.
titzer•1mo ago
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.
trueismywork•1mo ago
torstenvl•1mo ago
Even if the implementation specified that the data would be indeterminate depending on what existed in that memory location previously, the bug would still exist.
Even if you hand-coded this in assembly, the bug would still exist.
The essence of the bug is uninitialized data being garbage. That's always gonna be a latent bug, regardless of whether the behavior is defined in an ISO standard.
forrestthewoods•1mo ago
That said, we all learn this one! I spent like two weeks debugging a super rare desync bug in a multiplayer game with a P2P lockstep synchronous architecture.
Suffice to say I am now a zealot about providing default values all the time. Thankfully it’s a lot easier since C++11 came out and lets you define default values at the declaration site!
titzer•1mo ago
kevin_thibedeau•1mo ago
ablob•1mo ago
You don't want to zero out the memory? Slap a "foo = uninitialized" in there to have that exact behavior and get the here be demons sign for free.
forrestthewoods•1mo ago
Uninitialized state is totally fine as an opt-in performance optimization. But having a well defined non-garbage default value should obviously be the default.
Did C fuck that up 50 years ago? Yeah probably. They should have known better even then. But that’s ok. It’s a historical artifact. All languages are full of them. We learn and improve!
1718627440•1mo ago
forrestthewoods•1mo ago
If uninitialization was opt-in you would still be free to "assume uninitialized until proven otherwise". But uninitialized memory is such a monumental catastrophic footgun that really is not a justifiable reason to make that default behavior. Which, again, is why no modern languages make that (terrible) design choice.
1718627440•1mo ago
AlotOfReading•1mo ago
That purpose would be better served by reclassifying uninitialized reads as erroneous behavior, which they are for C++26 onwards. What useful purpose is served by having them be UB specifically?
torstenvl•1mo ago
Plenty of things are UB just because major implementations do things wildly differently. For example:
Having initialization be UB means that implementations where it's zero cost can initialize them to zero, or implementations designed for safety-critical systems can initialize them to zero, or what have you, without the standard forcing all implementations to do so.AlotOfReading•1mo ago
https://godbolt.org/z/ncaKGnoTb
forrestthewoods•1mo ago
1718627440•1mo ago
masklinn•1mo ago
Rather "if the implementation doesn't say otherwise".
Generally speaking compiler writers are not mustache-twirling villains stroking a white cat thinking of the most dastardly miscompilation they could implement as punishment. Rather they implement optimisation passes hewing as close as they can to the spec's requirements. Which means if you're out of the spec's guarantees you get whatever emergent behaviour occurs when the optimisation passes run rampant.
torstenvl•1mo ago
Every asm or IR instruction is emitted by the compiler. It isn't a "doesn't say otherwise" kind of thing. Whatever the motivations are, the compiler and its authors are responsible for everything that results.
"if you're out of the spec's guarantees you get whatever emergent behaviour occurs" is simply and patently not factual. There isn't a single compiler in existence for which this is true. Every compiler makes additional guarantees beyond the ISO standard, sometimes due to local dialect, sometimes due to other standards like POSIX, sometimes controlled by configuration or switches (e.g., -fwrapv).
1718627440•1mo ago
Yes, I couldn't assume that such code can be deleted safely. Not sure, if people really rely on it, given that it doesn't work.
> erroneous behavior
So they finally did the thing and made the crazy optimizations illegal?
> If the execution of an operation is specified as having erroneous behavior, the implementation is permitted to issue a diagnostic and is permitted to terminate the execution of the program.
> Recommended practice: An implementation should issue a diagnostic when such an operation is executed. [Note 3: An implementation can issue a diagnostic if it can determine that erroneous behavior is reachable under an implementation-specific set of assumptions about the program behavior, which can result in false positives. — end note]
I don't get it at all. The implementation is already allowed to issue diagnostics as it likes including when the line number of the input file changes. In the case of UB it is also permitted to emit code, that terminates the program. This sounds all like saying nothing. The question is what the implementation is NOT allowed to do for erroneous behaviour, that would be allowed for undefined behaviour.
Also if they do this, does that mean that most optimizations are suddenly illegal?
Well, yeah the compiler can assume UB never happens, optimizes and that can sometimes surprise the programmer. But I the programmer also program based on that assumption. I don't see how defining all the UB serves me.
kevin_thibedeau•1mo ago
andrewaylett•1mo ago
tialaramex•1mo ago
So these variables will be more or less what the current "defanged" Rust std::mem::uninitialized() function gets you. A bit slower than "truly" uninitialized variables, but not instant death in most cases if you made a mistake because you're human.
Those C++ people who feel they actually need uninitialized variables can tell the compiler explicitly [for that particular variable] in C++ 26 that they opt out of this safeguard. They get the same behaviour you've seen described in this thread today, arbitrary Undefined Behaviour if you read the uninitialized variable. This would be similar to modern Rust's MaybeUninit::uninit().assume_init() - you are explicitly telling the compiler it's OK to set fire to everything, you should probably not do this, but we did warn you.
koyote•1mo ago
I have production code where we specifically do not initialise some data in order to be more performant (it gets set before it is read, but not at declaration as we do not have the values at that point).
I do agree that this (and numerous other footguns) make C++ a pain to work with. I also think it's too late to fix.
Ideally, all values would be initialised by default and instead you could forcefully construct something that is not initialised (e.g. something like `no_init double x[100];`). Instead, we have the bug-prone default and twenty different ways to initialise something, each with their own caveats.
C++ would be a much better language if most if not all defaults were reversed.
simonask•1mo ago