I never would have guessed that the unreachable() function would get executed in that example. Probably not something you’d encounter in practice, though I have seen some weird things happen with layers of #ifdef
Until Rust proved actually you can get really good or better performance if the language itself is better. I really don’t know how C++ digs itself out of the UB hole it has dug.
Probably by working together with Rust. Eliminating undefined behavior from unsafe Rust is a big deal for the Rust community at the moment. And given that most unsafe rust code exists to call into C or C++, concepts like pointer provenance need to be extended. And proper pointer provenance guarantees can both decrease UB and increase optimization potential.
This particular case is likely an example of that. Rust used to have this problem, but it wasn't ever intended to. So IIRC it got fixed in LLVM for Rust, and this is probably now C++ taking advantage of that.
And unfortunately that argument would be incorrect, because not only is there a realistic chance of hitting this on embedded systems, the fact that LLVM baked this into its low-level semantics resulted in miscompilations in Rust for a time, where `loop {}` is a valid way to implement a diverging function: https://github.com/rust-lang/rust/issues/28728
Of course the infinite loop should run as expected.
It breaks the most fundamental debugging expectations (such as "delete code until problem disappears") if the fundamental, minimal building blocks of a language, when on their own, do random rubbish.
To understand a program that does something, better first understand a program that does nothing.
As a fan of sensible analogies:
You put a salad bowl with vinegar into the fridge and notice that when you do that, the fridge stinks afterwards. You try again without the vinegar, then without the salad. In C++ world, upon receiving the empty bowl, the fridge detonates ("it is not useful"), blowing up your house. That is not OK.
But if you program a for loop computing the sum from 1 to n, this also gets replaced by a constant (unless you build in debug mode). Why would an empty loop be different?
Yeah the argument here is clear, also rather silly. Either you must accept that your language allows for completely useless computation, or, if the compiler is so good at detecting "unreal programs" it should also refuse to compile them.
Well you leave the C++ realm (execution model), as you should with UB and it depends on implementation. The implementation of the compiler was such that the two functions are placed after each other in the machine code; and if the first function doesn't return, then you continue executing into the code for the next function.
But the compiler assumes the function will make forward progress. If the function does that, it will return, so why doesn’t the compiler emit a function epilogue?
I'm also confused that an uncalled function is even compiled and linked, wouldn't it make sense to remove it entirely if the compiler can detect that it's never called?
If it's declared as static, maybe (well, usually, in my experience. You'll also usually get an unused warning). Otherwise the compiler can't assume some other compilation unit won't want it. Linkers can perform a garbage collection pass but they don't often do it by default and they often need finer grained information from the compiler (see the gcc arguments --ffunction-sections and -Wl,--gc-sections)
I can understand adding the 'unreachable' function to the object file, I can even understand plugging it into the final executable, what I (and most other people) object to is making it the de-facto entry point.
This is literally the opposite behaviour compared to what is written in the source code, even when you "assume the infinite loop terminates".
That's the problem with UB, once you hit it (or even have it in your code), you can't really trust anything about the execution anymore. That the function is called isn't something the compiler does on purpose, it's just that the main function is compiled empty due to the UB and the function directly behind it is executed because the CPU just keeps looking for the next instruction.
The CPU doesn't really see functions, it just sees instructions. Functions are a convention on top of the machine code. What happens in this case is the compiler emits essentially a malformed function: it ends without performing a return, so execution just continues into the next function in memory. You can get the same behaviour by missing a 'return' statement from a function that needs one (though in that case I've also seen kind of the opposite: the function returns into the function two slots up in the stack, essentially returning from the function that called it! Undefined behaviour can utterly destroy normal control flow).
Probably the process was one optimization pass saw that the function will never return due to an infinite loop, and removed the function return from the IR of the function, then a later pass saw that the infinite loop was a no-op and undefined so removed that as well, leaving a function that basically did nothing, not even return.
The assembly gives a bit of a hint as to what's happening.
main:
unreachable():
push rbx
...
Due to the undefined behavior, it decides calling main must be impossible, so the easiest thing to do is just give up, don't bother defining the rest of it. You can also do the same with std::unreachable(). But the label for the function still sticks around for some reason, so when you jump to it, it falls through. Which leads to the really stupid fact that reordering the functions changes the behavior.
I assume there are good reasons they can't just completely delete the label. Maybe it would screw linking, or with cases where you deliberately have multiple labels for the same function. And if the effect is only visible due to undefined behavior, it's not technically wrong. But I have always thought this is such a stupid case, surely it can't be that complex to add a trap instruction, even in an optimized build you shouldn't really care if it slows down a function that's "never called".
The "billion-dollar mistake" was about implicitly nullable values, i.e., allowing a variable with type `T` to also be set to `null`, not null-terminated strings.
Anyway, one argument is that UB is fundamentally useful in languages that are insufficiently type-safe, like C and C++. The "holes" in the specification allow for regions where the compiler can optimize the code in ways you may not expect.
As we have developed more advanced type systems, the utility of undefined behavior has lessened considerably.
Agreed that this is why a lot of people support the current UB situation, but the history of UB makes this feel wrong:
As far as I can tell, C89 did not use performance as a justification for any of its undefined behaviors. They were non-portabilities, like signed overflow and null pointer dereferences, or they were outright bugs, like use-after-free. But now experts like Chris Lattner and Hans Boehm point to optimization potential, not portability, as justification for undefined behaviors. I conclude that the rationales really have shifted from the mid-1980s to today: an idea that meant to capture non-portability has been preserved for performance, trumping concerns like correctness and debuggability.
Probably because null-terminated strings are completely avoidable, whereas some amount of UB is all but required for performance (albeit C and C++ have far too much).
What I found is that this is common in embedded and kernel code as a halt-on-error pattern. When a fatal error occurs and there’s no operating system to exit to, you simply stop:
There isn't ever a good reason to have an infinite loop
That seems to be a very broad statement. For example in a system where interrupts mostly control things this sort of 'do not close the program' could be useful.
A guy I worked with had one I never would think of because I do not work in that field.
The loop must be a trivially empty iteration statement -- meaning its body is literally empty
This seems to say that the loop body can not be "continue". Indeed, I just tried -std=c++26 with ";" and got an infinite loop as promised, but "continue" restores the undefined behavior:
This is unfortunate since I know of one style guide that prefers "continue" over single semicolons. I guess all those code will be doing "while(true) {}" from now on.
When both conditions are met, the loop body is replaced with a call to std::this_thread::yield(). This gives execution of the loop the forward-progress semantics it previously lacked.
That's the epitome of the hidden code downside that Linus and many others dislike about C++. For constructors and destructors it's somewhat unavoidable and not so random, though Rust does better at limiting the blast radius of non-local code, at least in the drop case.
If they didn't want to adopt the C11 rule, the C++ committee should've explored a rule that required the compiler to emit a diagnostic or error for trivial loops (whether as defined by C11 or otherwise), requiring the programmer to explicitly insert ::yield or similar. No hidden code, and less opportunity for the compiler to do surprising things.
The C committee has been rigorously enumerating UB cases in the standard and addressing each case in turn, often by requiring a diagnostic, error, or by turning it into implemention defined behavior. But inserting code like that would be unthinkable.
There are valid use cases for the infinite while(1) loop in microcontroller programming (contrary to popular belief it seems). Autogenerated HAL code for the stm32 uses it for error handlers, and they support C++ so I am surprised this was UB.
Of course it is a bad idea to use this to wait/stall in power sensitive applications, in that case use wake from interrupt.
As an aside, I like to include a software breakpoint in my error handlers. It makes debugging easier without wasting a hardware breakpoint (which are physically limited by the microcontroller):
Breadcrumbs for "blog", "year", "month" etc are broken and give 404s :(
One can browse other blog entries so it really doesnt matter too much.
I never would have guessed that the unreachable() function would get executed in that example. Probably not something you’d encounter in practice, though I have seen some weird things happen with layers of #ifdef
TLDR: For almost 1/6 of a century, the C++ standards broke the simplest infinite loop and only just recently fixed it.
Idiots!
Don’t they really that people write real programs to solve real problems? This isn’t a theoretical academic exercise!
They also realized that people choose compilers based on performance benchmarks, and that insane optimizations let them win.
Until Rust proved actually you can get really good or better performance if the language itself is better. I really don’t know how C++ digs itself out of the UB hole it has dug.
Probably by working together with Rust. Eliminating undefined behavior from unsafe Rust is a big deal for the Rust community at the moment. And given that most unsafe rust code exists to call into C or C++, concepts like pointer provenance need to be extended. And proper pointer provenance guarantees can both decrease UB and increase optimization potential.
IIUC, my understanding is shallow.
This particular case is likely an example of that. Rust used to have this problem, but it wasn't ever intended to. So IIRC it got fixed in LLVM for Rust, and this is probably now C++ taking advantage of that.
The argument is that an infinite loop without side effects isn't a real program. It's not useful for anything except wasting cycles.
And unfortunately that argument would be incorrect, because not only is there a realistic chance of hitting this on embedded systems, the fact that LLVM baked this into its low-level semantics resulted in miscompilations in Rust for a time, where `loop {}` is a valid way to implement a diverging function: https://github.com/rust-lang/rust/issues/28728
Of course the infinite loop should run as expected.
It breaks the most fundamental debugging expectations (such as "delete code until problem disappears") if the fundamental, minimal building blocks of a language, when on their own, do random rubbish.
To understand a program that does something, better first understand a program that does nothing.
As a fan of sensible analogies:
You put a salad bowl with vinegar into the fridge and notice that when you do that, the fridge stinks afterwards. You try again without the vinegar, then without the salad. In C++ world, upon receiving the empty bowl, the fridge detonates ("it is not useful"), blowing up your house. That is not OK.
But if you program a for loop computing the sum from 1 to n, this also gets replaced by a constant (unless you build in debug mode). Why would an empty loop be different?
Yeah the argument here is clear, also rather silly. Either you must accept that your language allows for completely useless computation, or, if the compiler is so good at detecting "unreal programs" it should also refuse to compile them.
You are an idiot if you write an infinite loop. An infinite loop is a waste of CPU cycles and energy when run.
If it wasn't so hard to detect (the trivial cases are easy, but it gets hard quickly) I'd say the program should fail to compile.
And how would you generate assembly to keep a microcontroller idle then?
You call the CPU halt instruction.
i have never before thought that a function could 'fall through' to another function. why does this behavior even exist?
Well you leave the C++ realm (execution model), as you should with UB and it depends on implementation. The implementation of the compiler was such that the two functions are placed after each other in the machine code; and if the first function doesn't return, then you continue executing into the code for the next function.
But the compiler assumes the function will make forward progress. If the function does that, it will return, so why doesn’t the compiler emit a function epilogue?
This makes no sense to me
If I think about asm:
function1:
function2:
main:
the 2nd call might happen internally due to branch prediction but in practice it shouldn't and the processor fixes this
With UB the compiler has no particular requirement to emit the 'ret'. (or, in the example, anything at all for the function)
I'm also confused that an uncalled function is even compiled and linked, wouldn't it make sense to remove it entirely if the compiler can detect that it's never called?
If it's declared as static, maybe (well, usually, in my experience. You'll also usually get an unused warning). Otherwise the compiler can't assume some other compilation unit won't want it. Linkers can perform a garbage collection pass but they don't often do it by default and they often need finer grained information from the compiler (see the gcc arguments --ffunction-sections and -Wl,--gc-sections)
I can understand adding the 'unreachable' function to the object file, I can even understand plugging it into the final executable, what I (and most other people) object to is making it the de-facto entry point.
This is literally the opposite behaviour compared to what is written in the source code, even when you "assume the infinite loop terminates".
That's the problem with UB, once you hit it (or even have it in your code), you can't really trust anything about the execution anymore. That the function is called isn't something the compiler does on purpose, it's just that the main function is compiled empty due to the UB and the function directly behind it is executed because the CPU just keeps looking for the next instruction.
Yeah, that's what UB does. You get to see the arbitrary behaviour of the underlying machine with whatever the compiler produces.
The CPU doesn't really see functions, it just sees instructions. Functions are a convention on top of the machine code. What happens in this case is the compiler emits essentially a malformed function: it ends without performing a return, so execution just continues into the next function in memory. You can get the same behaviour by missing a 'return' statement from a function that needs one (though in that case I've also seen kind of the opposite: the function returns into the function two slots up in the stack, essentially returning from the function that called it! Undefined behaviour can utterly destroy normal control flow).
Probably the process was one optimization pass saw that the function will never return due to an infinite loop, and removed the function return from the IR of the function, then a later pass saw that the infinite loop was a no-op and undefined so removed that as well, leaving a function that basically did nothing, not even return.
The assembly gives a bit of a hint as to what's happening.
Due to the undefined behavior, it decides calling main must be impossible, so the easiest thing to do is just give up, don't bother defining the rest of it. You can also do the same with std::unreachable(). But the label for the function still sticks around for some reason, so when you jump to it, it falls through. Which leads to the really stupid fact that reordering the functions changes the behavior.
I assume there are good reasons they can't just completely delete the label. Maybe it would screw linking, or with cases where you deliberately have multiple labels for the same function. And if the effect is only visible due to undefined behavior, it's not technically wrong. But I have always thought this is such a stupid case, surely it can't be that complex to add a trap instruction, even in an optimized build you shouldn't really care if it slows down a function that's "never called".
as an aside, i've always preferred the zoidberg for (;;) to while(true)
I think you're thinking of (;,,;)
Why is null-terminated C string considered a "billion dollar mistake", but UB isn't?
Null terminated strings were an intentional compromise, known to be inferior for execution but superior for memory
Null being an "allowed" value for pointers is the mistake e.g. what became nullptr. "Allowed" because garbage values are garbage.
Think of the alternative where we'd be dealing with endless issues because someone though 255 or 2^16-1 characters ought to be enough for everyone.
The "billion-dollar mistake" was about implicitly nullable values, i.e., allowing a variable with type `T` to also be set to `null`, not null-terminated strings.
Anyway, one argument is that UB is fundamentally useful in languages that are insufficiently type-safe, like C and C++. The "holes" in the specification allow for regions where the compiler can optimize the code in ways you may not expect.
As we have developed more advanced type systems, the utility of undefined behavior has lessened considerably.
Agreed that this is why a lot of people support the current UB situation, but the history of UB makes this feel wrong:
https://research.swtch.com/ub
Probably because null-terminated strings are completely avoidable, whereas some amount of UB is all but required for performance (albeit C and C++ have far too much).
Unfortunate. There isn't ever a good reason to have an infinite loop so concerned compilers could have just diagnosed this as a warning.
The article mentions a use case for that:
Low level code can and should use assembly to get the precise effect they desire in these cases.
That would be pretty cumbersome though. If you're targeting N different architectures, you would have to write N different assembly blocks.
I shouldn't need to drop to assembly to get an infinite loop that works!
Why not just allow infinite loops instead of having me write assembly for it though?
For Rust the infinite loop is important enough to have its own keyword.
That seems to be a very broad statement. For example in a system where interrupts mostly control things this sort of 'do not close the program' could be useful.
A guy I worked with had one I never would think of because I do not work in that field.
But yeah a warning would probably be useful.
Interrupt driven super loops are very common on bare metal systems.
Compilers can still diagnose something as a warning even if it's not UB.
Sometimes while (true){} doesn't mean anything clever. It just means the system is broken stay here.
Insert screaming here.
An infinite loop, with no library calls whatsoever, gets a system call inserted. That's a horrible surprise waiting to happen.
The entire concept of the "forward progress guarantee" is broken. An infinite loop should compile to an infinite loop. Nothing more, nothing less.
Yeah, this is almost the worst way they could choose to 'fix' the problem.
The article, most unfortunately, doesn't explain why anyone would want infinite loops to be UB in the first place. I found this explanation: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1528.htm
The article mentions it's a halt-on-error pattern:
https://www.sandordargo.com/blog/2026/09/16/cpp26-trivial-in...
This seems to say that the loop body can not be "continue". Indeed, I just tried -std=c++26 with ";" and got an infinite loop as promised, but "continue" restores the undefined behavior:
- "while(true);" -> https://godbolt.org/z/T65o51crx
- "while(true) continue;" -> https://godbolt.org/z/Pj9raEcnP
This is unfortunate since I know of one style guide that prefers "continue" over single semicolons. I guess all those code will be doing "while(true) {}" from now on.
https://google.github.io/styleguide/cppguide.html#Formatting...
That's the epitome of the hidden code downside that Linus and many others dislike about C++. For constructors and destructors it's somewhat unavoidable and not so random, though Rust does better at limiting the blast radius of non-local code, at least in the drop case.
If they didn't want to adopt the C11 rule, the C++ committee should've explored a rule that required the compiler to emit a diagnostic or error for trivial loops (whether as defined by C11 or otherwise), requiring the programmer to explicitly insert ::yield or similar. No hidden code, and less opportunity for the compiler to do surprising things.
The C committee has been rigorously enumerating UB cases in the standard and addressing each case in turn, often by requiring a diagnostic, error, or by turning it into implemention defined behavior. But inserting code like that would be unthinkable.
There are valid use cases for the infinite while(1) loop in microcontroller programming (contrary to popular belief it seems). Autogenerated HAL code for the stm32 uses it for error handlers, and they support C++ so I am surprised this was UB.
Of course it is a bad idea to use this to wait/stall in power sensitive applications, in that case use wake from interrupt.
As an aside, I like to include a software breakpoint in my error handlers. It makes debugging easier without wasting a hardware breakpoint (which are physically limited by the microcontroller):
Why does the loop mean halt in that embedded case example?