221 comments

[ 11.1 ms ] story [ 224 ms ] thread
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
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++?

probably - i guess then i meant -Werror=return-type
C on the other hand puts an implicit `return 0` at the end, but only on the main function for some reason. Very weird.
C++ also special-cases the `main` function. Probably because `main` is the interface to the OS so it gets special language treatment.
In C++ you're not allowed to call main yourself, so that the compiler can call the global constructors at the start of main.
Which also exist as C extensions, in some compilers.
In C. In C++ it's an error to not return, as it should be.
.... The blog post literally demonstrates that it is not. Feel free to repro on your own machine.
The main function in the blog post returns at the end of all control paths. Firstly because main has an implicit "return 0" at the end and secondly because no control paths end. It doesn't jump to unreachable because it lacks a return - it jumps there because that's how this implementation has chosen to compile UB, despite the presence of all necessary returns.
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.
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?
I think the argument is that the equivalent of an infinite loop would be a halt / abort instruction, not a complete removal of the loop and continue running anything else.
Not necessarily what you want in that case either: it's a common pattern in cases where you want the system to halt until you can attach a debugger to inspect the state. A halt/abort instruction that trashes that state would be undesirable (some CPUs have an instruction that is equivalent, but many do not, after all, why bother if you can just write an infinite do-nothing loop?).
Expecting a piece of code to be compiled to a precise sequence of machine instructions is exactly what you should not do with high-level languages like C++. Their task is exactly to abstract the machine away. They give you the guarantee that the final observable result will be what you asked for, not that the means to obtain that result will be what you have in mind.

If you write a loop to zero out some memory, it can be compiled to a loop, or to a call to an optimized predefined function, or even to a sequence of single zeroing instructions, if the size is small enough.

Even a single statement as a=0 may be compiled to a "load immediate" instruction, or an "XOR with itself", or a "sub with itself", or a move from another register known to be 0.

I'm not sure what this has to do with my comment. I am aware of all of this. It would be nice if an infinite loop was defined to be 'do nothing, indefinitely'. On architectures with an instruction that works that works precisely that way, it could be turned into that, perhaps, but that's an implementation detail and should follow the as-if principle (I'm also not sure any compiler would bother).
> I'm not sure what this has to do with my comment

Because you explicitly mentioned details that belong in the implementation, not in the semantic:

> A halt/abort instruction that trashes [the] state would be undesirable

If you want to attach a debugger, then use a breakpoint, don't try to obtain the same effect within the code.

A halt instruction is probably the appropriate representation for an infinite loop - the process gets stuck same as the infinite loop without proceeding. Abort is the instruction you want unreachable() to compile down into. It wasn’t an either or but both depending on the specific behavior you want.
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.
(comment deleted)
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.

If the CPU halts it isn't used at all. If the CPU does an infinite loop then it all goes to heat.

If the loop is doing anything then it cannot be optimized away. Only loops with no side effects meaning they are just turning the CPU into a heater count.

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.
> 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.

But also very different. If code below this loop executes after elimination and wouldnt have before, that is a very significant change in semantics
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?
The compiler can assume that the function will return, but it can also statically deduce that the function cannot return. That's a contradiction, so the compiler deduces that the function is simply UB when called, i.e. no need to emit an epilogue. It's the logical principle of explosion in compiler format, basically.
Because there is an infinite loop that makes the epilogue unreachable, so it is safe for the compiler to remove it!

Sure, that optimization interacts badly with the optimization that removes the infinite loop. But half the point of UB is to avoid needing to deal with such interactions, because they are defined out of existence.

This makes no sense to me

If I think about asm:

function1:

    (do stuff)

    jp function1

    ret

function2:

    (other stuff)

    ret

main:

    call function1

    call function2

the 2nd call might happen internally due to branch prediction but in practice it shouldn't and the processor fixes this
(comment deleted)
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?
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.

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).
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:

> 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

Oh undoubtedly; I didn't mean to imply that performance was the reason behind the origin of UB, though I can see how my comment would read that way. Thank you for adding the note!
This seems to be more about portability of the compilers than of the code. Any behavior that works on one Turing-complete system can be specified and then ported to any other such system.
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).
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?
Because we want compilers to perform the optimizations, not programmers.
Because we want people to write simple idiomatic code that runs fast today and in ten years on alien hardware.

> Maybe I'm doing some rounding?

Compilers won't do this optimization when it is illegal to. If you disagree with the compiler's idea of what is legal, you can either write inline assembly, or put this code into an always inlined, but never optimized function.

Unfortunate. There isn't ever a good reason to have an infinite loop so concerned compilers could have just diagnosed this as a warning.
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.
Warnings are poisoned in C++. It is popular to tell C++ compilers to convert all their warnings into fatal errors. So your only options are "Silently allow probably bad code" and "Program does not compile" because C++ programmers are allergic to nuance.
Oh you mean the -Wall -Werror crowd.
Sometimes while (true){} doesn't mean anything clever. It just means the system is broken stay here.
> 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'm curious, what exactly do you imagine going wrong here?
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.

Freestanding environments specifically don't have to do this, they get a carve-out. (I dunno if this applies elsewhere though.)
Sounds like a compiler bug that a standard feature implementable in freestanding doesn't work in freestanding.
I would expect an infinite loop

    while(true) std::this_thread::yield(); 
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.
under what conditions would the infinite loop be scheduled over something else after it has run out of its time slice?
I would expect them to be the same or for the former to be worse. It's rarely useful to call sched_yield at all, but calling it repeatedly in a loop seems more likely to expose bad behavior in a scheduler than improve the interaction. Schedulers are already perfectly well designed to handle threads trying to take up 100% of the CPU: that's the default state for any CPU-bound task.
I would expect quite a number of embedded use cases to suddenly break. Yay, free CVEs!
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".
So let it livelock the entire warp when someone writes an infinite loop. Should we start replacing integer division by zero with INT_MAX so that people aren't "surprised" by their program crashing?
I mean that's what they did, and that's why there's that UB. All I'm saying is that this is the "weird platform behaviors exist and must be legalized by the standard" kind of UB and not the "we want a 0.5% win for benchmaxxing" kind of UB (the standard has plenty of both).
No, they didn't. UB is a cop out and inserting yield is just plain bad. Locking up one or more threads in an implementation defined manner would be the outcome of least surprise (I already know it's going to lock up at least the one thread).
The standards intended interpretation of UB was always intended to be something like "implementation defined, no documentation required" to allow for implementation weirdness, even unpredictable ones. It was compiler authors who decided do abuse this allowance to do really unintuitive things instead of weird platform weirdness.
100% There are implementations that have sane behavior for "UB" instead of making it an excuse to misbehave.

The ISO standard is not the same as a language from a single vendor.

IIRC a lot of it was benchmark gaming between GCC and LLVM.
Because one can bleeping see that that's what would happen. Locking up a thread isn't a good thing, but it's a lot better than UB. There was never a need to make this UB.

And you can't guarantee forward process. Alan Turing told me so.

Isn't that the strategy for most of the stuff in C++? It's the common denominator of a wide variety of platforms. That's why numbers didn't have to be two's complement and characters didn't have to be ASCII for ages.
I don't see the issue. Just let wrong code do wrong things, but let it do the expected wrong thing.
Good luck defining "expected" for most of the more complex cases.
Seems trivial to me for infinite loops. Nothing special to specify here, they're already specced by the definition of loops.
performance doesn't matter.... so datacenters would be equally happy running software that runs half as fast but uses 10% more power?
> 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.

The programmer can opt out by terminating the loop.
Every flag that changes the semantics of the code bifurcates the language into two languages.
If we accept this definition, C++ already seems to be many different languages. At my job, I'm currently fighting floating-point determinism issues across different build configurations, compilers, CPUs, operating systems, and standard library and libm implementations so that snapshot tests pass with the same hashes on all platforms. I can confirm that this is a complete nightmare.
Yup. I tried hard to not allow D's behaviors to be changed based on a compiler switch. Yes, we have switches to enable certain features, but not silent behavior changes.

It's not perfect, but the forest of such switches in C compilers motivated D to not have them.

Alternatively every flag that changes the semantics of the code is a workaround for either legacy code no one will fix or language committee decisions that have unintended side effects.
Are C/C++ chars signed or unsigned? What a mess!
The state of affairs in C and C++ is abhorrently bad. Not only is there `char` but also `signed char` and `unsigned char`, and the standards seem to leave the signedness interpretation of bare `char` up to the implementation.

Ada explicitly settled on `Character` being an enumeration type based on a specific character set encoding. When I learned Ada 95, the ARM specified the ISO 8859-1 character set for `Character`, likewise with `Wide_Character` and `Wide_Wide_Character` explicitly settling on 16- and 32-bit implementations of UCS. I wish more language specifications made decisions like this.

D following suit with tying its `char`, `wchar`, and `dchar` to UTF-8, UTF-16, and UTF-32 is commendable.

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
Yes, it was a horrible situation that has been replaced by an only slightly less horrible situation.
I don't totally agree with this. To me, UB is an order of magnitude worse than pretty much anything else. I don't necessarily disagree that this isn't still an order of magnitude worse than a proper solution, but I also don't write an C++, so I'm mostly just commenting as an outside observer.
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 is it 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.

Yeah, my slightly more verbose take is that a language that requires you to not ever make any mistakes in order to have a program behave in a predictable way is not a particularly good choice of language if you the ability to pick something else.
The mistake was declaring infinite loops to be UB in the first place.
> That your compiler allowed you to get away with it for decades is a compiler bug

That UB was added in C++11.

That sounds like 1.5 decades to me...
FWIW while(true) / for(;;) (or any other loop condition that is a true constant-expression) is NOT UB in C, only C++.

In any case stuff like __asm__ __volatile__("" ::: "memory") prevent such optimizations in the rare case you do need branch-to-self.

> programs don't cause UB to be executed at run time.

The insidious thing about UB is that it doesn't actually have to be executed to wreck your program. UB is not primarily about runtime behavior, it's about how the compiler interprets your code. The behavior that is undefined is your compiler's behavior.

You're right. I didn't enumerate every way UB can make a program go wrong. I'm sure I don't even know every way that can happen. But "UB doesn't happen at run time" is an assumption that lots of "optimizations" are based on.
Yes, but now it has to. I guess that's better? than UB, maybe.
Oh, definitely agreed. It's still not great, but it's a lot better than before.
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.
Ok, I get this.

The problem is that what you want is completely against the spirit of the entire language.

If your point is that C++ should be more like C in general, I can agree with that. But if your point is that C++ should be literal on this specific case, performance be damned, and the rest of it is ok, then no, that's a bad one.

A call to a standard library function is still subject to the as if rule. It doesn't have to manifest into a call instruction to a standard library function. Much like memcpy in source code doesn't have to manifest to a call instruction.
Me: I don't expect to be stabbed.

You: But you only might be stabbed. It isn't required to happen only permitted.

A bad but conforming implementation of the standard can screw you over on every line of your program.
Yes, but the compiler does have to preserve any observable behavior produced by the call to the standard library function. Being able to omit this inserted yield() by the as if rule would mean that it isn't observable, which would also mean that the compiler could already add or not add it anywhere as needed without changing the behavior of the program. Which would seemingly make the inserted yield() pointless as it would have no effect.
Arguably the only change to program behavior is to performance characteristics on hosted environments. It does not change any observable behavior otherwise.

In environments where there are strong forward progress guarantees a busy infinite loop does the same as far as the abstract machine is concerned, as the OS will eventually put the thread to sleep anyway and other threads can make progress. How soon the thread yields is not "observable behavior" (as defined by the standard document).

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.
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.

(comment deleted)
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 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:

    messy_pure_computation();
    some_atomic.store(1, relaxed);
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 don’t really buy that justification, for three reasons:

1. Most implementations do not do this automatic cooperative multitasking trick and most users don’t want it done to their code.

2. The fact that a “step” is guaranteed to happen in finite time is far too weak for most use cases. I’ve done plenty of kernel programming, and a lot of kernels are partially or fully cooperative scheduled. Even somewhat long loops need manually inserted preemption points.

3. “Finite” can be a very long time indeed. There are literally competitions to see who can make the largest busy beaver machine.

I thought it was generally so the compiler can merge two computation loops without proving if one of them runs forever .
Correct. See N1528: "Why undefined behavior for infinite loops?" https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1528.htm
That’s actually a fairly good answer.

I’m starting to wonder whether newly designed programming languages should explicitly distinguish probably terminating loops from potentially infinite loops. Lean does, for good reason.

Is it a good answer though? How often does this opportunity come up? And if you have two trivial infinite loops one after the other, do you really need to insert `yield()` in order to be able to merge them?
That's not the issue.

Suppose you have some state like this:

    const node *head1;
    int sum1, sum2;
And you have:

    void func()
    {
        for ( int *p = head; p; p = p->next )
            sum1 += p->val1;
    
        for ( int *p = head; p; p = p->next )
            sum2 += p->val2;
    }
The compiler really wants to merge the loops (this will be a nearly 2x speedup in this contrived case). In other words, the compiler would like to generate this instead:

    void func()
    {
        for ( int *p = head; p; p = p->next ) {
            sum1 += p->val1;
            sum2 += p->val2;
        }
    }
Naively, this optimization looks obviously correct: since there is no synchronization in func(), nothing could validly observe the changes in the order of the stores.

Here's the problem. While C and C++ consider data races to be UB (which is why the compiler is allowed to mess with the order in which potentially shared state is written here), the presence of a data race is still observable in a problematic sense. Suppose thread 2 is doing something like this:

    while (true) {
        printf("%d\n", sum2);
    }
If thread 1 calls func() while this loop is running, then the program has undefined behavior [0]. Except there's a really nasty corner case. If the linked list has a cycle, then func() contains an infinite loop. (All it takes to cause this is head->next == head.) And, if func() has an infinite loop then, as originally written, sum2 is never modified and there is not a data race. So a sneaky programmer could set up the infinite loop, call func() in one thread, do the printf loop in another thread, and the compiler would need to run that code correctly because it's not UB. If the compiler transforms func() as above, then it introduces a data race where none existed, and it's a bug.

But this optimization seems important, and C and C++ sidestep this issue by declaring that func() itself is UB if the linked list contains a cycle. So the transformation does not introduce UB in my example because, in the problematic case, the UB is already there in the original code. Problem solved. Yuck.

(Realistically the compiler will also probably accumulate the sum in registers and add to sum1 and sum2 at the end. One could quibble that this subsequent transformation invalidates my point, but it's easy enough to make a slightly more complex example that doesn't have this problem.)

None of this is to say that I like C and C++'s solution. It's gross. The new C++ change to sort-of-solve it is extremely gross.

FWIW (and I sort of alluded to this above), there is an IMO much more interesting reason that compilers should care about infinite loops that doesn't apply to C/C++. In languages like Lean (but borrowing C-like syntax), you can write something like:

ProofType proof() { // some body here }

The entire basis of the proof model in Lean is that the existence of a "term" like proof() that returns the type ProofType implies that an object of ProofType can be constructed (I think this is usually described as saying that ProofType is "inhabited"). This is pretty concrete -- you could literally run proof() to obtain this object.

But infinite loops completely break it: you could just write:

    ProofType proof()
    {
        while (true)
            ;
    }
(Sure, a clever compiler could reject this particular function. But a clever programmer can out-clever the compiler.)

So, in Lean, you either need to prove to the compiler that all your loops terminate or you need to mark the function as "partial", which tells the compiler that it cannot assume that the existence of the function means that the return type is inhabited. This would be a pretty radical change to ...

> But this optimization seems important

This is the only point I don't fully agree with. I'd want to know that this optimization is worthwhile. Is it?

Yes. Loops that manipulate data are extremely common. Combining memory accesses and increasing cache locality are extremely beneficial to performance. Transformations that rewrite loops to increase the chances of locality/combining are therefore likely to be worthwhile.
> Insert screaming here.

I have the suspicion that the members of the C++ standards committee are increasingly not from this planet.

> 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
Yeah they haven't learnt their lesson at all. Fixing subtle UB by making it even more subtle! Mental.
> 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.

> 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.

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.

> 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...

In other words, it's a sane default that you can opt out of on a case by case basis which is the way it should have been all along.
D initializes floating point variables to NaN by default. And chars to 0xFF. Yes it's controversial!
I’d guess the concern is performance, not what initializer value is used. And performance is a valid concern that is discussed in the proposal, and a reason there’s an escape hatch. Still, it might cause some confusion.
AFAIK zero-init is unfortunately the default compiler use (but you can change that)
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.
They were saying the problematic philosophy started in C++ 20, not the variable initialization rule.

Yes the reason is obvious, but it’s neither simple nor black and white. One huge problem is that this can cause serious performance regressions, and you have to change your code to opt out, e.g. add “[[indeterminate]]”. There are many, many cases in high performance computing where the intended & desired behavior is don’t touch my variables until I fill them.

This is changing C++ core principles, there’s a new designation for the state of a variable: erroneous. It’s also subtle and weird, because you can still have well-defined behavior even with erroneous state. It does seem like this might be an experiment though, I don’t think this is the end of the story. (It seems they’re already talking some redesign of this idea.)

What I'm most annoyed at with the variable initialization change is that:

  - It's potentially a performance change in every single function, especially ones that have sizable fixed-size buffers
  - If you have regressions you have to spray [[indeterminate]] everywhere, because there is no coarser way of suppressing it.
  - While the language says unrecognized attributes are ignored, compilers frequently warn on unrecognized attributes. Clang, for instance, currently warns on [[indeterminate]].
  - There is no defined macro name for backwards compatibility.
Which means that libraries are going have to all declare their own macros for [[indeterminate]] and pepper their code with it.
Uninitialized variables were already UB to read, because some architectures have trap representations, even for integers. Every register on Itanium has one.
That's not reason enough to have the compiler initialize.
That's assuming you were reading it without writing to it. There are three common cases when that isn't true.

The first is that you have a fixed buffer large enough for the maximum message size even though the typical ones aren't that big. You most often write 1% of the buffer and read it back, the other 99% is never accessed.

The second is that you always write the entire contents before reading it but the compiler may not be able to see that.

And the third is that you have a code path where that variable is simply not used.

You would then have the compiler emitting instructions to write zeros that are either overwritten before being read or are never read at all.

Moreover, zero initializing the data doesn't actually remove the bugs when that isn't the case. Consider the first case. You have a fixed buffer used to store variable length messages. For the first message the buffer is now zeros instead of uninitialized, but for every subsequent message the remainder of the buffer still contains the remainder of the previous message and subjects you to information disclosure or data modification if you're reading back a different amount than was written in the previous call.

Now consider the second or third case. You unintentionally read from a variable before assigning to it. You get zeros instead of uninitialized memory, but if you weren't expecting zeros, well, the UID field is now 0.

If you are writing to it before reading from it then it's not uninitialised, and in 99% of cases the compiler can see that and will not set it to 0 at its declaration because that's a dead store.
Consider this (quite common) code:

  char buffer[LARGE_SIZE];
  if(maybe_fill_buffer(buffer, sizeof(buffer))) {
      use_result(buffer);
  }
The function maybe_fill_buffer() is an external library function that either fills the buffer and returns true or doesn't modify it and returns false. Or maybe it unconditionally fills it, or unconditionally returns false without accessing it. The compiler can't see any of that though because it's in an external library. For all it knows that function is going to read from it instead of writing to it.

Notice that if it could actually figure it out 99% of the time then it could also emit a warning the 1% of the time that it can't and encourage you to make an explicit choice, which would have been a better option if that was actually the rate.

Zero initializing also hides bugs.

Say you have some code that should not be reading the initial state and is buggy if it does. Without zero-init, valgrind and msan will give you an immediate and false positive message that your code is wrong-- or forget dynamic analysis: the compiler can often statically tell you that the code will use an uninitialized variable. Zero initialize it and you lose that signal.

Making it not UB and just implementation defined / non-determinstic is probably fine (I'd let compiler authors opine there)... but zero init is pretty costly and not necessarily superior from a whole system reliability perspective.

"valgrind and msan will give you an immediate and false positive message"

Why a false positive? Are you one of those people that 'knows' that their code is correct and always blames the tool? What you describe sounds like a real error. Sort of. The error is not triggered by reading the value, it gets triggered when it is used in some conditional context and the undefinedness has an observable impact on the execution of the program.

This is a change from undefined/erroneous behaviour to something else - defined but maybe not what you wanted.

I agree that this can make error analysis more difficult.

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

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.
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.

> They intentionally broke bitwise operators on volatiles because they wanted to be impose their atomic religion everywhere.

Could you elaborate on this?

"broke" is arguably an overstatement. C++20 deprecated some (most?) operations on volatile variables [0] in part because they can misleadingly imply an atomic operation:

> volatile external modifications are only truly meaningful for loads and stores. Other read-modify-write operations imply touching the volatile object more than once per byte because that’s fundamentally how hardware works. Even atomic instructions (remember: volatile isn’t atomic) need to read and write a memory location []. These RMW operations are therefore misleading and should be spelled out as separate read ; modify ; write, or use volatile atomic operations which we discuss below.

This was not received particularly well in the embedded community (e.g., [1]) due to said deprecation affecting compound bitwise operations on volatile variables, which are extremely widely used to interact with hardware registers. This pushback eventually resulted in C++23 un-deprecating compound bitwise operators on volatile variables [2].

[0]: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p11...

[1]: https://www.reddit.com/r/cpp/comments/jswz3z/compound_assign...

[2]: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p23...

Basically on a rare occasion somebody (JF Bastien) was able to land a C++ proposal which warns you if you'd do something stupid, and inevitably C programmers cosplaying as C++ programmers insisted "it's not stupid it's advanced" and so now C++ must not even warn you this is a bad idea.

It still is a bad idea, but being warned would make them feel bad.

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

I have been saying that C++23, or maybe C++26 due to reflection, will eventually be the last standard that actually matters.

For a large number of C++ users, it boils down to what it offers beyond C, but not to the extent WG21 is driving it since C++20.

Also the major surviving three compilers have lost wind on their sails as the corporations sponsoring their development have switched focus to other compiled languages.

Other than the whole security debate, there are no features that would make C++ significantly better for LLVM, GCC, CLR, V8, CUDA,.. improvements.

In fact, some of those projects still require C++17.

If this sounds strange, how many care nowadays about ISO Fortran 2023, or ISO COBOL 2023, despite the amount of software written in them powering many busisesses, or Python libraries even, e.g. SciPy.

Or even with C, almost 20 years later many still reach out to C99, ignoring everything else.

Not to take away from your points; SciPy is now Fortran-free completely[0] (we are also requiring C++17 at most). NumPy never had it. BLAS is all C/Assembly in all optimized vendors. For LAPACK we are working on it [1].

Once there is enough pain, none of the talking points matter for any language. They don't and can't die but linger. I fear that time for C family might come in a decade which would be a shame given how magical Cpp compilers are, all that effort folks pouring in.

[0]: https://github.com/scipy/scipy/issues/18566 [1]: https://github.com/ilayn/semicolon-lapack

Thanks for the update overview, and interestingly you also mention C++17, as the version you currently care about.
Is there any significant performance difference?
Not really, all competitive compilers saturated to the same performance. Often Fortran and "restrict" sprinkled C compile to the same command set.
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 actually replace the loop 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.

Interesting 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, so this is already beyond the standard, I doubt gcc and clang will do the transformation by default.

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.

> 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.

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.
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):

  __BKPT();
  while (1)
    ;
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?

Ask your compiler vendor for a -fallow-infinite-loops
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.

> [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.
Repeal the compiler optimizer dev employment act.
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!

You know what you’re getting, the code is right there!
If it's one of those two it's not fully undefined behavior it's just implementation defined. UB can do anything.
The abrupt shift to LLM slop halfway through is jarring and disgusting to read.
> 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

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.
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.
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

It is certainly not almost always an error.

It is a very normal thing to do when doing embedded programming. It normally means "I do not know how to handle this error. Let the watchdog reset me."

[delayed]
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.
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"
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.

"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.

cant wait for ai to re-write all of the software we wrote in this dogshit programming language