Comment by wahern
19 hours 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.
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.
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
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.
2 replies →
> They intentionally broke bitwise operators on volatiles because they wanted to be impose their atomic religion everywhere.
Could you elaborate on this?
"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...
1 reply →
GNU C does the same: memory copies can be optimized into memcpy, various operations can be realized as calls into libgcc, etc.
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.
Those do for the most part correspond to operations which make sense in an embedded context, though.
> 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.
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.
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.
1 reply →
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.
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.
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.)
4 replies →
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.
> 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...
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.
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.
D initializes floating point variables to NaN by default. And chars to 0xFF. Yes it's controversial!
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.
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.
> 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.
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.
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".
> 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
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.
I try to minimize use of destructors for the same reason.
5 replies →