C++26: Trivial infinite loops are no longer undefined behaviour
Thread
Unofficial Hacker News client; not affiliated with Y Combinator.
C++26: Trivial infinite loops are no longer undefined behaviour
Unofficial Hacker News client; not affiliated with Y Combinator.
JoshTriplett · · focus · HN ↗
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.
rcxdude · · focus · HN ↗
ameliaquining · · focus · HN ↗
rcxdude · · focus · HN ↗
JoshTriplett · · focus · HN ↗
pjmlp · · focus · HN ↗
criddell · · focus · HN ↗
Isn't the point that the loop was undefined behavior and so the spinning thread might not actually be spinning to begin with? It could be doing anything and sometimes did stuff like run the next block of code.
If you really want an infinite loop that does nothing (not sure why), you can do that now on any standards conforming compiler with some of the methods Sandor described.
rcxdude · · focus · HN ↗
I'm not too concerned about it being possible to make a loop at all (there's a lot of ways to add a 'side-effect' that will probably result in the same assembly), I'm concerned with a) the strange unwillingness to just define a sensible behaviour in this case, especially when C already has one (and GCC already in practice implements a slightly different but also perfectly reasonable interpretation, both of which work for all the normal ways someone might write such a loop), and b) the huge amount of existing code which uses this construct because for the most part compilers did not actually cause problems with it.
nicebyte · · focus · HN ↗
> 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).
I'm not disagreeing with you, but two things worth considering are 1) you don't always write loops like that _intentionally_; 2) if a bug like that slips into production system, it would be good to make sure it doesn't starve other threads.
rcxdude · · focus · HN ↗
mitxela · · focus · HN ↗
rfgplk · · focus · HN ↗
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.
ameliaquining · · focus · HN ↗
mitxela · · focus · HN ↗
yk · · focus · HN ↗
nicebyte · · focus · HN ↗
rcxdude · · focus · HN ↗
112233 · · focus · HN ↗
ozgrakkurt · · focus · HN ↗
Also performance doesn't matter that much and developer time is more important btw, keep using react.
muvlon · · focus · HN ↗
JoshTriplett · · focus · HN ↗
muvlon · · focus · HN ↗
fc417fc802 · · focus · HN ↗
mitxela · · focus · HN ↗
IcyWindows · · focus · HN ↗
The ISO standard is not the same as a language from a single vendor.
mitxela · · focus · HN ↗
cryptonector · · focus · HN ↗
mitxela · · focus · HN ↗
mschuetz · · focus · HN ↗
cryptonector · · focus · HN ↗
Dylan16807 · · focus · HN ↗
mschuetz · · focus · HN ↗
vrighter · · focus · HN ↗
ibobev · · focus · HN ↗
I think that a compiler option should control this. It can be a nice optimization, but the programmer should be able to opt out.
Xirdus · · focus · HN ↗
WalterBright · · focus · HN ↗
ibobev · · focus · HN ↗
WalterBright · · focus · HN ↗
It's not perfect, but the forest of such switches in C compilers motivated D to not have them.
cryptonector · · focus · HN ↗
WalterBright · · focus · HN ↗
nrr · · focus · HN ↗
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.
saghm · · focus · HN ↗
fc417fc802 · · focus · HN ↗
saghm · · focus · HN ↗
chowells · · focus · HN ↗
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.
saghm · · focus · HN ↗
JoshTriplett · · focus · HN ↗
throwaway786678 · · focus · HN ↗
That UB was added in C++11.
chowells · · focus · HN ↗
TuxSH · · focus · HN ↗
In any case stuff like __asm__ __volatile__("" ::: "memory") prevent such optimizations in the rare case you do need branch-to-self.
classified · · focus · HN ↗
The insidious thing about UB is that it doesn't necessarily 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.
chowells · · focus · HN ↗
cryptonector · · focus · HN ↗
saghm · · focus · HN ↗
marcosdumay · · focus · HN ↗
I think you are misinterpreting that. That phrase unambiguously says the loop is preserved on the final binary.
JoshTriplett · · focus · HN ↗
marcosdumay · · focus · HN ↗
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.
JoshTriplett · · focus · HN ↗
mitxela · · focus · HN ↗
CamperBob2 · · focus · HN ↗
Merging a buggy loop with another loop creates... a buggy loop.
teo_zero · · focus · HN ↗
Now, the compiler could try to understand if the first loop does or doesn't terminate, and apply or not the optimization accordingly, but Turing tought us that is indeed a hard task!
Or it could decide to never apply it, for fear of those rare and usually pathological cases where the first loop doesn't terminate.
Or it could decide to apply it by default and accept that in those cases the program does something different than what the source code says. The latter is better known as UB.
The third option won, and that's why infinite loops are UB in the standard.
mitxela · · focus · HN ↗
The standard example is a linked list instead of an array because the compiler can't prove it never has a cycle.
CamperBob2 · · focus · HN ↗
That is no longer undefined behavior in my book. That is defined behavior that just has an unusually-shitty definition.
It's all moot anyway given other trends in progress, but... UB, bah humbug. Stop trying to fix problems that no one had. This is why people are clamoring to replace C/C++ with Rust and AI and whatever. The language needed to become more understandable and more predictable in everyday use, and instead it got worse.
mitxela · · focus · HN ↗
imtringued · · focus · HN ↗
Why use a for loop with a bound as an example instead of while loops with linked lists? He or she can prompt an LLM for a better example so laziness doesn't count as an excuse.
teo_zero · · focus · HN ↗
I'm the author of the example. I wanted to keep it as simple as possible, and this is the most common form of for loop. I was sure that HN readers would be clever enough to "map" it to whatever they have in their mind that satisfy the undecidability of the condition.
But since you're nitpicking, I haven't specified the types of i and n: i is uint8_t and n is uint32_t. Does it terminate? It depends on the value of n!
imtringued · · focus · HN ↗
> but Turing tought us that is indeed a hard task!
Analyzing whether a bounded loop terminates is impossible, got it.
>Or it could decide to apply it by default and accept that in those cases the program does something different than what the source code says. The latter is better known as UB.
But the reason why it lets the compiler fuse the loops has nothing to do with whether the loop terminates or not. The infinite loop UB is just a way of adding more UB and then invoking non infinite loop optimization.
We don't know if A[i] aliases with the pointer that stores the address of the B array and note I mean B itself not A and B overlapping. It could also alias with the loop bound. So the first loop must run until completion simply because it could accidentally overwrite a pointer or variable that is used in the second loop.
But now that we have infinite loop UB we can ignore all of that and it's not because infinite loops themselves produce optimization potential, it's because more stuff is UB now so the compiler is allowed to break aliasing rules, which is the actual thing that was preventing the optimization. The infinite loop UB is just the permission slip.
teo_zero · · focus · HN ↗
Forget if "i<n" is decidable or not: the sense is that there will always be some loops that the compiler can't determine if it's finite or not.
Forget if A and B can alias or not: the sense is having two independent actions that can be executed in the same loop or in two consecutive loops.
Let's see... what about the following example, that replaces all a's with @ and all e's with & in a zero-terminated string s?
If a compiler is allowed to assume that the first loop terminates, then it may optimize it to: Is this explanation less insane?leni536 · · focus · HN ↗
fc417fc802 · · focus · HN ↗
You: But you only might be stabbed. It isn't required to happen only permitted.
leni536 · · focus · HN ↗
ack_complete · · focus · HN ↗
leni536 · · focus · HN ↗
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).
rcxdude · · focus · HN ↗
usefulcat · · focus · HN ↗
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.
[deleted] · · focus · HN ↗
[deleted]
UncleMeat · · focus · HN ↗
amluto · · focus · HN ↗
I admit I’m unconvinced that this is particularly useful.
(I got many other ideas that did not pass my personal smell test.)
raphlinus · · focus · HN ↗
[1]: <a href="https://youtu.be/g9Rgu6YEuqY?si=_l9JwKhjvIdFEDEX&t=3819" rel="nofollow">https://youtu.be/g9Rgu6YEuqY?si=_l9JwKhjvIdFEDEX&t=3819
amluto · · focus · HN ↗
1. Most implementations do not do this automatic cooperative multitasking trick and most users [0] 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.
Put another way, undefined behavior is a sharp line - if code has UB, it has UB and it if doesn’t, it doesn’t. But code being slow is not a sharp line - something can take 1 ns or 1 ms or 1 second or 1 hour or 1 year or 100 years or 1M years, etc.
A scheduler that fails to schedule a runnable thread in finite time is wrong, but so is a scheduler that fails to schedule it for 100 years or for a week. If it merely takes a minute, then whether it’s right or wrong depends on the situation.
So if you’re talking about schedulers (which that part of talk mostly is), then I don’t think the ability to say “infinite loop without side effects are UB, so my scheduler is correct if I assume that all side-effect-free loops are finite” is actually useful.
There are real world examples. At one point, Go only preempted its cooperative threads at certain points, but this was a problem and newer versions of Go can even preempt tight loops. Python, which threads the worst-of-both-worlds middle ground between asynchronous and cooperative preemption, does not allow an infinite loop (in ordinary Python code) to starve other threads.
[0] Most users of C- or Rust-like languages anyway. Quite a few more managed languages (e.g. Go) are the other way around.
mitxela · · focus · HN ↗
murderfs · · focus · HN ↗
amluto · · focus · HN ↗
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.
cryptonector · · focus · HN ↗
amluto · · focus · HN ↗
Suppose you have some state like this:
And you have: 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: 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:
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:
(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 C and C++, but it would fully solve forward-progress problem :)
[0] This one is no joke. I can come up with examples that would jump to inappropriate addresses using a construct like this if there's a data race -- just replace sum2 with a function pointer.
cryptonector · · focus · HN ↗
This is the only point I don't fully agree with. I'd want to know that this optimization is worthwhile. Is it?
zbentley · · focus · HN ↗
cryptonector · · focus · HN ↗
classified · · focus · HN ↗
I have the suspicion that the members of the C++ standards committee are increasingly not from this planet.