Top
Best
New

Posted by ibobev 5 days ago

C++26: Trivial infinite loops are no longer undefined behaviour(www.sandordargo.com)
173 points | 288 commentspage 3
kmarc 4 days ago|
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.

deepsun 4 days ago||
> 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.

oleganza 4 days ago||
Why is null-terminated C string considered a "billion dollar mistake", but UB isn't?
DonaldPShimoda 4 days ago||
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.

returningfory2 4 days ago||
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

DonaldPShimoda 4 days ago|||
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!
Guvante 4 days ago|||
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.

account42 4 days ago||
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.
cppcppcpp 4 days ago||
VB6 :)
fooker 4 days ago|||
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.

colejohnson66 4 days ago||
Why not just execute what I wrote? Maybe I'm doing some rounding?
fooker 4 days ago|||
Because we want people to write simple idiomatic code that runs fast today and also 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.

layer8 4 days ago|||
Because we want compilers to perform the optimizations, not programmers.
nicoburns 4 days ago|||
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).
ameliaquining 4 days ago||
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
ahy1 4 days ago||
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.

mwkaufma 4 days ago|
The good uses cases for trivial infinite loops is a vanishingly small subset of infinite loops.
ahy1 4 days ago||
It is obviously a subseet. But an important one.
adzm 4 days ago||
as an aside, i've always preferred the zoidberg for (;;) to while(true)
glouwbug 4 days ago|
I think you're thinking of (;,,;)
glum64 4 days ago||
label: goto label;
pianom4n 4 days ago||
The abrupt shift to LLM slop halfway through is jarring and disgusting to read.
BobbyTables2 4 days ago||
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!

Sharlin 4 days ago||
The argument is that an infinite loop without side effects isn't a real program. It's not useful for anything except wasting cycles.
nh2 4 days ago|||
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.

Jaxan 4 days ago||
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?
vlovich123 4 days ago||
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.
rcxdude 4 days ago||
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?).
vlovich123 3 days ago|||
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.
teo_zero 4 days ago|||
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.

rcxdude 3 days ago||
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).
teo_zero 3 days ago||
> 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.

kibwen 4 days ago||||
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
lou1306 4 days ago|||
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.
bryanlarsen 4 days ago|||
They also realized that people choose compilers based on performance benchmarks, and that insane optimizations let them win.
vlovich123 4 days ago||
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.
bryanlarsen 4 days ago||
Probably by working together with Rust. Eliminating undefined behavior from unsafe Rust is a big deal for the Rust community at the moment. And given that most unsafe rust code exists to call into C or C++, concepts like pointer provenance need to be extended. And proper pointer provenance guarantees can both decrease UB and increase optimization potential.

IIUC, my understanding is shallow.

vlovich123 4 days ago|||
That's a niche level thing that helps in some scenarios, and generally not as much for C++ which is much more weakly typed than Rust is. Weak typing + static typing is why safety problems in C++ are going to be really difficult to fix without fundamentally changing the language.
bryanlarsen 4 days ago||
I expect some changes to the language from this direction, some way to attach provenance information or limitations to a pointer. Presumably through a #pragma at first. Strict typing in the C++ sense, not the Rust sense. An annotation like "volatile".

Pointer provenance is just one example, there are others.

nicoburns 4 days ago|||
This particular case is likely an example of that. Rust used to have this problem, but it wasn't ever intended to. So IIRC it got fixed in LLVM for Rust, and this is probably now C++ taking advantage of that.
Panzerschrek 4 days ago|||
> 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.

chrystalkey 3 days ago||
But also very different. If code below this loop executes after elimination and wouldnt have before, that is a very significant change in semantics
bluGill 4 days ago||
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.

dare944 4 days ago|||
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.

bluGill 4 days ago||
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.

echoangle 4 days ago||||
And how would you generate assembly to keep a microcontroller idle then?
bluGill 4 days ago||
You call the CPU halt instruction.
echoangle 4 days ago||
What if my CPU doesn't have that? I don't think Atmel Microcontrollers do for example.
bluGill 4 days ago|||
Better CPU selection. Embedded almost always have power requirements and you need to put your CPU into a low power mode not a loop which is running fast. You can also design your hardware such that you can turn the power off completely in these cases (or perhaps reboot).

Now that I think of it, a different project (I worked just down the aisle, but I wasn't on it) solved a lot customer complaints by turning all the "while(1);" loops into blink an error code - which since it does IO is defined behavior. Which probably is the correct answer to your question - don't just spin doing nothing, spin in such a way that the user has a clue why nothing is working (and in turn you can find out and perhaps fix real world bugs)

dare944 4 days ago||
This is myopic. In many cases it takes time, and sometimes considerable programming effort, to enter and exit low power modes. So you don't do it willy-nilly; you do it when you believe the system has quiesced. That means, on a purely interrupt driven system that is not yet ready to sleep, the code may very well be spinning in an empty infinite loop somewhere.

There's no need to inform the "user" because there's nothing wrong with the system. Its simply waiting until the benefit of sleeping outweighs the cost of getting there.

bluGill 4 days ago||
The context here is an infinite loop with no side effects. You have all the time needed to enter those states.
dare944 4 days ago|||
It may take 100s of instructions to enter a deep sleep state, and 100s to 1000s more to exit it. If you anticipate having to service an event sooner than that, you don't enter sleep at all (especially since there's likely a point at which you're committed to sleep, and have to go all the way down in order to come right back out again). Instead, you hang around twiddling your thumbs until the event comes along.

Now if your processor has a halt/wait-for-interrupt instruction (most do but some don't) you can escape into assembly and use that. But it probably makes little to no difference to energy utilization, and of course its not portable. A nice while (true); would seem obvious, except that the C++ committee insisted that it wasn't.

Just one example of where the committee lost sight of the fact that it was defining an imperative programming language.

echoangle 4 days ago|||
Not necessarily, you could also want to make an infinite loop and wait for an interrupt without eanting to power down.
AMDmi3 4 days ago|||
AVRs have SLEEP instruction.
AnimalMuppet 4 days ago|||
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.
semiinfinitely 4 days ago||
cant wait for ai to re-write all of the software we wrote in this dogshit programming language
account42 4 days ago|
Unfortunate. There isn't ever a good reason to have an infinite loop so concerned compilers could have just diagnosed this as a warning.
echoangle 4 days ago||
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:

pdonis 4 days ago|||
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.
sigbottle 4 days ago|||
__asm__ __volatile("hlt"); when doing quick and hacky debugging could work
gpderetta 4 days ago|||
one could already add loads off a volatile and portably prevent the loop from being optimized. But there was already a lot of existing embedded code that had this sort of loop, (and more will be written as it is an existing idiom) which the committee wanted to un-break.
account42 4 days ago|||
Low level code can and should use assembly to get the precise effect they desire in these cases.
echoangle 4 days ago|||
Why not just allow infinite loops instead of having me write assembly for it though?
account42 4 days ago||
Because a compiler being allowed to assume that a loop always terminates gives it more room to optimize the 99% of loops that aren't supposed to run until the heat death of the universe.
echoangle 4 days ago||
Or you could just detect while loops with constant condition (like the C standard) and not touch any programs that don't exhibit UB while allowing infinite loops for other use cases at zero runtime cost and negligible compile cost.
mdspan 4 days ago||||
That would be pretty cumbersome though. If you're targeting N different architectures, you would have to write N different assembly blocks.
rcxdude 4 days ago|||
I shouldn't need to drop to assembly to get an infinite loop that works!
sumtechguy 4 days ago|||
> 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.

not_the_fda 4 days ago|||
Interrupt driven super loops are very common on bare metal systems.
mdspan 4 days ago|||
Compilers can still diagnose something as a warning even if it's not UB.
tialaramex 3 days ago|||
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.
anticensor 3 days ago||
Oh you mean the -Wall -Werror crowd.
cppcppcpp 4 days ago|||
Can, yes. Must, no.
weinzierl 4 days ago||
For Rust the infinite loop is important enough to have its own keyword.
tialaramex 3 days ago|||
Rust only actually has a single loop, the other loops in Rust are just syntax sugar. Early in compilation the compiler will "de-sugar" a while or for loop into that ordinary infinite loop and so by the time your code is optimised it can't matter how you wrote the loop.

This answers the question Matt Godbolt had which caused him to create what would become Compiler Explorer, is a fancy modern for-each loop able to deliver the same perf as my 1970s loop? In Rust the answer is necessarily "Yes" because by the time the backend sees your program they're the same thing.

The reason Matt wanted to know is that obviously a for-each loop often has better ergonomics, so if they mean the same thing we should prefer our team to write this - but if they're slower that's a tough question, should we trade performance for clarity? The "Yes" answer that Matt found for C++ and which is baked into Rust means you don't need to make that trade decision, write whatever is easier to understand and maintain.

kibwen 4 days ago||||
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).

weinzierl 3 days ago|||
I always thought the lack of a do-while loops in Rust was just a random quirk. Apparently not. Thanks for the insight.
tialaramex 3 days ago|||
Ooh, that's elegant, thanks for sharing
account42 2 days ago|||
A loop without a terminating condition in the loop header is not the same as a loop that doesn't terminate.