4 ms·
> 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 semant
by wahern 13d ago
> 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 13d ago> 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 13d agoIf 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.
- rfgplk 13d agoIt'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 13d ago> 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/p2795r5.html#motivation https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p27...
- fc417fc802 13d agoIn 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 13d agoD initializes floating point variables to NaN by default. And chars to 0xFF. Yes it's controversial!
- dahart 13d agoI’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.
- pbalau 13d agoIs 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 13d agoAn 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 13d agoI try to minimize use of destructors for the same reason.
- andrepd 13d agoDestructors 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 13d agoUnderstanding 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 13d agoI 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 13d ago
- pwdisswordfishq 13d agoGNU C does the same: memory copies can be optimized into memcpy, various operations can be realized as calls into libgcc, etc.
- slaymaker1907 13d agoAnd 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 13d agoThose do for the most part correspond to operations which make sense in an embedded context, though.
- kevin_thibedeau 13d agoThe 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 13d ago> They intentionally broke bitwise operators on volatiles because they wanted to be impose their atomic religion everywhere. Could you elaborate on this?
- aw1621107 13d ago"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/p1152r0.html https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p11... [1]: https://www.reddit.com/r/cpp/comments/jswz3z/compound_assignment_to_volatile_must_be/ https://www.reddit.com/r/cpp/comments/jswz3z/compound_assign... [2]: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p2327r1.pdf https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p23...
- gpderetta 13d agoAs 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 13d ago> 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 13d agoIf 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 13d agoThere 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.