4 ms·
The problem is that a lot of the flexibility introduced by UB doesn't serve the developer. Take signed integer overflow, for example. Making it UB might've mad
by crote 5mo ago
The problem is that a lot of the flexibility introduced by UB doesn't serve the developer.
Take signed integer overflow, for example. Making it UB might've made sense in the 1970s when PDP-1 owners would've started a fight over having to do an expensive check on every single addition. But it's 2026 now. Everyone settled on two's complement, and with speculative execution the check is basically free anyways. Leaving it UB serves no practical purpose, other than letting the compiler developer skip having to add a check for obscure weird legacy architectures. Literally all it does is serve as a footgun allowing over-eager optimizations to blow up your program.
Although often a source of bugs, C's low-level memory management is indeed a great source of flexibility with lots of useful applications. It's all the other weird little UB things which are the problem. As the article title already states: writing C means you are constantly making use of UB without even realizing it - and that's a problem.
- saagarjha 5mo agoSigned overflow checks are typically not free unfortunately they have a cost of about 5% or thereabouts
- vlovich123 5mo agoIn hot paths it can be even more. This is why even Rust defines it as wrapping but elides the overflow panic in release builds.
- steveklabnik 5mo agoIt is defined as an error. That error’s default handling is wrapping when debug_assertions is off, and panic when it’s on, but since it’s an incorrect program (though not UB) either behavior is acceptable in any mode.
- jstimpfle 5mo agoIf it is defined as an error, but the compiled build will continue to run with the value wrapped around, I would say that's indistinguishable from UB.
- 12_throw_away 5mo agoNo. An integer getting deterministically set to an unintended value is a bug. A bug is not the same thing as UB. (Even if it were non-deterministic, it would still not be anything like UB.) It's not the same ballpark, not even the same sport.
- jstimpfle 5mo agoWhat if the wrapped index is used to construct an invalid pointer? It might be possible, not sure. What if the integer is used to read the wrong data from disk, or corrupt data on disk by writing to the wrong location?
- 12_throw_away 5mo ago> What if the wrapped index is used to construct an invalid pointer? Constructing an invalid pointer in rust is UB, yes, but integer wraparound is not. > What if the integer is used to read the wrong data to a disk, or corrupt data on disk by writing to the wrong location? Then it is a very bad bug. > What if the program controls a nuclear power plant and the integer causes the control system to fail, causing memory errors due to radiation from the meltdown? Then it is a very very bad bug. > What if the wrapped integer causes the program to output the true name of god, and the programmer, in their last minutes of existence, looks up to see, overhead, without any fuss, the stars going out? Ok, you got me, this one is UB.
- kobebrookskC3 5mo ago> Constructing an invalid pointer in rust is UB no, it is dereferencing, not constructing, an invalid pointer, that is UB. there is even a safe function provided to construct an invalid but non-null pointer: `https://doc.rust-lang.org/stable/std/ptr/fn.dangling.html https://doc.rust-lang.org/stable/std/ptr/fn.dangling.html`
- saagarjha 5mo agoYeah on average. On some paths it's almost free
- ablob 5mo agoIf we're talking two's complement it's not undefined that is right. Having to emit checks though, that is where I beg to differ. A check is only useful if you want to actually change the behavior when it happens, otherwise it is useless. Furthermore, it might be "essentially free" from a branch prediction point, but low and behold caches exist. You would pollute both the instruction cache with those instructions _and_ the branch prediction cache. From this it doesn't follow at all, that there is no cost. In the end small things do add up, and if you're adding many little things "because it doesn't cost much nowadays" you will end up with slow software and not have one specific bottleneck to look at. I do agree that having the option for checked operations is nice (see C#), but I have needed this behavior (branching on overflow) exactly once so far.
- Xirdus 5mo ago> A check is only useful if you want to actually change the behavior when it happens, otherwise it is useless. You almost always want to change the behavior to erroring out on overflow. The few cases where overflow really is intended and fine can be handled by explicit opt-out. And I refuse to buy the argument that "small things add up" in the world where we do string building and parsing every few microseconds. Checked math will have unnoticable impact compared to all the other things we do, in almost every type of program.
- jstimpfle 5mo agoThis string manipulation stuff is very common, and that's why in 2026, an age where science fiction has become a reality, many things are still absurdly slow. Exactly because of such sloppiness, which does accumulate in many cases, and when one least expected it.
- Xirdus 5mo ago100% agreed on the sloppiness. But overflow checking is not sloppiness. It's the opposite of sloppiness. Unchecked math is sloppiness, allowing overflows to happen silently and uncontrollably is sloppiness. It just so happens this kind of sloppiness makes code faster, unlike other kinds of sloppines that make code slower. Not doing necessary safety checks is faster than doing these necessary checks, but it doesn't make these checks any less necessary. Not validating user input also makes code faster, and is also sloppy.
- jstimpfle 5mo agoYou can run your code under ASAN and UBSAN nowadays, it will catch many or most of issues as they happen. But that's completely besides the point. UB on signed overflow, or really most of UB, is not unrelated to C flexibility. It is a detail of the spec related to portability and performance. IIRC it is even required to make such trivial optimizations as turning for (int i = 0; i < n; i++) func(a[i]); into for (Foo *p = a, *last = a + n; p < last; p++) func(p); saving arithmetics and saving a register, on architectures where `int` is smaller than pointers. But there is also options like -fwrapv on GCC for example, allowing you to actually use signed overflow.
- Chinjut 5mo agoHow is undefined behavior necessary for this transformation?
- jstimpfle 5mo agoIIRC computation of the address is done by computing offset from base pointer as a multiplication in (32-bit) int, (like p + (i * sizeof (Foo)). The right term might overflow, but due to signed overflow being UB, the compiler is able to assume that it does not, so the transformation to do the arithmetic entirely in (64-bit) pointer space is valid.
- tyg13 5mo agoExactly. You as the programmer know that the loop counter won't overflow, and in general, essentially nobody would actually write it that way. But if you don't assume it can't happen, the possibility for signed overflow is everywhere in address computations. This is also a major blocker for auto-vectorization. Can't coalesce a load of a[i], a[i+1], a[i+2], a[i+3] into a load of a[i:i+3] if there's a possibility that `i+1`, `i+2` or `i+3` wrapped around (thus causing your "contiguous" load to be non-contiguous). This is a big reason why you shouldn't use `unsigned` for loop counters, especially if they're going to be used as an index into an address calculation.
- Chinjut 5mo ago