C++26: Trivial infinite loops are no longer undefined behaviour
sandordargo.com
sandordargo.com
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.
It wouldn't work when this kind of loop is generated by macros/templates in some unreachable case left after const folding.
<meta> is the single WORST OFFENDER, where they hardcode std::vector (literally std::vector in the std namespace) std::ranges std::allocator.
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...
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.)
- 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.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 when you mess up. 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 associated 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.
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 access it and returns false. Or maybe it unconditionally fills it, or unconditionally returns false without reading from 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.
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.
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.
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".
They're not, all destructors are explicit. Seems like a skill issue on your end.
Here's a fun one for your amusement:
foo(a, b, c);
The parameters are pass by value. a, b and c are objects that have destructors. Have a look at the code generated for that.It is nice that the compiler does the dirty work for you, but the various paths with exceptions and recovery with invisible code may not be well tested.
You're talking to walter bright, the guy who wrote the digital mars C++ compiler
Okay.
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
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.
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.
Could you elaborate on this?
> 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...
It still is a bad idea, but being warned would make them feel bad.
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
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.
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
Interestingly, posix realtime FIFO scheduling doesn't preempt even on kernel thread based implementations, so one reading of the standard would require yield on this case. But that can actually be potentially catastrophic as FIFO scheduling is expected to be deterministic. But realtime scheduling is already beyond the standard: I doubt gcc and clang will do the transformation by default.
In practice the equivalence is necessary to make some obscure corner of the memory model work and prevent some undesirable optimizations; I expect that in practice the compilers, if they implement this at all, will provide an opt-in flag, but they will optimize as-if the call was there.
Unlike C++, Rust does not manage exceptions at all; in C++, you must consider situations where exceptions arise.
It’s way way more rare in Rust though.
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.
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.
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.
> 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.
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.
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.Also performance doesn't matter that much and developer time is more important btw, keep using react.
The ISO standard is not the same as a language from a single vendor.
I think that a compiler option should control this. It can be a nice optimization, but the programmer should be able to opt out.
It's not perfect, but the forest of such switches in C compilers motivated D to not have them.
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.
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.
That UB was added in C++11.
In any case stuff like __asm__ __volatile__("" ::: "memory") prevent such optimizations in the rare case you do need branch-to-self.
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.
I think you are misinterpreting that. That phrase unambiguously says the loop is preserved on the final binary.
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.
Merging a buggy loop with another loop creates... a buggy loop.
for (i=0;i<n;i++)
A[i]=0;
for (i=0;i<n;i++)
B[i]=0;
It can be conveniently transformed into this: for (i=0;i<n;i++)
A[i]=B[i]=0;
They are exactly equivalent except if the first loop never terminates.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.
The standard example is a linked list instead of an array because the compiler can't prove it never has a cycle.
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.
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.
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!
> 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.
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?
for (char *p=s; *p; p++)
if (*p=='a')
*p='@';
for (char *p=s; *p; p++)
if (*p=='e')
*p='&';
If a compiler is allowed to assume that the first loop terminates, then it may optimize it to: for (char *p=s; *p; p++) {
if (*p=='a')
*p='@';
if (*p=='e')
*p='&';
}
Is this explanation less insane?You: But you only might be stabbed. It isn't required to happen only permitted.
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).
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.
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.)
[1]: https://youtu.be/g9Rgu6YEuqY?si=_l9JwKhjvIdFEDEX&t=3819
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.
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.
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 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.
This is the only point I don't fully agree with. I'd want to know that this optimization is worthwhile. Is it?
I have the suspicion that the members of the C++ standards committee are increasingly not from this planet.
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...
https://www.sandordargo.com/blog/2026/09/16/cpp26-trivial-in...
Edit: sorry, missed the UB bit.
I only use it for error handling and of course it is a bad idea to use this to wait/stall in power sensitive applications, in that case use wake from interrupt.
As an aside, I like to include a software breakpoint in my error handlers. It makes debugging easier without wasting a hardware breakpoint (which are physically limited by the microcontroller):
__BKPT();
while (1)
;> 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.
...Uh, the example shown at the literal top of the blog demonstrates precisely the opposite?
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 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."
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.
Why is that rule needed? I could make my for loop try to solve the halting problem and it'll never finish either, circumventing that rule
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...
Isn't -Wreturn-type enabled by default in both gcc and clang atleast for c++?
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.
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 thisOh yeah and TFA also goes with:
> The funny bit is that C got this right.(...) but C included one more rule: loops whose controlling expression is a constant expression may not be assumed to terminate.
Well, duh! A broken clock is right twice a day it seems
This is literally the opposite behaviour compared to what is written in the source code, even when you "assume the infinite loop terminates".
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.
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.
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".
It’s a controversial trade-off to be sure, but it’s not like there isn’t a sound logic to it.
One can browse other blog entries so it really doesnt matter too much.
brutal. hope major compiler vendors throw in a flag that can bring some sanity to this
In other words, I am mentally well.
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!
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.
So yes, it is good for Rust.
... 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.
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.
Null being an "allowed" value for pointers is the mistake e.g. what became nullptr. "Allowed" because garbage values are garbage.
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.
> 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.
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.
> 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.
There are good uses for infinite loops.
Idiots!
Don’t they really that people write real programs to solve real problems? This isn’t a theoretical academic exercise!
IIUC, my understanding is shallow.
Pointer provenance is just one example, there are others.
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.
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.
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.
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.
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)
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.
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.
I, the programmer, will decide what cycles are wasted or not. That the C++ committee thought they knew better is hubris.
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.
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.
> 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:
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).
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.
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 just X (em dash) but also Y.