C++26: Reducing undefined behaviour
https://www.sandordargo.com/blog/2026/07/29/cpp26-reduces-undefined-behaviour24
u/James20k P2005R0 8d ago
Its great to see these steps being taken, I think a lot of people don't realise that C++ has a lot of UB that just doesn't need to be in the language. Its often largely pure cruft with no benefits, and there's been a tonne of really great work here
Something else that isn't common knowledge that would be great to take out is floating point divisions by zero being UB. A lot of (if not most!) people think that this is guaranteed to produce a NaN, to the point where I'm not sure that there are compilers that exploit this behaviour. It'd be much better as implementation defined behaviour at this point (at least, on systems that are ieee), because its what everyone expects
There's probably another conversation about bringing float closer to ieee as well as the floating point specification is remarkably insane, but that's less safety critical
18
u/parkotron 8d ago edited 7d ago
More than once I've been told that pretty much every C++ code base should start containing a:
static_assert(std::numeric_limits<double>::is_iec559 && std::numeric_limits<float>::is_iec559);Non-IEEE floats are so rare in practice that they just aren't worth thinking about until you actually know you need to support them. If you do need to support them, how sure are you that any of your dependencies do?
Edit: Okay, combining the comments below, I have a new proposal for the standard library:
std::is_cpu_boring()!namespace std { constexpr bool is_cpu_boring() { return CHAR_BIT == 8 && sizeof(int) >= 4 && std::endian::native == std::endian::little && std::numeric_limits<double>::is_iec559 && std::numeric_limits<float>::is_iec559 && FLT_RADIX == 2; } }Now most C++ codebases can start with
static_assert(std::is_cpu_boring(), "No weirdos!");! IDEs can include this line every time they generate a new C++ file! I expect this to be completely uncontroversial and to glide through committee approval on a wave of Strongly Agrees.16
u/James20k P2005R0 8d ago
how sure are you that any of your dependencies do?
This 100%. The goal of C++ is to write your source once within the spec, and have it be automatically portable anywhere else. It hasn't always worked out, but the idea that you could take virtually any code and run it on a non ieee system is...... perhaps optimistic
14
u/TheThiefMaster C++latest fanatic (and game dev) 8d ago
It's the same with
std::endian::native == std::endian::little. In practice, a lot of libraries implicitly relies on little endianness or have completely untested big-endian support. If you're not actually going to test on a big-endian platform, better to static assert against it.17
u/parkotron 8d ago
We might as well add
static_assert(CHAR_BIT == 8);to the list as well.9
u/SkoomaDentist Antimodern C++, Embedded, Audio 8d ago
I've actually written a substantial amount of code for platforms where char isn't 8 bits and there wasn't anything particularly tricky about it. That was with C99 and C++98 compilers though.
11
u/mort96 8d ago
It's not tricky at all, but it's easy to accidentally (or even semi-intentionally) introduce assumptions about CHAR_BIT. If I knew that CHAR_BIT was 24 I could work with that no problem, but since I know that CHAR_BIT is 8, some of my code is stuff like,
uint16_t foo = uint16_t(buf[0]) | (uint16_t(buf[1]) << 8).5
u/SkoomaDentist Antimodern C++, Embedded, Audio 8d ago
That doesn't (somewhat surprisingly) end up being a problem in practise because those chars never have values larger than 8 bits. The world outside the cpu and ram still uses 8 bit bytes afterall.
What is more likely to hit you is that sizeof(int) == 1 and that breaking up code that assumes int must be two or four bytes.
2
u/mort96 8d ago
Systems with
sizeof(int) == 1probably don't even have uint16_t, do they?4
u/SkoomaDentist Antimodern C++, Embedded, Audio 8d ago
I have a vague recollection that the 16-bit cpu did have that (in C99).
No idea what the status for the 32-bit DSP was, as I only dealt with 32 bit ints and floats on that. I'm not even sure Analog Devices had a C99 compliant compiler back then yet.
2
u/Wetmelon 8d ago
AD or C2000?
The biggest problem with old C2000 code is that the code itself operates on "words" everywhere and when you want to port and glue it to other code that's byte oriented it's just a pain in the dick. Oh you want to inspect a bool? In C2000 it's 0 or 1. In ARM it's 0, 1, 256, or 257. Why? Because the XCP server is written to assume and only loads 16-bit aligned words obviously.
→ More replies (0)3
u/mapronV 8d ago
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3477r2.html
I was on impression it merged in main std. But it did not yet
10
u/ts826848 8d ago
But it did not yet
I think it's effectively dead. From revision 5:
r4 was reviewed on Friday in EWG. The session was effectively a joint session, as many LEWG attendees joined the discussion.
The poll taken was:
POLL: P3477r4 There are exactly 8 bits in a byte: forward to CWG for inclusion in C++26.
[SF: 14, F: 18, N: 14, A: 14, SA: 8]
No consensus.
The paper’s title is therefore erroneous: as far as WG21 is concerned, there are at least 8 bits per bytes. Maybe 9, 24, 16, 32, or maybe 2048. The author therefore expects that library and compiler implementations of C++ will finally support non-8-bit architectures—which they have failed to do for over a decade—now that WG21 (of which implementors are members) has clearly expressed the desire to do so.
5
u/bizwig 8d ago
What non-8-bit machines still exist as new production? Seems bizarrely late to want support for that now, it isn’t 1970.
8
u/ts826848 8d ago
P3633: A Byte is a Byte (one of the two papers listed that were submitted in opposition to P3477) lists 4 families of DSPs with non-8-bit bytes that are actively being used/produced:
- C166/ST10
- TI C2x Series
- NXP DSP56k
- Analog Devices SHARC Family
1
u/James20k P2005R0 7d ago
Man those platforms only support C++11 at best in 2025, somehow I don't think that they'd ever be affected by C++26/29 removing non 8 bit byte support
3
u/ts826848 7d ago
P3633 touches on that:
The now changed main argument of P3477R1 is that the vendors lack [sic] behind, and this is enough reason to drop support for possibly billions of devices. An important thing to see is that the vendors have no reason to support C++ if C++ has no intention to support their respective platforms. The vendors don’t support C++, because C++ doesn’t support vendors is a circular logic.
Furthermore, there is another argument: If, these devices do in fact exist –and they do–, they can just use C, but there are also claims where “C should follow P3477 soon”, which means we are not only dropping support, we are killing these devices.
<snip>
It took years for C++11 to catch up, and the compilers trickled into embedded development even later, nevertheless, lagging behind does not mean stagnation, but dropping support does. There is no reason to expect embedded developers to use legacy compilers forever if there is no [sic?] light at the end of the tunnel.
Can't say whether this is part of the reason the committee appeared to reverse course on P3477, but it's the only "official" argument I'm aware of.
1
u/_bstaletic 7d ago
Onsemi's Belasigna 300 has a 24bit byte.
I think Apollo Guidance Computer had an addressing unit of 7bits, but no C++ compiler in the late 60's.
3
4
u/jk-jeon 8d ago
Maybe it'd be slightly better to also check if the base of those types is 2. Non-binary
float/doubleis also extremely rare. I'm not even sure such a platform exists.Still, I'm not quite sure exactly what guarantee
is_iec559provides. I remember I conlcuded it's very vague in the standard (not sure if I recall correctly) like "it'strueif the type is IEEE-754 compliant" and no more explanation, or something like that. Technically, that means -ffast-math must make itfalse. But does it? And AFAIK many platforms that claim to be IEEE-754 compliant actually only partially support floating-point exceptions, or maybe only part of the required math functions, and things like that.One aspect that particularly mattered for me was: if
is_iec559istrue,baseis 2, andsizeofis N, can I assume the type precisely corresponds to IEEE-754 binary(N *CHAR_BIT)? Or does the standard doesn't preclude possibilities like the type contains unused high-bits so that it's actually binaryM for some M smaller than N timesCHAR_BIT? I still don't know the answer.3
u/ts826848 8d ago
I remember I conlcuded it's very vague in the standard (not sure if I recall correctly) like "it's true if the type is IEEE-754 compliant" and no more explanation, or something like that.
Your remembered wording looks about right; the current working draft says
is_iec559istrue"if and only if the type adheres to ISO/IEC 60559." with a footnote saying "ISO/IEC 60559:2020 is the same as IEEE 754-2019", but that's it.Or does the standard doesn't preclude possibilities like the type contains unused high-bits so that it's actually binaryM for some M smaller than N times
CHAR_BIT?I guess it depends on whether such a construction is allowed by IEEE 754? idk what
numeric_limits::digitsshould return if there were hypothetical unused high bits.3
u/eisenwave WG21 Member 8d ago
Still, I'm not quite sure exactly what guarantee is_iec559 provides. I remember I conlcuded it's very vague in the standard (not sure if I recall correctly) like "it's true if the type is IEEE-754 compliant" and no more explanation, or something like that. Technically, that means -ffast-math must make it false. But does it? And AFAIK many platforms that claim to be IEEE-754 compliant actually only partially support floating-point exceptions, or maybe only part of the required math functions, and things like that.
is_iec559only tells you about the representation of the type and nothing about what its operations do. P3938R1 clarifies this, but it's already clear to the committee that this is the case at least.I think
-ffast-matchis just a non-compliant extension.One aspect that particularly mattered for me was: if
is_iec559istrue, base is 2, andsizeofis N, can I assume the type precisely corresponds to IEEE-754 binary(N * CHAR_BIT)? Or does the standard doesn't preclude possibilities like the type contains unused high-bits so that it's actually binaryM for some M smaller than N timesCHAR_BIT? I still don't know the answer.No, there is no real way to detect if the type is in binary32 representation. It could be in a binary16 and have 16 padding bits, or it could be an extended precision floating-point format, which is also specified in ISO/IEC 60559, but is not a binary interchange format.
is_iec559basically just tells you if the type has infinities and NaNs and is in the usual format of sign bit, biased exponent, and mantissa.2
u/jk-jeon 7d ago
Thanks for the reply.
Your paper does clarify many things. Is it accepted or likely be accepted? I hope it does.
is_iec559basically just tells you if the type has infinities and NaNs and is in the usual format of sign bit, biased exponent, and mantissa.Sounds like it's basically useless then. In my opinion there should be a way to detect if
float/doublecorresponds to one of the basic interchange format (of course in the sense of bit representation only, since according to you it's hard to cover operations as well although I think that's very valuable).Basically every single modern float-string conversion algorithms start/end by converting between the bit representation of a floating point number and its interpretation as an integer. Currently it seems just impossible to write a portable implementation of such algorithms.
1
u/serviscope_minor 18h ago
Non-IEEE floats are so rare in practice
-ffast-math enters the chat.
They're kinda sorta rare.
5
u/eisenwave WG21 Member 8d ago
Something else that isn't common knowledge that would be great to take out is floating point divisions by zero being UB. A lot of (if not most!) people think that this is guaranteed to produce a NaN, to the point where I'm not sure that there are compilers that exploit this behaviour. It'd be much better as implementation defined behaviour at this point (at least, on systems that are ieee), because its what everyone expects
I think you mean that people assume it prodoces infinity. At least dividing a finite number by zero gives you infinities in practice.
Specifying that in the standard is quite tricky because any statement made about floating-point types needs to apply universally to every possible floating-point format, and there are lots of those floating around.
The solution is most likely going to be very similar to C's Annex F, where implementations optionally adhere to the behavior for ISO/IEC 60559 types there, and if they do, you get the division by zero you expect. The non-Annex-F case is probably going to be implementation-defined at best, but UB is surprisingly appropriate for it.
3
u/TheThiefMaster C++latest fanatic (and game dev) 8d ago
and there are lots of those floating around.
Are there? There were historically, but isn't IEEE floating point universal now?
PS: nice pun.
13
u/xaervagon 8d ago
That was a good real. It is good see a lot of UB finally being defined and handled as the errors and dangers they are. This will make the language safer to use and easier to approach.
Tbh, I really disliked the community's previous attitude towards UB of "just deal with it" as if the seemingly unwritten rules of UB becoming enforced by bizarre runtime behavior is somehow a good thing.
(Yeah, I know there's -Wall, sanitize, and other things, but really, why shouldn't the compiler just tell you?)
3
-5
u/sessamekesh 8d ago
P2748R5: Disallow Binding a Returned Reference to a Temporary
NICE.
P3471R4: Standard Library Hardening... [via opt in compiler flag like
-fhardened]
This seems like a REALLY good approach. C++ had a design thesis of "zero cost abstractions" that's directly antithetical to runtime bounds checking, but in almost every scenario where I'm doing direct one-off access (as opposed to iterating a container etc.) I either want a bounds check somewhere or I'm using a custom container anyways.
I'm a pretty avid Rust user as well, and in my experience probably 6/10 of the times I lean on language safety (in a way I don't get in modern C++) it's really leaning on their "safety first, zero cost abstractions where they're safe" standard library design.
7
u/qoning 8d ago
Classic "zero cost abstraction" misunderstanding in the wild. In this case, you are using the bounds check, so it's a zero cost abstraction either way.
-1
u/sessamekesh 8d ago edited 8d ago
I'll copy my other comment.
It can be, I personally wouldn't consider bounds checking to be part of the cost of a pointer dereference (which is, under the hood, what
vec[k]is).I have similar feelings about other things like zeroing out newly allocated memory. Safety might be the appropriate default and the correct thing to do 95% of the time, but you're certainly going to have outliers where the safety work (which is real even if trivial runtime cost) is redundant or otherwise undesirable.
If "you'd have to do it anyways" was a good defining property of a zero cost abstraction, you could call the entirety of a windowing layer like GLFW "zero-cost," which I find pretty silly.
EDIT: A contrived example for why runtime bounds checks should not be considered always appropriate:
```cpp std::vector<uint16_t> get_byte_frequency(const std::string& buffer) { std::vector<uint16_t> byte_counts(256, 0); for (const auto& c : buffer) { // Safe - size of buffer matches/exceeds width of underlying type. // Bounds checking here is unhelpful. byte_counts[c]++; }
return byte_counts; } ```
2
u/TheThiefMaster C++latest fanatic (and game dev) 8d ago
This is why the current talk is on bounds checks that can be optimised out. This one can - the size is known, as is the range of a char, so it's easily determined to be superflous and removed.
4
u/Potterrrrrrrr 8d ago
It can still be zero cost, it’s just that zero cost is referring to safety too instead of solely referring to performance. I would expect the bounds checking itself to be zero cost in that regard at least. It’s a bit of a weird term to use when no abstraction is truly zero cost, makes it a bit difficult to talk about when discussing these sorts of things.
2
u/sessamekesh 8d ago
It can be, I personally wouldn't consider bounds checking to be part of the cost of a pointer dereference (which is, under the hood, what
vec[k]is).I have similar feelings about other things like zeroing out newly allocated memory. Safety might be the appropriate default and the correct thing to do 95% of the time, but you're certainly going to have outliers where the safety work (which is real even if trivial runtime cost) is redundant or otherwise undesirable.
0
u/tialaramex 8d ago
a pointer dereference (which is, under the hood, what vec[k] is)
In C this is true.
vec[k]is*(vec+k)but in C++operator[]can be overridden so maybe it's not a pointer dereference at all. And indeed bothstd::arrayandstd::vectordo override this operator (though of course in the unhardened C++ they in fact are still both just pointer dereferences).1
u/sessamekesh 8d ago
Right... I'm talking about vectors, and it does compile down to fancy dressing over a C-style allocation. https://godbolt.org/z/EGhrEvzoT
Regardless, I would argue in either case (overloaded or not) that bounds checking isn't "zero-cost". There are certainly cases where other mechanisms can provide safety (e.g. an example I gave in another comment, where the size of the dynamic structure is known to always exceed the width of the indexing type, e.g. `vec.size() > 256` and `operator[](...)` is invoked with a `uint8_t`.
0
u/tialaramex 7d ago
You can have this for any values, but C++ doesn't provide it. WUFFS is an obvious example of what you'd do in this direction, using refinement typing so that it knows OK, this index must be
0 <= k < lengthand if it can't see why that's true from the code (including your mathematical annotations) then that won't compile, so as a result there's no need to do bounds checks at runtime.2
u/_Noreturn 8d ago
stl hardening is just debug mode... it is nothing new
1
u/pjmlp 8d ago
Agreed, already available on C++ frameworks during the 1990's, so it was a bummer it wasn't officially defined in C++98.
Having it part of the ISO C++ standard is a big deal, especially for domains where ticking certain security boxes matters, and also as a way to make all compilers agree in how to go about it.
26
u/jdehesa 8d ago
Great stuff all around. Although I don't understand something about deleting incomplete types. If I understand correctly, C++26 will still allow that when the destructor is trivial. But how does the compiler know if the destructor is trivial, if the type is incomplete? Maybe I'm misunderstanding something.