[HN Gopher] C++26: Trivial infinite loops are no longer undefine...
___________________________________________________________________
C++26: Trivial infinite loops are no longer undefined behaviour
Author : ibobev
Score : 161 points
Date : 2026-09-17 20:52 UTC (1 days ago)
HTML web link (www.sandordargo.com)
TEXT w3m dump (www.sandordargo.com)
| MiroslavPokorny wrote:
| Breadcrumbs for "blog", "year", "month" etc are broken and give
| 404s :(
|
| One can browse other blog entries so it really doesnt matter too
| much.
| Aurornis wrote:
| 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
| saghm wrote:
| That's kind of what you get with UB; the compiler doesn't need
| to do what you expect.
| mathisfun123 wrote:
| > 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...
| kouosi wrote:
| > 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++?
| mathisfun123 wrote:
| probably - i guess then i meant -Werror=return-type
| drdexebtjl wrote:
| C on the other hand puts an implicit `return 0` at the end,
| but only on the main function for some reason. Very weird.
| mpyne wrote:
| C++ also special-cases the `main` function. Probably
| because `main` is the interface to the OS so it gets
| special language treatment.
| mitxela wrote:
| In C++ you're not allowed to call main yourself, so that
| the compiler can call the global constructors at the
| start of main.
| pjmlp wrote:
| Which also exist as C extensions, in some compilers.
| mitxela wrote:
| In C. In C++ it's an error to not return, as it should be.
| mathisfun123 wrote:
| .... The blog post literally demonstrates that it is not.
| Feel free to repro on your own machine.
| mitxela wrote:
| 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.
| BobbyTables2 wrote:
| 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!
| bryanlarsen wrote:
| They also realized that people choose compilers based on
| performance benchmarks, and that insane optimizations let them
| win.
| vlovich123 wrote:
| 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 wrote:
| 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.
| nicoburns wrote:
| 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.
| vlovich123 wrote:
| 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 wrote:
| 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.
| Sharlin wrote:
| The argument is that an infinite loop _without side effects_
| isn 't a real program. It's not useful for anything except
| wasting cycles.
| kibwen wrote:
| 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
| nh2 wrote:
| 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 wrote:
| 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 wrote:
| 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 wrote:
| 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?).
| teo_zero wrote:
| 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 wrote:
| 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).
| lou1306 wrote:
| 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.
| bluGill wrote:
| 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.
| echoangle wrote:
| And how would you generate assembly to keep a microcontroller
| idle then?
| bluGill wrote:
| You call the CPU halt instruction.
| echoangle wrote:
| What if my CPU doesn't have that? I don't think Atmel
| Microcontrollers do for example.
| bluGill wrote:
| 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 wrote:
| 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 wrote:
| The context here is an infinite loop with no side
| effects. You have all the time needed to enter those
| states.
| echoangle wrote:
| Not necessarily, you could also want to make an infinite
| loop and wait for an interrupt without eanting to power
| down.
| dare944 wrote:
| 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.
| AMDmi3 wrote:
| AVRs have SLEEP instruction.
| dare944 wrote:
| 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 wrote:
| 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.
| AnimalMuppet wrote:
| 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.
| Panzerschrek wrote:
| > 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.
| adzm wrote:
| i have never before thought that a function could 'fall through'
| to another function. why does this behavior even exist?
| kzrdude wrote:
| 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.
| Someone wrote:
| 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?
| muvlon wrote:
| 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.
| gizmo686 wrote:
| 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.
| raverbashing wrote:
| 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
|
| Oh yeah and TFA also goes with:
|
| > The funny bit is that C got this right.(...) but C included
| one more rule: loops whose controlling expression is a
| constant expression may not be assumed to terminate.
|
| Well, duh! A broken clock is right twice a day it seems
| rcxdude wrote:
| With UB the compiler has no particular requirement to emit
| the 'ret'. (or, in the example, anything at all for the
| function)
| echoangle wrote:
| 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?
| rcxdude wrote:
| If it's declared as static, maybe (well, usually, in my
| experience. You'll also usually get an unused warning).
| Otherwise the compiler can't assume some other compilation
| unit won't want it. Linkers can perform a garbage collection
| pass but they don't often do it by default and they often
| need finer grained information from the compiler (see the gcc
| arguments --ffunction-sections and -Wl,--gc-sections)
| lou1306 wrote:
| I can understand adding the 'unreachable' function to the
| object file, I can even understand plugging it into the
| final executable, what I (and most other people) object to
| is making it the de-facto entry point.
|
| This is literally the opposite behaviour compared to what
| is written in the source code, even when you "assume the
| infinite loop terminates".
| echoangle wrote:
| That's the problem with UB, once you hit it (or even have
| it in your code), you can't really trust anything about
| the execution anymore. That the function is called isn't
| something the compiler does on purpose, it's just that
| the main function is compiled empty due to the UB and the
| function directly behind it is executed because the CPU
| just keeps looking for the next instruction.
| rcxdude wrote:
| Yeah, that's what UB does. You get to see the arbitrary
| behaviour of the underlying machine with whatever the
| compiler produces.
| rcxdude wrote:
| 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.
| echoangle wrote:
| > 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.
| rcxdude wrote:
| they have instructions for implementing them, but the
| important point here is that functions are still only
| defined by instructions that are executing between a call
| and ret instruction (or their equivalent more spelled-out
| equivalent operations), and not only can these not match up
| with what the compiler considers a function (for useful
| reasons like tail-calls as well as not-useful reasons like
| compiler bugs and UB), it might not be statically obvious
| exactly what instructions these are. So the CPU in practice
| has only a rough guess of where the function boundaries are
| (it might use these guesses for things like branch
| prediction, but they don't define the visible execution of
| the code beyond the nuts and bolts of what those
| instructions actually do).
| apple1417 wrote:
| 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".
| rcxdude wrote:
| 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).
| adzm wrote:
| as an aside, i've always preferred the zoidberg for (;;) to
| while(true)
| glouwbug wrote:
| I think you're thinking of (;,,;)
| oleganza wrote:
| Why is null-terminated C string considered a "billion dollar
| mistake", but UB isn't?
| Guvante wrote:
| 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 wrote:
| 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 wrote:
| VB6 :)
| DonaldPShimoda wrote:
| 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 wrote:
| 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 wrote:
| 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!
| nicoburns wrote:
| 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 wrote:
| 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
| fooker wrote:
| 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 wrote:
| Why not just execute what I wrote? Maybe I'm doing some
| rounding?
| layer8 wrote:
| Because we want compilers to perform the optimizations, not
| programmers.
| fooker wrote:
| 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.
| account42 wrote:
| 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 wrote:
| 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:
| account42 wrote:
| Low level code can and should use assembly to get the precise
| effect they desire in these cases.
| mdspan wrote:
| That would be pretty cumbersome though. If you're targeting
| N different architectures, you would have to write N
| different assembly blocks.
| rcxdude wrote:
| I shouldn't need to drop to assembly to get an infinite
| loop that works!
| echoangle wrote:
| Why not just allow infinite loops instead of having me
| write assembly for it though?
| account42 wrote:
| 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 wrote:
| 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.
| pdonis wrote:
| 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 wrote:
| __asm__ __volatile("hlt"); when doing quick and hacky
| debugging could work
| gpderetta wrote:
| 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.
| weinzierl wrote:
| For Rust the infinite loop is important enough to have its own
| keyword.
| kibwen wrote:
| 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).
| sumtechguy wrote:
| > 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 wrote:
| Interrupt driven super loops are very common on bare metal
| systems.
| mdspan wrote:
| Compilers can still diagnose something as a warning even if
| it's not UB.
| cppcppcpp wrote:
| Can, yes. Must, no.
| aabolfazl wrote:
| Sometimes while (true){} doesn't mean anything clever. It just
| means the system is broken stay here.
| JoshTriplett wrote:
| > 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.
| rcxdude wrote:
| Yeah, this is almost the worst way they could choose to 'fix'
| the problem.
| ameliaquining wrote:
| I'm curious, what exactly do you imagine going wrong here?
| rcxdude wrote:
| The biggest headache will probably be it getting emitted in
| inappropriate contexts: where there is no actual means to
| sched_yield for whatever reason (bare metal, kernel,
| whatever). The second is just that the behaviour of the
| infinite loop changes: suddenly you're getting a bunch of
| extra system calls from your spinning thread instead of just
| a high CPU usage, which could disguise the issue or perhaps
| cause problems for other parts of the system. I don't see a
| good reason for the transformation: pretty much any time you
| are writing a bare infinite loop like this you don't want
| anything else to happen (it's also silly that it only happens
| with a particular spelling of an infinite loop, keeping the
| others still undefined).
| JoshTriplett wrote:
| "Emitted in inappropriate contexts" is very much one of the
| shapes I would expect unpleasant surprises to take, yeah.
| If you're writing code in C, you often need a lot of
| control over exactly what's happening. You might, for
| instance, be writing a .so for use with LD_PRELOAD, where
| it's important that you know everything being called so you
| can't accidentally recurse. You might be writing code for a
| sandbox, where you have an allowlist of permitted syscalls.
| pjmlp wrote:
| Exact control is only available in Assembly, minus
| unavoidable hardware flaws, everything else even a minor
| compiler update might change the outcome of the code.
| criddell wrote:
| > suddenly you're getting a bunch of extra system calls
| from your spinning thread instead of just a high CPU usage
|
| 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 wrote:
| It being undefined behaviour before doesn't make all
| possible definitions of that behaviour equally
| reasonable. The strangest thing to me is that I don't
| know who this behaviour definition _is for_. Infinite
| loops like this are a pattern that 's almost entirely
| mutually exclusive with situations where a scheduler is
| relevant.
|
| 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 wrote:
| arguably there are already several places in the language
| where things like this can happen. for example,
| initializing a static function variable has certain thread
| safety guarantees (two threads entering the function won't
| step on each other), and while it's nice to not worry about
| it, this can certainly be a problem if you're trying to
| stay close to the metal and not pull in any dependencies.
|
| > 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 wrote:
| This is true. Static initialization will often generate
| calls to lock functions and that can be a faff to deal
| with. But I don't see what the point of the sched_yield()
| is. Using it at all is already a code smell and calling
| it repeatedly in a tight loop is the kind of thing kernel
| developers were trying to beat out of application
| developers decades ago because it just isn't really very
| helpful (and often actively harmful) with any but the
| dumbest of schedulers. It's certainly not very useful for
| avoiding thread starvation.
| mitxela wrote:
| Impl specific. If you're building bare metal, pass
| -ffreestanding so GCC knows it's not allowed to call OS
| functions.
| rfgplk wrote:
| 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.
| ameliaquining wrote:
| Freestanding environments specifically don't have to do
| this, they get a carve-out. (I dunno if this applies
| elsewhere though.)
| mitxela wrote:
| Sounds like a compiler bug that a standard feature
| implementable in freestanding doesn't work in freestanding.
| yk wrote:
| 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.
| nicebyte wrote:
| under what conditions would the infinite loop be scheduled
| over something else after it has run out of its time slice?
| rcxdude wrote:
| 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.
| 112233 wrote:
| I would expect quite a number of embedded use cases to
| suddenly break. Yay, free CVEs!
| ozgrakkurt wrote:
| 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.
| muvlon wrote:
| 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".
| JoshTriplett wrote:
| 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?
| muvlon wrote:
| 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).
| fc417fc802 wrote:
| 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).
| mitxela wrote:
| 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.
| IcyWindows wrote:
| 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.
| mitxela wrote:
| IIRC a lot of it was benchmark gaming between GCC and
| LLVM.
| cryptonector wrote:
| 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.
| mitxela wrote:
| 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.
| mschuetz wrote:
| I don't see the issue. Just let wrong code do wrong things
| But let it do the expected wrong thing, rather than
| changing the code to something unexpected.
| cryptonector wrote:
| "No." <-- the C++ committee.
| vrighter wrote:
| performance doesn't matter.... so datacenters would be
| equally happy running software that runs half as fast but
| uses 10% more power?
| ibobev wrote:
| > 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.
| Xirdus wrote:
| The programmer can opt out by terminating the loop.
| WalterBright wrote:
| Every flag that changes the semantics of the code bifurcates
| the language into two languages.
| ibobev wrote:
| 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.
| WalterBright wrote:
| 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.
| cryptonector wrote:
| 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.
| WalterBright wrote:
| Are C/C++ chars signed or unsigned? What a mess!
| saghm wrote:
| 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
| fc417fc802 wrote:
| Yes, it was a horrible situation that has been replaced by an
| only slightly less horrible situation.
| saghm wrote:
| I don't totally agree with this. To me, UB is an order of
| magnitude worse than pretty much anything else, so this is
| more than "slightly less" horrible. I don't necessarily
| disagree that this is still horrible, but I also don't
| write an C++, so I'm mostly just commenting as an outside
| observer.
| chowells wrote:
| Not only that, the code was _wrong_. The specification is
| quite clear that correct programs don 't cause UB to be
| executed at run time. If your wrong code now produces wrong
| results, that's because it's wrong. That your compiler
| allowed you to get away with it for decades is a compiler
| bug, not a feature.
|
| Do I fully believe all of the above? Not exactly. But
| compiler authors do. Does it make a really good argument to
| never use C or C++? Yes. If only we had 50 years of
| optimization work in any language with better semantics.
| saghm wrote:
| 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.
| JoshTriplett wrote:
| The mistake was declaring infinite loops to be UB in the
| first place.
| throwaway786678 wrote:
| _> That your compiler allowed you to get away with it for
| decades is a compiler bug_
|
| That UB was added in C++11.
| cryptonector wrote:
| Yes, but now it has to. I guess that's better? than UB,
| maybe.
| marcosdumay wrote:
| 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.
| JoshTriplett wrote:
| 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.
| marcosdumay wrote:
| 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.
| JoshTriplett wrote:
| I was utterly unconvinced that the original infinite-loop
| UB gave the compiler any important performance
| optimization, and I'm unconvinced that _this_ is
| providing useful value to compensate for its surprise. If
| I wanted a yield in my infinite loop, I 'd add one.
| mitxela wrote:
| The original UB was to allow the compiler to merge two
| loops without proving termination.
| CamperBob2 wrote:
| What difference does it make? If the loop doesn't
| terminate, it doesn't terminate, which is almost always a
| bug, except when it's not. If it _does_ terminate, then
| great, it terminates.
|
| Merging a buggy loop with another loop creates... a buggy
| loop.
| teo_zero wrote:
| Take this example: 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.
| leni536 wrote:
| 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.
| fc417fc802 wrote:
| Me: I don't expect to be stabbed.
|
| You: But you only _might_ be stabbed. It isn 't
| _required_ to happen only _permitted_.
| ack_complete wrote:
| 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.
| rcxdude wrote:
| 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.
| usefulcat wrote:
| 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.
| UncleMeat wrote:
| 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).
| amluto wrote:
| 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.)
| raphlinus wrote:
| 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
| mitxela wrote:
| I thought it was generally so the compiler can merge two
| computation loops without proving if one of them runs forever
| .
| murderfs wrote:
| Correct. See N1528: "Why undefined behavior for infinite
| loops?" https://www.open-
| std.org/jtc1/sc22/wg14/www/docs/n1528.htm
| ameliaquining wrote:
| The article, most unfortunately, doesn't explain why anyone would
| want infinite loops to be UB in the first place. I found this
| explanation: https://www.open-
| std.org/jtc1/sc22/wg14/www/docs/n1528.htm
| omoikane wrote:
| The article mentions it's a halt-on-error pattern:
|
| https://www.sandordargo.com/blog/2026/09/16/cpp26-trivial-in...
|
| Edit: sorry, missed the UB bit.
| rcxdude wrote:
| That's more why you would want them to be defined in the
| first place.
| Lvl999Noob wrote:
| That says why they _don 't_ want it to be UB. The question, I
| believe, was why they want statically-known-infinite non-
| trivial loops to continue being UB.
| pseudohadamard wrote:
| One minor nit, it's not halt-on-error, it's restart-on-error
| since the watchdog then restarts the system. The technical
| term for this is rejuvenation and it's standard practice in
| SCADA and similar to deal with this-shouldn't-happen error
| conditions.
| layer8 wrote:
| See https://news.ycombinator.com/item?id=49760653.
| omoikane wrote:
| > 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...
| nullc wrote:
| Nice underhanded coding technique / bugdoor material once the
| "infinite loops are not UB" notion is widespread enough. :P
| wahern wrote:
| > 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.
| IsTom wrote:
| > 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.
| rcxdude wrote:
| If it's truly unreachable then it's not likely to be a
| problem. If it is reachable and it's emerging from some
| macros and templates then I would be more inclined want a
| warning for it.
| IsTom wrote:
| Yeah, but then you need compiler to somehow know if it's
| truly unreachable to know when to emit the warning and when
| to not do that.
| krupan wrote:
| If it's a warning and not an error then emit it whether
| it's unreachable or not
| rfgplk wrote:
| 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.
| aw1621107 wrote:
| > 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...
| fc417fc802 wrote:
| 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.
| WalterBright wrote:
| D initializes floating point variables to NaN by default.
| And chars to 0xFF. Yes it's controversial!
| dahart wrote:
| 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.
| mitxela wrote:
| 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.
| dahart wrote:
| 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.)
| ack_complete wrote:
| 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.
| mitxela wrote:
| Uninitialized variables were already UB to read, because
| some architectures have trap representations, even for
| integers. Every register on Itanium has one.
| cryptonector wrote:
| That's not reason enough to have the compiler initialize.
| zrm wrote:
| 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 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.
| nullc wrote:
| 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.
| pbalau wrote:
| 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".
| andrepd wrote:
| An empty loop, under some non-obvious conditions, on some
| compiler flags but not others, silently transforms into a
| system call. In a systems programming language.
| WalterBright wrote:
| I try to minimize use of destructors for the same reason.
| andrepd wrote:
| Destructors run predictably at list, and are pervasive
| everywhere. You know that when you exit a scope, be that
| a function or whatever it may, the destructors of
| variables in that scope are called. That is clear and
| consistent. The transformation mentioned above is not.
| WalterBright wrote:
| Understanding the code requires understanding the
| destructors of the objects you're using. Since they are
| invisibly inserted, they are a source difficulty in
| entirely understanding the code.
| rcxdude wrote:
| I do wonder if any of the language servers that insert
| implied type annotations would ever also show things like
| destructor calls in a similar manner. It seems like it
| would be quite useful.
| otabdeveloper4 wrote:
| > Since they are invisibly inserted
|
| They're not, all destructors are explicit. Seems like a
| skill issue on your end.
| WalterBright wrote:
| Since AFAIK I'm still the only person to write a correct
| C++ (C++98) compiler from preprocessor to object file, I
| know all about destructors.
|
| 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.
| aw1621107 wrote:
| > 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
| pwdisswordfishq wrote:
| GNU C does the same: memory copies can be optimized into
| memcpy, various operations can be realized as calls into
| libgcc, etc.
| slaymaker1907 wrote:
| 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.
| rcxdude wrote:
| Those do for the most part correspond to operations which
| make sense in an embedded context, though.
| kevin_thibedeau wrote:
| 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.
| Sniffnoy wrote:
| > They intentionally broke bitwise operators on volatiles
| because they wanted to be impose their atomic religion
| everywhere.
|
| Could you elaborate on this?
| aw1621107 wrote:
| "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...
| tialaramex wrote:
| 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.
| 112233 wrote:
| 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
| pjmlp wrote:
| 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.
| ilayn wrote:
| 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
| pjmlp wrote:
| Thanks for the update overview, and interestingly you
| also mention C++17, as the version you currently care
| about.
| gpderetta wrote:
| As the only observable behaviour of this_thread::yield is
| forward progress, because of the as-if rule, the compiler
| doesn't actually need to replace the loop, when running on a
| runtime that guarantees preemption. That's the case when
| std::threads are backed by kernel threads. On a M:N
| implementation, then yes, a yield would need to be added, but
| that would be desirable.
|
| Interestingly, posix realtime FIFO scheduling doesn't preempt
| even on kernel thread based implementations, so one reading of
| the standard would require yield on this case. But that can
| actually be potentially catastrophic as FIFO scheduling is
| expected to be deterministic. But realtime scheduling is
| already beyond the standard: I doubt gcc and clang will do the
| transformation by default.
|
| In practice the equivalence is necessary to make some obscure
| corner of the memory model work and prevent some undesirable
| optimizations; I expect that in practice the compilers, if they
| implement this at all, will provide an opt-in flag, but they
| will optimize as-if the call was there.
| add2 wrote:
| > 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.
| steveklabnik wrote:
| 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.
| cryptonector wrote:
| 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.
| peterus wrote:
| There are valid use cases for the infinite while(1) loop in
| microcontroller programming (contrary to popular belief it
| seems). Autogenerated HAL code for the stm32 uses it for error
| handlers, and they support C++ so I am surprised this was UB.
|
| I only use it for error handling and of course it is a bad idea
| to use this to wait/stall in power sensitive applications, in
| that case use wake from interrupt.
|
| As an aside, I like to include a software breakpoint in my error
| handlers. It makes debugging easier without wasting a hardware
| breakpoint (which are physically limited by the microcontroller):
| __BKPT(); while (1) ;
| rfgplk wrote:
| 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)
| usefulcat wrote:
| 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.
| aw1621107 wrote:
| > 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?
| mitxela wrote:
| Ask your compiler vendor for a -fallow-infinite-loops
| shmerl wrote:
| Why does the loop mean halt in that embedded case example?
| rcxdude wrote:
| 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.
| ErikCorry wrote:
| > [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.
| rcxdude wrote:
| 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.
| glum64 wrote:
| label: goto label;
| z3ratul163071 wrote:
| c++ reaching new lows
| cppcppcpp wrote:
| This is good for Rust!
| layer8 wrote:
| This actually fixes a case that became a problem for Rust
| implementations: https://github.com/rust-
| lang/rust/issues/28728
|
| So yes, it is good for Rust.
| kibwen wrote:
| The Rust case was fixed back in 2021 via changes to LLVM,
| it didn't require waiting for the C++ standards body.
| bitbasher wrote:
| 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!
| cppcppcpp wrote:
| You know what you're getting, the code is right there!
| mwkaufma wrote:
| If it's one of those two it's not fully undefined behavior it's
| just implementation defined. UB can do anything.
| pianom4n wrote:
| The abrupt shift to LLM slop halfway through is jarring and
| disgusting to read.
| 0x69420 wrote:
| > 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
| Panzerschrek wrote:
| 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.
| rcxdude wrote:
| 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.
| deschutes wrote:
| 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
| kazinator wrote:
| I'd much rather have the compiler diagnose an infinite loop than
| silently pretend that it's not reachable, or that it can be
| rewritten to a yield.
|
| In other words, I am mentally well.
| WalterBright wrote:
| 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.
| paparulo329 wrote:
| The mere concept of undefined behavior is hilarious to me. "Oh
| this part? No we can't and won't even try figuring out what doing
| that does, this page intentionally left blank; yes we are a very
| serious whole ass standards body thanks for asking"
| layer8 wrote:
| 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.
| Dwedit wrote:
| "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.
| semiinfinitely wrote:
| cant wait for ai to re-write all of the software we wrote in this
| dogshit programming language
| ratelimitsteve wrote:
| as the kind of person who has been reading the jargon file for
| fun since the 90s, I thought I had at least a passing familiarity
| with a lot of hackish slang from the old days. today i learned
| about nasal demons as a phrase for undefined behavior. i supposed
| there's still fossils in the dirt
| ahy1 wrote:
| 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 wrote:
| The good uses cases for _trivial_ infinite loops is a
| vanishingly small subset of infinite loops.
| ahy1 wrote:
| It is obviously a subseet. But an important one.
| Aardwolf wrote:
| > the implementation may assume any thread will eventually do one
| of the following: terminate, call a library I/O function, access
| a volatile glvalue, or perform a synchronization or atomic
| operation
|
| Why is that rule needed? I could make my for loop try to solve
| the halting problem and it'll never finish either, circumventing
| that rule
| layer8 wrote:
| See https://news.ycombinator.com/item?id=49760653.
| mitxela wrote:
| So the compiler can merge two computation loops without proving
| termination.
| cryptonector wrote:
| Compute the Ackermann function, write the result, terminate..
| when the sun goes red giant and swallows the earth.
| deepsun wrote:
| > 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.
| kmarc wrote:
| 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.
| Surac wrote:
| For me everything more than c with classes is too much cognitive
| load. Templates my by ok for implementing Generics but most of
| the other changes this comitee has produces are complete waste of
| brain i think
___________________________________________________________________
(page generated 2026-09-19 10:01 UTC)