> 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.
The C++ committee has a habit of thumbing its nose at standard practice. They intentionally broke bitwise operators on volatiles because they wanted to be impose their atomic religion everywhere. Then they had to walk that back after they broke every embedded library directly manipulating hardware registers.
Empty infinite loops are also commonplace in embedded C once main is done with init and within exception handlers. They don't care about anything beyond their narrow systems programming worldview.
By narrow luck compiler writers so far have been the sane bunch, and have ignored C++ committee on many important points. Thus we still have explicitly non-conformant things like -fno-exceptions that lets one use C++ compiler on embedded.
But I wonder how long that can last, with the way C++ is going.
At one point, it will make practical sense to update codebase to some other language, rather than keep fighting this one
And memcpy is kind of special to C/C++ compilers. Sure, it exists as a function, but it will often have special purpose code generated for that particular location.
It’s obvious why you want to inline memcpy, but the specialization is more interesting. For example, I’ve seen the compiler optimize a memcpy with a static number of bytes and then use SIMD registers to do the copying with no loop at all. It can even be smart enough to take advantage of memory alignment for this.
> 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
It wouldn't work when this kind of loop is generated by macros/templates in some unreachable case left after const folding.
If it's truly unreachable then it's not likely to be a problem. If it is reachable and it's emerging from some macros and templates then I would be more inclined want a warning for it.
It's catastrophic actually. Like disastrously catastrophic. It started with C++20 mostly, and has only kept getting worse from then. See zero initializing variables by default (WHY?) compare/meta including half the STL and HARDCODING those symbols, std::initializer_list being in the std namespace (if you don't include <initializer_list> you literally can't use it, and there is no such thing as a __initializer_list or some internal symbol), the entire coroutine library where you MUST provide coroutine_handle, noop_coroutine, suspends et al (coroutines aren't that bad because they're not necessarily spaghetti).
<meta> is the single WORST OFFENDER, where they hardcode std::vector (literally std::vector in the std namespace) std::ranges std::allocator.
I can't find anything saying variables are zero initialized by default in C++20. But the reason to do so is obvious: many bugs are caused by the lack of this, and as long as you can opt out with "= void" or something, it's not violating C++ core principles.
Strictly speaking the standard only requires some pattern that is not tied to program state. Zero works for that, but so do other static patterns like 0xABAB... or the like.
> (WHY?)
The motivation section of the corresponding paper [0] might be interesting. tl;dr: it lets wrong code be wrong without suffering from (all) the consequences of full-blown UB.
As the only observable behaviour of this_thread::yield is forward progress, because of the as-if rule, the compiler doesn't actually need to replace the loop, when running on a runtime that guarantees preemption. That's the case when std::threads are backed by kernel threads. On a M:N implementation, then yes, a yield would need to be added, but that would be desirable.
Interestingly, posix realtime FIFO scheduling doesn't preempt even on kernel thread based implementations, so one reading of the standard would require yield on this case. But that can actually be potentially catastrophic as FIFO scheduling is expected to be deterministic. But realtime scheduling is already beyond the standard: I doubt gcc and clang will do the transformation by default.
In practice the equivalence is necessary to make some obscure corner of the memory model work and prevent some undesirable optimizations; I expect that in practice the compilers, if they implement this at all, will provide an opt-in flag, but they will optimize as-if the call was there.
There needs to be a way to stop this. A trivial infinite loop can be useful such as for getting you into a state where you can attach a debugger and examine state then have execution resume elsewhere.
> 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.
Unlike C++, Rust does not manage exceptions at all; in C++, you must consider situations where exceptions arise.
If panics are set to unwind, you do need to consider it, and the UnwindSafe auto trait is there to help with memory safety, but logical issues can still arise.
In the context of that particular complaint, yes. From what I understand the gist of it is basically that you should be able to tell what is going on by looking at the code locally (i.e., the code is "explicit").
> I think Linus's complain was before there was a c++ standard.
These emails [0]? IIRC those are the most well-known ones and they are from the mid-2000s
An empty loop, under some non-obvious conditions, on some compiler flags but not others, silently transforms into a system call. In a systems programming language.
Not only that, the code was wrong. The specification is quite clear that correct programs don't cause UB to be executed at run time. If your wrong code now produces wrong results, that's because it's wrong. That your compiler allowed you to get away with it for decades is a compiler bug, not a feature.
Do I fully believe all of the above? Not exactly. But compiler authors do. Does it make a really good argument to never use C or C++? Yes. If only we had 50 years of optimization work in any language with better semantics.
I grilled an LLM for a bit to see if it could justify the old forward progress rule. The only thing I got that passed the smell test was that it’s useful for the optimizer to be able to optimize:
You will find the answer you seek not from an LLM, but from the talk Forward Progress Guarantees in C++ by Olivier Giroux at CppNow 2023. It's a long talk, with lots of details about forward progress, but I've set the timestamp[1] to the infinite loop bit.
It's not even about optimizing some big tech codebase by 0.5%. The progress guarantees in particular are in place s.t. Nvidia can choose a certain implementation strategy in Cuda C++ that has "surprising" consequences for users (one thread getting stuck in an infinite loop that never yields can livelock its entire warp) but still get to claim "full C++ standards compliance".
Yeah, I don't get it either. Like if I wanted to call std::thread::yield() inside an infinite loop, I could, you know, just do that myself?
An obvious question (that TFA does not address) is, why is the forward-progress guarantee needed? Since that is the ostensible justification for this new invisible behavior.
The biggest headache will probably be it getting emitted in inappropriate contexts: where there is no actual means to sched_yield for whatever reason (bare metal, kernel, whatever). The second is just that the behaviour of the infinite loop changes: suddenly you're getting a bunch of extra system calls from your spinning thread instead of just a high CPU usage, which could disguise the issue or perhaps cause problems for other parts of the system. I don't see a good reason for the transformation: pretty much any time you are writing a bare infinite loop like this you don't want anything else to happen (it's also silly that it only happens with a particular spelling of an infinite loop, keeping the others still undefined).
The language is already littered with these "the compiler shall insert" and then a reference to the STANDARD LIBRARY FEATURE N.X. Which means if you're compiling in a freestanding environment half the time you'll get linker errors such as "couldn't find symbol whatever". And what's worse the compiler inserts a call to a function that is LITERALLY STD NAMESPACED. Meaning you have to provide that signature yourself. See how std vector is hardcoded into compare/meta and I can't remember what else.
This then forces developers to create undefined behaviour because according to the standard you can't namespace std your own functions even though it's required to get it to work.
to be designed to play nice with the scheduler, while I would assume a infinite loop
while(true);
to not play nice with the scheduler. Now, I can't really imagine where this matters except for horrible hacky attempts at faking a real time scheduler on windows, but breaking horrible hacky attempts at faking a real time scheduler sounds like the kind of bug you hear about in the evening news.
Forward progress guarantee is what allows for conversion between recursion and iteration for performance optimization. Otherwise these have different characteristics (recursion blows the stack, a loop hangs).
I don't understand your problem. Did you expect your C++ program to get uninterrupted access to the computer? What progression do you think isn't happening there?
I think you are misinterpreting that. That phrase unambiguously says the loop is preserved on the final binary.
I expect an infinite loop to be compiled into, for instance, a jump instruction jumping to itself. The OS, if there is any, is welcome to interrupt and context switch. I don't expect code that has no function calls at all to have a system call inserted into it.
Any program in an OS only gets as much resources allocated to it as the OS allows (OK, in any general-purpose OS written in the past few decades). sched_yield() doesn't actually reduce that allocation in most cases, anyhow: in fact it has a higher chance of increasing the resources that the thread uses spinning in a loop because it's gonna be thrashing the scheduler as well.
> 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.
That says why they don't want it to be UB. The question, I believe, was why they want statically-known-infinite non-trivial loops to continue being UB.
One minor nit, it's not halt-on-error, it's restart-on-error since the watchdog then restarts the system. The technical term for this is rejuvenation and it's standard practice in SCADA and similar to deal with this-shouldn't-happen error conditions.
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.
I only use it for error handling and 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):
UB according to the standard committee is "we didn't think of it". It's not literal UB it's well known what it compiles down to, every time.
(.loop:
jmp .loop)
It's not "we didn't think of it", it's literally "the standard has nothing to say about it", which means that any standard-conforming implementation is free to do whatever it wants, meaning that different implementations may handle it differently.
> It's not literal UB it's well known what it compiles down to, every time. (.loop: jmp .loop)
That might be true for a particular version of a particular compiler, but if you assume that it's true for all standard-conforming compilers (now and in the future) then you're making an assumption that is not supported by the standard.
> the implementation may assume any thread will eventually do one of the following: terminate, call a library I/O function, access a volatile glvalue, or perform a synchronization or atomic operation
Why is that rule needed? I could make my for loop try to solve the halting problem and it'll never finish either, circumventing that rule
I don't see how it can be useful. It's almost always an error to write such a loop. The only reason for it to exist is in very low-level code to do nothing, but for such cases using something like an external function written in assembly is perfectly fine, no C++ standard changes are necessary. It's even makes things harder by complicating the standard with little to no benefits in exchange.
On the other hand the idea that repeated iteration warrants a carve out is in itself curious.
I'm sure there will be some bullshit example of how after inlining you can find repetition like this but clearly other languages get along fine without prohibiting infinite loops.
Furthermore, if the goal was to allow for code motion between identical loops absent side effects they could have just said that and spared the ordinary infinite loop.
In a world where C++ is a language unrelated to C another reasonable position would have been to prohibit spelling loops that cannot terminate and provide a fix it for the possible meanings (unreachable, spin).
Injecting a side effect to solve this issue is just horrendous
You can write very low-level code without mucking around with assembly. For example, it's obvious that ARM's cortex-M cores were designed to be possible to code for entirely in C. For example, the interrupt mechanism follow the platform's calling convention so an interrupt vector can just be a plain C function. And something being usually an error doesn't make it a good idea to be undefined, nor does it explain the behavior that they have defined.
For me everything more than c with classes is too much cognitive load. Templates my by ok for implementing Generics but most of the other changes this comitee has produces are complete waste of brain i think
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
> Probably not something you’d encounter in practice
it's actually probably the most common footgun you'll encounter in practice: non-void functions with no return statements just keep executing past their end. ask me how i know.
compile with -Wreturn-type if you want to avoid such things...
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?
the 2nd call might happen internally due to branch prediction but in practice it shouldn't and the processor fixes this
Oh yeah and TFA also goes with:
> The funny bit is that C got this right.(...) but C included one more rule: loops whose controlling expression is a constant expression may not be assumed to terminate.
Well, duh! A broken clock is right twice a day it seems
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)
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".
I suspect it's more a chain of: emitting the ret is unnecessary because the infinite loop will never return -> emitting the infinite loop is unnecessary because there's no side effects within it and it's undefined behaviour -> emitting any setup for the function is necessary because it's doing nothing else (all probably decisions from different stages of the compiler).
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 CPU doesn't really see functions, it just sees instructions. Functions are a convention on top of the machine code.
Not really true, most instructions set have instructions specifically to implement functions as found in normal programming languages. x86 has CALL and RET for example.
> [The C rule was rejected for C++ because it] could inhibit useful optimizations
If be curious if these are the sorts of optimizations I would find useful to the point where I would be happy to pay the price of this annoying new behaviour.
Or are they just the sorts of optimizations that a compiler writer finds useful who is engaged in a multi year career-defining pissing contest with a competing team?
Don't get me wrong, I have myself engaged in a multi year career-defining pissing contest with a competing team. It's fun. But let's not kid ourselves that it's for the users' sake.
The bizarre thing is that the C rule is pretty deliberately narrowly scoped to still enable those optimizations, and the new C++ definition pretty much follows it except for this extra bit they tagged on that no-one was asking for.
> The mentioned proposal was also accepted as a defect report, so implementations may apply the fix to earlier C++ modes as well. That is why you might not be able to reproduce the old behaviour on a recent compiler even in C++20 mode.
brutal. hope major compiler vendors throw in a flag that can bring some sanity to this
... thankfully. Gives many of us well-paid jobs, and the inexplicable joy of archeology (why certain decisions were made at some point in the nineties, and what buggy implementation a bits header is fixing).
And I'm not even snarky here. I kinda like to do this.
The mere concept of undefined behavior is hilarious to me. "Oh this part? No we can't and won't even try figuring out what doing that does, this page intentionally left blank; yes we are a very serious whole ass standards body thanks for asking"
The purpose is to free optimizers from solving the halting problem (and similar undecidable propositions), which they can’t. So the approach is to reduce the allowable programs to those that optimizers can reliably reason about. By the very nature of the problem, these programs cannot in general be distinguished by an algorithm, because again that would require solving the halting problem. So the non-allowable programs are simply declared to be out-of-scope (aka UB).
It’s a controversial trade-off to be sure, but it’s not like there isn’t a sound logic to it.
> How did we get here?
... introduced in C++11 alongside threading support. The standard says that the implementation may assume any thread will eventually do one of the following: terminate, call a library I/O function, access a volatile glvalue, or perform a synchronization or atomic operation.
I'm more surprised it passed through the committee, they should've seen that back in 2011. I can not imagine such a bug in spec would pass through a Java committee, as they discuss every little thing for years (sometimes decades). It's not like embedded code is something new.
It just spins the CPU in the loop, stopping execution from progressing. Technically, whether this fully halts the system depends on what else is going on: you might need to fully disable interrupts before entering the loop to get a full halt. OTOH you can design your system so that everything happens in interrupts (with modern interrupt controllers the common wisdom of doing as little as possible in interrupts no longer applies and it can be a good way to get a predictable and low-latency system) and so you finish your setup code with an infinite loop to stop the CPU running off the end of your function when it's not executing one of the interrupts.
In a lot of cases, you might insert some 'wait-for-interrupt' type instruction in the loop that halts the CPU more 'cleanly' (and in a lower power mode), and usually this will appear as a side-effect and keep the behaviour defined. But this is not always desirable or possible.
as the kind of person who has been reading the jargon file for fun since the 90s, I thought I had at least a passing familiarity with a lot of hackish slang from the old days. today i learned about nasal demons as a phrase for undefined behavior. i supposed there's still fossils in the dirt
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).
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.
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
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.
An infinite loop which does nothing is practically useless. So, compilers optimize it out. That's the whole philosophy of modern compilers - to reduce execution time by preserving semantics. In case of an infinite loop elimination it's an optimization making code infinite times faster.
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.
And where to you think all that "wasted" energy/cycles would go otherwise? Why do you presume there's some other, more efficient way the CPU could be spending its time while waiting for an event to process?
I, the programmer, will decide what cycles are wasted or not. That the C++ committee thought they knew better is hubris.
Non-trivial infinite loops are very much not an "idiot" thing on embedded systems. "Run until power off" or "run until the warhead detonates" are perfectly normal things to do in that world.
> 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:
If this is a genuine use case, I wonder why the language can't just introduce a built-in function for it. For example, std::get_stuck_here(). Then the compiler would know not to optimize this away. The implementation under the hood could still be an infinite loop, but the compiler would not have to guess why it's there.
> 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 reason for this is interesting. Loop constructs that you're guaranteed to enter have implications for control flow (in every language, not just Rust). It means that the following program is valid in Rust:
let x; // declared, but uninitialized variable
loop { // control flow is guaranteed to enter this loop
if some_condition() {
x = 42; // initialize x
break;
}
}
foo(x); // Rust knows that x is initialized as of here in all possible paths
In contrast, while loops check their condition before entering, which means the entire loop body might be skipped. Languages which guarantee initialization-before-use might special-case certain conditions for while loops as a hint to the control flow analysis (e.g. Java special-cases `while(true)`), but obviously this doesn't generalize to arbitrary conditions.
Interestingly, this all suggest that, in C-like languages, the more natural implementation of an infinite loop should not be `while(true)` nor `for(;;)`, but rather `do {} while(true)`, because do-while are also guaranteed to enter their body (and note that Rust doesn't feature do-while loops).
> 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.
The C++ committee has a habit of thumbing its nose at standard practice. They intentionally broke bitwise operators on volatiles because they wanted to be impose their atomic religion everywhere. Then they had to walk that back after they broke every embedded library directly manipulating hardware registers.
Empty infinite loops are also commonplace in embedded C once main is done with init and within exception handlers. They don't care about anything beyond their narrow systems programming worldview.
By narrow luck compiler writers so far have been the sane bunch, and have ignored C++ committee on many important points. Thus we still have explicitly non-conformant things like -fno-exceptions that lets one use C++ compiler on embedded.
But I wonder how long that can last, with the way C++ is going.
At one point, it will make practical sense to update codebase to some other language, rather than keep fighting this one
3 replies →
> They intentionally broke bitwise operators on volatiles because they wanted to be impose their atomic religion everywhere.
Could you elaborate on this?
1 reply →
GNU C does the same: memory copies can be optimized into memcpy, various operations can be realized as calls into libgcc, etc.
And memcpy is kind of special to C/C++ compilers. Sure, it exists as a function, but it will often have special purpose code generated for that particular location.
It’s obvious why you want to inline memcpy, but the specialization is more interesting. For example, I’ve seen the compiler optimize a memcpy with a static number of bytes and then use SIMD registers to do the copying with no loop at all. It can even be smart enough to take advantage of memory alignment for this.
Those do for the most part correspond to operations which make sense in an embedded context, though.
> 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
It wouldn't work when this kind of loop is generated by macros/templates in some unreachable case left after const folding.
If it's truly unreachable then it's not likely to be a problem. If it is reachable and it's emerging from some macros and templates then I would be more inclined want a warning for it.
2 replies →
It's catastrophic actually. Like disastrously catastrophic. It started with C++20 mostly, and has only kept getting worse from then. See zero initializing variables by default (WHY?) compare/meta including half the STL and HARDCODING those symbols, std::initializer_list being in the std namespace (if you don't include <initializer_list> you literally can't use it, and there is no such thing as a __initializer_list or some internal symbol), the entire coroutine library where you MUST provide coroutine_handle, noop_coroutine, suspends et al (coroutines aren't that bad because they're not necessarily spaghetti).
<meta> is the single WORST OFFENDER, where they hardcode std::vector (literally std::vector in the std namespace) std::ranges std::allocator.
I can't find anything saying variables are zero initialized by default in C++20. But the reason to do so is obvious: many bugs are caused by the lack of this, and as long as you can opt out with "= void" or something, it's not violating C++ core principles.
6 replies →
> See zero initializing variables by default
Strictly speaking the standard only requires some pattern that is not tied to program state. Zero works for that, but so do other static patterns like 0xABAB... or the like.
> (WHY?)
The motivation section of the corresponding paper [0] might be interesting. tl;dr: it lets wrong code be wrong without suffering from (all) the consequences of full-blown UB.
[0]: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p27...
3 replies →
As the only observable behaviour of this_thread::yield is forward progress, because of the as-if rule, the compiler doesn't actually need to replace the loop, when running on a runtime that guarantees preemption. That's the case when std::threads are backed by kernel threads. On a M:N implementation, then yes, a yield would need to be added, but that would be desirable.
Interestingly, posix realtime FIFO scheduling doesn't preempt even on kernel thread based implementations, so one reading of the standard would require yield on this case. But that can actually be potentially catastrophic as FIFO scheduling is expected to be deterministic. But realtime scheduling is already beyond the standard: I doubt gcc and clang will do the transformation by default.
In practice the equivalence is necessary to make some obscure corner of the memory model work and prevent some undesirable optimizations; I expect that in practice the compilers, if they implement this at all, will provide an opt-in flag, but they will optimize as-if the call was there.
There needs to be a way to stop this. A trivial infinite loop can be useful such as for getting you into a state where you can attach a debugger and examine state then have execution resume elsewhere.
> 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.
Unlike C++, Rust does not manage exceptions at all; in C++, you must consider situations where exceptions arise.
If panics are set to unwind, you do need to consider it, and the UnwindSafe auto trait is there to help with memory safety, but logical issues can still arise.
It’s way way more rare in Rust though.
Is it hidden if it's explained in the standard?
I think Linus's complain was before there was a c++ standard. An updated version of the complaint would be "this shit is doing too much".
> Is it hidden if it's explained in the standard?
In the context of that particular complaint, yes. From what I understand the gist of it is basically that you should be able to tell what is going on by looking at the code locally (i.e., the code is "explicit").
> I think Linus's complain was before there was a c++ standard.
These emails [0]? IIRC those are the most well-known ones and they are from the mid-2000s
[0]: https://harmful.cat-v.org/software/c++/linus
An empty loop, under some non-obvious conditions, on some compiler flags but not others, silently transforms into a system call. In a systems programming language.
6 replies →
> When both conditions are met, the loop body is replaced with a call to std::this_thread::yield().
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.
I guess given that it was UB before, the compiler was already allowed to put a system call here if it wanted for some reason
Not only that, the code was wrong. The specification is quite clear that correct programs don't cause UB to be executed at run time. If your wrong code now produces wrong results, that's because it's wrong. That your compiler allowed you to get away with it for decades is a compiler bug, not a feature.
Do I fully believe all of the above? Not exactly. But compiler authors do. Does it make a really good argument to never use C or C++? Yes. If only we had 50 years of optimization work in any language with better semantics.
3 replies →
Yes, it was a horrible situation that has been replaced by an only slightly less horrible situation.
1 reply →
Yes, but now it has to. I guess that's better? than UB, maybe.
I grilled an LLM for a bit to see if it could justify the old forward progress rule. The only thing I got that passed the smell test was that it’s useful for the optimizer to be able to optimize:
by moving the store before the computation. (Stronger stores would require additional analysis.)
I admit I’m unconvinced that this is particularly useful.
(I got many other ideas that did not pass my personal smell test.)
You will find the answer you seek not from an LLM, but from the talk Forward Progress Guarantees in C++ by Olivier Giroux at CppNow 2023. It's a long talk, with lots of details about forward progress, but I've set the timestamp[1] to the infinite loop bit.
[1]: https://youtu.be/g9Rgu6YEuqY?si=_l9JwKhjvIdFEDEX&t=3819
I thought it was generally so the compiler can merge two computation loops without proving if one of them runs forever .
1 reply →
But the compilers have to optimize the crap code in big tech codebases by 0.5%, it saves a lot of money.
Also performance doesn't matter that much and developer time is more important btw, keep using react.
It's not even about optimizing some big tech codebase by 0.5%. The progress guarantees in particular are in place s.t. Nvidia can choose a certain implementation strategy in Cuda C++ that has "surprising" consequences for users (one thread getting stuck in an infinite loop that never yields can livelock its entire warp) but still get to claim "full C++ standards compliance".
10 replies →
performance doesn't matter.... so datacenters would be equally happy running software that runs half as fast but uses 10% more power?
Yeah, this is almost the worst way they could choose to 'fix' the problem.
> An infinite loop should compile to an infinite loop.
I think that a compiler option should control this. It can be a nice optimization, but the programmer should be able to opt out.
Every flag that changes the semantics of the code bifurcates the language into two languages.
4 replies →
The programmer can opt out by terminating the loop.
Yeah, I don't get it either. Like if I wanted to call std::thread::yield() inside an infinite loop, I could, you know, just do that myself?
An obvious question (that TFA does not address) is, why is the forward-progress guarantee needed? Since that is the ostensible justification for this new invisible behavior.
I'm curious, what exactly do you imagine going wrong here?
The biggest headache will probably be it getting emitted in inappropriate contexts: where there is no actual means to sched_yield for whatever reason (bare metal, kernel, whatever). The second is just that the behaviour of the infinite loop changes: suddenly you're getting a bunch of extra system calls from your spinning thread instead of just a high CPU usage, which could disguise the issue or perhaps cause problems for other parts of the system. I don't see a good reason for the transformation: pretty much any time you are writing a bare infinite loop like this you don't want anything else to happen (it's also silly that it only happens with a particular spelling of an infinite loop, keeping the others still undefined).
7 replies →
The language is already littered with these "the compiler shall insert" and then a reference to the STANDARD LIBRARY FEATURE N.X. Which means if you're compiling in a freestanding environment half the time you'll get linker errors such as "couldn't find symbol whatever". And what's worse the compiler inserts a call to a function that is LITERALLY STD NAMESPACED. Meaning you have to provide that signature yourself. See how std vector is hardcoded into compare/meta and I can't remember what else.
This then forces developers to create undefined behaviour because according to the standard you can't namespace std your own functions even though it's required to get it to work.
2 replies →
I would expect an infinite loop
to be designed to play nice with the scheduler, while I would assume a infinite loop
to not play nice with the scheduler. Now, I can't really imagine where this matters except for horrible hacky attempts at faking a real time scheduler on windows, but breaking horrible hacky attempts at faking a real time scheduler sounds like the kind of bug you hear about in the evening news.
2 replies →
I would expect quite a number of embedded use cases to suddenly break. Yay, free CVEs!
Forward progress guarantee is what allows for conversion between recursion and iteration for performance optimization. Otherwise these have different characteristics (recursion blows the stack, a loop hangs).
I don't understand your problem. Did you expect your C++ program to get uninterrupted access to the computer? What progression do you think isn't happening there?
I think you are misinterpreting that. That phrase unambiguously says the loop is preserved on the final binary.
I expect an infinite loop to be compiled into, for instance, a jump instruction jumping to itself. The OS, if there is any, is welcome to interrupt and context switch. I don't expect code that has no function calls at all to have a system call inserted into it.
8 replies →
Any program in an OS only gets as much resources allocated to it as the OS allows (OK, in any general-purpose OS written in the past few decades). sched_yield() doesn't actually reduce that allocation in most cases, anyhow: in fact it has a higher chance of increasing the resources that the thread uses spinning in a loop because it's gonna be thrashing the scheduler as well.
> 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:
- "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...
Nice underhanded coding technique / bugdoor material once the "infinite loops are not UB" notion is widespread enough. :P
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...
Edit: sorry, missed the UB bit.
That says why they don't want it to be UB. The question, I believe, was why they want statically-known-infinite non-trivial loops to continue being UB.
One minor nit, it's not halt-on-error, it's restart-on-error since the watchdog then restarts the system. The technical term for this is rejuvenation and it's standard practice in SCADA and similar to deal with this-shouldn't-happen error conditions.
That's more why you would want them to be defined in the first place.
See https://news.ycombinator.com/item?id=49760653.
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.
I only use it for error handling and 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):
Ask your compiler vendor for a -fallow-infinite-loops
UB according to the standard committee is "we didn't think of it". It's not literal UB it's well known what it compiles down to, every time. (.loop: jmp .loop)
It's not "we didn't think of it", it's literally "the standard has nothing to say about it", which means that any standard-conforming implementation is free to do whatever it wants, meaning that different implementations may handle it differently.
> It's not literal UB it's well known what it compiles down to, every time. (.loop: jmp .loop)
That might be true for a particular version of a particular compiler, but if you assume that it's true for all standard-conforming compilers (now and in the future) then you're making an assumption that is not supported by the standard.
> It's not literal UB it's well known what it compiles down to, every time. (.loop: jmp .loop)
...Uh, the example shown at the literal top of the blog demonstrates precisely the opposite?
> the implementation may assume any thread will eventually do one of the following: terminate, call a library I/O function, access a volatile glvalue, or perform a synchronization or atomic operation
Why is that rule needed? I could make my for loop try to solve the halting problem and it'll never finish either, circumventing that rule
Compute the Ackermann function, write the result, terminate.. when the sun goes red giant and swallows the earth.
See https://news.ycombinator.com/item?id=49760653.
So the compiler can merge two computation loops without proving termination.
I don't see how it can be useful. It's almost always an error to write such a loop. The only reason for it to exist is in very low-level code to do nothing, but for such cases using something like an external function written in assembly is perfectly fine, no C++ standard changes are necessary. It's even makes things harder by complicating the standard with little to no benefits in exchange.
On the other hand the idea that repeated iteration warrants a carve out is in itself curious.
I'm sure there will be some bullshit example of how after inlining you can find repetition like this but clearly other languages get along fine without prohibiting infinite loops.
Furthermore, if the goal was to allow for code motion between identical loops absent side effects they could have just said that and spared the ordinary infinite loop.
In a world where C++ is a language unrelated to C another reasonable position would have been to prohibit spelling loops that cannot terminate and provide a fix it for the possible meanings (unreachable, spin).
Injecting a side effect to solve this issue is just horrendous
You can write very low-level code without mucking around with assembly. For example, it's obvious that ARM's cortex-M cores were designed to be possible to code for entirely in C. For example, the interrupt mechanism follow the platform's calling convention so an interrupt vector can just be a plain C function. And something being usually an error doesn't make it a good idea to be undefined, nor does it explain the behavior that they have defined.
For me everything more than c with classes is too much cognitive load. Templates my by ok for implementing Generics but most of the other changes this comitee has produces are complete waste of brain i think
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
That's kind of what you get with UB; the compiler doesn't need to do what you expect.
> Probably not something you’d encounter in practice
it's actually probably the most common footgun you'll encounter in practice: non-void functions with no return statements just keep executing past their end. ask me how i know.
compile with -Wreturn-type if you want to avoid such things...
> compile with -Wreturn-type if you want to avoid such things...
Isn't -Wreturn-type enabled by default in both gcc and clang atleast for c++?
1 reply →
C on the other hand puts an implicit `return 0` at the end, but only on the main function for some reason. Very weird.
3 replies →
In C. In C++ it's an error to not return, as it should be.
2 replies →
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?
2 replies →
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
Oh yeah and TFA also goes with:
> The funny bit is that C got this right.(...) but C included one more rule: loops whose controlling expression is a constant expression may not be assumed to terminate.
Well, duh! A broken clock is right twice a day it seems
2 replies →
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)
3 replies →
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".
I suspect it's more a chain of: emitting the ret is unnecessary because the infinite loop will never return -> emitting the infinite loop is unnecessary because there's no side effects within it and it's undefined behaviour -> emitting any setup for the function is necessary because it's doing nothing else (all probably decisions from different stages of the compiler).
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 CPU doesn't really see functions, it just sees instructions. Functions are a convention on top of the machine code.
Not really true, most instructions set have instructions specifically to implement functions as found in normal programming languages. x86 has CALL and RET for example.
https://en.wikipedia.org/wiki/X86_calling_conventions
Of course the compiler can stil optimize by inlining etc., but functions still mostly exist at the assembly level.
1 reply →
> [The C rule was rejected for C++ because it] could inhibit useful optimizations
If be curious if these are the sorts of optimizations I would find useful to the point where I would be happy to pay the price of this annoying new behaviour.
Or are they just the sorts of optimizations that a compiler writer finds useful who is engaged in a multi year career-defining pissing contest with a competing team?
Don't get me wrong, I have myself engaged in a multi year career-defining pissing contest with a competing team. It's fun. But let's not kid ourselves that it's for the users' sake.
The bizarre thing is that the C rule is pretty deliberately narrowly scoped to still enable those optimizations, and the new C++ definition pretty much follows it except for this extra bit they tagged on that no-one was asking for.
> The mentioned proposal was also accepted as a defect report, so implementations may apply the fix to earlier C++ modes as well. That is why you might not be able to reproduce the old behaviour on a recent compiler even in C++20 mode.
brutal. hope major compiler vendors throw in a flag that can bring some sanity to this
Breadcrumbs for "blog", "year", "month" etc are broken and give 404s :(
One can browse other blog entries so it really doesnt matter too much.
This language/ecosystem is just crippled...
... thankfully. Gives many of us well-paid jobs, and the inexplicable joy of archeology (why certain decisions were made at some point in the nineties, and what buggy implementation a bits header is fixing).
And I'm not even snarky here. I kinda like to do this.
The mere concept of undefined behavior is hilarious to me. "Oh this part? No we can't and won't even try figuring out what doing that does, this page intentionally left blank; yes we are a very serious whole ass standards body thanks for asking"
The purpose is to free optimizers from solving the halting problem (and similar undecidable propositions), which they can’t. So the approach is to reduce the allowable programs to those that optimizers can reliably reason about. By the very nature of the problem, these programs cannot in general be distinguished by an algorithm, because again that would require solving the halting problem. So the non-allowable programs are simply declared to be out-of-scope (aka UB).
It’s a controversial trade-off to be sure, but it’s not like there isn’t a sound logic to it.
Sometimes while (true){} doesn't mean anything clever. It just means the system is broken stay here.
I'd much rather have the compiler diagnose an infinite loop than silently pretend that it's not reachable, or that it can be rewritten to a yield.
In other words, I am mentally well.
There's no way for a compiler to reliably determine that a loop is infinite, making it very difficult for the standard to require such determinations.
> How did we get here? ... introduced in C++11 alongside threading support. The standard says that the implementation may assume any thread will eventually do one of the following: terminate, call a library I/O function, access a volatile glvalue, or perform a synchronization or atomic operation.
I'm more surprised it passed through the committee, they should've seen that back in 2011. I can not imagine such a bug in spec would pass through a Java committee, as they discuss every little thing for years (sometimes decades). It's not like embedded code is something new.
If an infinite loop can be both:
1. An infinite busy loop.
2. A thread yield/sleep.
It is by definition undefined behavior. You don't know what you're going to get!
If it's one of those two it's not fully undefined behavior it's just implementation defined. UB can do anything.
You know what you’re getting, the code is right there!
c++ reaching new lows
This is good for Rust!
This actually fixes a case that became a problem for Rust implementations: https://github.com/rust-lang/rust/issues/28728
So yes, it is good for Rust.
1 reply →
Optimizations are nice and all. But they should not ever be allowed to change the behaviour of the program, from what is expected by reading the code.
There are good uses for infinite loops.
The good uses cases for trivial infinite loops is a vanishingly small subset of infinite loops.
It is obviously a subseet. But an important one.
Why does the loop mean halt in that embedded case example?
It just spins the CPU in the loop, stopping execution from progressing. Technically, whether this fully halts the system depends on what else is going on: you might need to fully disable interrupts before entering the loop to get a full halt. OTOH you can design your system so that everything happens in interrupts (with modern interrupt controllers the common wisdom of doing as little as possible in interrupts no longer applies and it can be a good way to get a predictable and low-latency system) and so you finish your setup code with an infinite loop to stop the CPU running off the end of your function when it's not executing one of the interrupts.
In a lot of cases, you might insert some 'wait-for-interrupt' type instruction in the loop that halts the CPU more 'cleanly' (and in a lower power mode), and usually this will appear as a side-effect and keep the behaviour defined. But this is not always desirable or possible.
label: goto label;
as an aside, i've always preferred the zoidberg for (;;) to while(true)
I think you're thinking of (;,,;)
The abrupt shift to LLM slop halfway through is jarring and disgusting to read.
as the kind of person who has been reading the jargon file for fun since the 90s, I thought I had at least a passing familiarity with a lot of hackish slang from the old days. today i learned about nasal demons as a phrase for undefined behavior. i supposed there's still fossils in the dirt
Why is null-terminated C string considered a "billion dollar mistake", but UB isn't?
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.
https://research.swtch.com/ub
2 replies →
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.
1 reply →
This comes from a fundamental misunderstanding of what UB is.
Think of this piece of code - `y * x / y`.
Would you like to simplify it to just `x` ?
You need to either lean on UB to do so or have some magical way to prove that y can not be 0.
Otherwise this transformation changes behavior, and is illegal.
Why not just execute what I wrote? Maybe I'm doing some rounding?
2 replies →
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).
I recommend this explanation of why UB is good and necessary (but C and C++ are doing it wrong, defining some things as UB that really shouldn't be): https://www.ralfj.de/blog/2021/11/18/ub-good-idea.html
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!
The argument is that an infinite loop without side effects isn't a real program. It's not useful for anything except wasting cycles.
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.
4 replies →
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
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.
> the simplest infinite loop
An infinite loop which does nothing is practically useless. So, compilers optimize it out. That's the whole philosophy of modern compilers - to reduce execution time by preserving semantics. In case of an infinite loop elimination it's an optimization making code infinite times faster.
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.
4 replies →
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 where to you think all that "wasted" energy/cycles would go otherwise? Why do you presume there's some other, more efficient way the CPU could be spending its time while waiting for an event to process?
I, the programmer, will decide what cycles are wasted or not. That the C++ committee thought they knew better is hubris.
1 reply →
And how would you generate assembly to keep a microcontroller idle then?
8 replies →
Non-trivial infinite loops are very much not an "idiot" thing on embedded systems. "Run until power off" or "run until the warhead detonates" are perfectly normal things to do in that world.
cant wait for ai to re-write all of the software we wrote in this dogshit programming language
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:
> 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:
If this is a genuine use case, I wonder why the language can't just introduce a built-in function for it. For example, std::get_stuck_here(). Then the compiler would know not to optimize this away. The implementation under the hood could still be an infinite loop, but the compiler would not have to guess why it's there.
2 replies →
Low level code can and should use assembly to get the precise effect they desire in these cases.
5 replies →
> 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.
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.
Can, yes. Must, no.
For Rust the infinite loop is important enough to have its own keyword.
The reason for this is interesting. Loop constructs that you're guaranteed to enter have implications for control flow (in every language, not just Rust). It means that the following program is valid in Rust:
In contrast, while loops check their condition before entering, which means the entire loop body might be skipped. Languages which guarantee initialization-before-use might special-case certain conditions for while loops as a hint to the control flow analysis (e.g. Java special-cases `while(true)`), but obviously this doesn't generalize to arbitrary conditions.
Interestingly, this all suggest that, in C-like languages, the more natural implementation of an infinite loop should not be `while(true)` nor `for(;;)`, but rather `do {} while(true)`, because do-while are also guaranteed to enter their body (and note that Rust doesn't feature do-while loops).
"This is not simply a common pattern on bare metal — it was also undefined behaviour in C++."
Not just X (em dash) but also Y.