Thinking that you know what `undefined` really is results in undefined behavior.
Thinking that you know what `undefined` really is results in undefined behavior.
This turns out not to be the case.
There actually is text in the current standard as to what acceptable behavior is when encountering undefined behavior.
This was turned into a "note" in later versions of the standard, but initially it was just as much a normative part of the standard as everything else.
The text is still there.
Compiler writers want to exploit UB, hence they say that this text effectively doesn't exist.
https://blog.metaobject.com/2018/07/a-one-word-change-to-c-s...
void f() {
char x[100];
gets(x);
}Most programmers (and pretty much all compiler writers) interpret it as meaning that the compiler can assume it can't happen (ignore the possibility) and translate accordingly.
Some, like mpweiher interpret it as meaning that the compiler must implement a trivial (but somehow unspecified) translation to the underlying hardware and let the behavior be what the hardware provides.
edit: We went through this many many times.
Right now, notionally serious people are making the case that C++ should enshrine the "bag-of-bits" model of pointers which is exactly this sort of stupidity. At first it looks like they want pointers are addresses which is kinda dumb but whatever, and you keep reading and they're like aha, also I should be able to dereference my bag-of-bits and have that work too... Full blown Provenance Via Integers, expect to spend the next forty years trying to specify how any of the optimisations you rely on can survive the resulting tangle, say hi to anybody who wanted Consume ordering and is still waiting.
That belief is endemic because that is the way things actually worked for many years! Compilers used to give us the obvious behavior, because they weren't smart enough to outfox us by doing anything else, because the CPUs we used were not fast enough to let them do the kinds of comprehensive optimization analyses which are now routine.
Each one of these optimizations have broken someone's code.
It is very popular in a small range of very influential companies.
As much as I'm not a fan of the status quo, the argument that changing "Possible" back to "Permissible" isn't particularly solid; the rest of the paragraph makes it clear it's not an exhaustive list of behaviours, merely examples from a "range".
The "range" clearly indicates that you can do something that's semantically in-between the options provided.
What current compilers do is nowhere near that range.
In older compilers, UB behaviour was a lot more predictable because the code transformations done by optimizing compilers were by far not as complex as today.
In the C standard, 'undefined behaviour' is basically a catch-all phrase for a lot of different things: from badly designed stdlib function that miss obvious security checks (like gets()) to the compiler generating non-sensical code.
The latter is what's much more dangerous. It's trivial to just not use the C stdlib functions, but code generation problems caused by UB are much harder to catch.
Compared to such 'obvious' memory corruption problems caused by invalid pointers or missing range checks, obscure code generation issues caused by UB are much harder to identify and the behaviour may also change randomly between compiler versions - this is the actually scary thing about UB, and it's also a relatively "new" thing.
The behavior is undefined.
One could also argue that most UB in C/C++ should actually be IB, but that's a different discussion.
Rust has UB if you don't obey the rules (Safe Rust doesn't because the rules are not your responsibility in Safe Rust, the unsafe superpowers allow you to do things the enforcement can't check but now all rules are your responsibility) but it's not somehow magically "more sane" than C++ it's ultimately lowering to the same LLVM IR that Clang uses. It's tolerable because you're only confronting the risk when you explicitly choose to do so. If you don't need to squeeze that last few drops of performance you can leave it. If you don't have the confidence, if your experts are too busy to review it, if you haven't had a proper night's sleep, you can write safe Rust today.
We live in parallel universes that must have intersected in this thread.
The only barrier is to actually know about and use those tools in the development workflow.
Rust is still superior of course (for memory safety at least), because it actually prevents most of those issues in the first place, but it's not like the rest of the world has been twiddling thumbs in the meantime.
Fuzz testing helps, but it's not commonly used in my experience.
Every UB could verry well be wrapped in error checking and we could have 0 UB out there. We don't do that, but that's because of (our) choice, not some superficial entity that poses machine when running into UB.
Not in C and C++. How would you check that a pointer is safe to dereference for example?
In high level languages it gets a lot more complicated (look up "pointer provenance").
For instance from skimming the list:
- An object is referred to outside of its lifetime (6.2.4).
- The value of a pointer to an object whose lifetime has ended is used (6.2.4).
- An array subscript is out of range, even if an object is apparently accessible with the given subscript (as in the lvalue expression a[1][7] given the declaration int a[4][5]) (6.5.6).
(6.5.7) Additive operators
> If the pointer operand and the result do not point to elements of the same array object or one past the last element of the array object, the behavior is undefined.
Interestingly a[1][5] is also UB, but for the dereferencing a past-the-end pointer, not for the arithmetic.
Also, dereferencing a pointer with the wrong dynamic type.
> all ca 200 UBs in my head
Technically there is no enumerated set of all possible UB. Anything not explicitly defined in the standard is UB.
/extremely-pedantic
It's a bit like that "Migrate off obsolete DB server" card that's been sat in your team's (well most teams) backlog for a couple of years. Everybody agrees it should be done... But like not this sprint.
My assessment is that at their current rate WG21 is adding new UB to the language slightly faster than it's being documented for the appendix, so this won't actually finish.
> The unary * operator denotes indirection. If the operand points to a function, the result is a function designator; if it points to an object, the result is an lvalue designating the object. If the operand has type "pointer to type", the result has type "type". If an invalid value has been assigned to the pointer, the behavior of the unary * operator is undefined.
below in a note (emphasis mine):
> Among the invalid values for dereferencing a pointer by the unary * operator are a null pointer, an address inappropriately aligned for the type of object pointed to, and the address of an object after the end of its lifetime.
Now the note is not normative, but I assume there are normative wording for defining what the "invalid" pointer values are, scattered around in the standard.
C++:
https://eel.is/c++draft/expr.unary.op#1.sentence-4
C++ is very particular in what it means for a pointer to point to an object, and this is also UB there.
Compilers have been known to remove exactly that sort of error checking code, as there was undefined behavior present.
It's a mess.
What I meant is “we (as in) the c/c++ community (or rather the specification committee)” could tomorrow decide that, lets say, all null pointer dereferences should lead to program exit. Existence of UB is a choice.
There are many cases of UB that would be cheap to check, but there are many more that are incredibly expensive to check.