PS: some pragmatic cleanup of UB and IB in the C standard would be welcome though.
PS: some pragmatic cleanup of UB and IB in the C standard would be welcome though.
UB is a feature, not a bug. So you don't really "clean" it up. Cleaning up of UB means a lot of overhead for programs.
for (i = 0; i < *len; i++)
array[i] = 0;
without having to check on each iteration whether len secretly points inside the array and got modified by the loop. It would be better if this were not a heuristic and there were clear compile-time information saying for sure that this optimization is safe.In case of similar but where it’s float array[N] etc then compiler can assume that they do not alias.
One of the reasons why Fortran was faster for decades was the default aliasing rules, restrict finally got us out of that trap. If we'd always assume everything alias plenty of code would become way slower.
Also C has well defined ways to access memory via different types. Make an union type and access via that. It makes it explicit.
Since then they specified that unions are the way to go.
It's quite common in existing real-world C code to rely on the ability to fill in a variable via a pointer of one type and then access it via a pointer of another type, and you expect the language to properly return the data you wrote to that pointer. The C committee determined that, just about always, that happens when one of the pointers is a char pointer (e.g., you call read() or write() on a struct), and they said that char pointers are allowed to alias by default but others can't. That's their guess, which is mostly accurate. There are much nicer answers here if you're not obligated to be compatible with existing C code - but in that case, especially today, I'd go with one of several other languages that can accurately express intention at the source code level instead of making backwards-incompatible changes to C. :)
Some examples:
1. When C came out, you couldn't assume a two's complement machine. So some architectures would yield different results on signed over/underflow than others, and some architectures might trap on overflow. Hence, signed overflow is undefined in C.
2. Did you know the Lisp Machine had a C compiler? It did. Because pointers on the Lisp machine were tagged and bounds-checked to ensure memory safety, this compiler implemented C pointers with fat pointers, containing a reference to an object and an offset within that object. Most machines you are familiar with allow indirecting through arbitrary integers; the Lisp Machine is an example of one that does not, trapping on invalid pointer accesses. Having C and C programs available in both environments mean that indirecting through a pointer that does not point to or inside some pre-existing static or dynamically allocated object is undefined.
3. Operating systems with virtual memory can arrange things such that indirecting through 0 (NULL) will raise a segmentation violation in user code, guaranteeing a pointer value that is always invalid -- but inside the kernel you may want to access memory location 0. So accessing null pointers is undefined.
By leaving these undefined, you are not committing implementors to adapt the abstract machine to the underlying hardware in cases where hardware can widely differ, allowing for simple implementations that yield fast code on all architectures they compile for yet still allowing for a useful subset of C code to be 100% portable.
Or just does what C does, make it UB and the programmers will be aware of implementation of overflows and won't suffer because of added checks. They will do the checks themselves if they want and handle their numbers with care
Imo the answer always boils down to performance. Java has no UB yet it works on billion devices (citation needed). It is doable, just costly.
MIPS compilers for C emit "addu" instructions for signed integer additions rather than using "add", to avoid overflow traps.
Using unsigned arithmetic for signed is not just so that C programs relying on the behavior do not blow up. When overflow behavior is reversible wraparound, the compiler more freedoms in rearranging a calculation. Suppose the programmer has written some a + b + c, and ensured that there is no overflow (when it's added left to right, as the abstract semantics says). If the addition mechanism has reversible overflow, the compiler can rearrange it to do the additions in any order. The result will be the same, correct one that (a + b) + c would produce.
But the idea that two's complement itself "wraps around" is mostly a myth. Two's complement has a fixed range of representation, and a result of adding two elements from that representation can be out of that range. In that case, the truncated value whose bit pattern corresponds to the unsigned arithmetic is not the correct result.
Even if such exotic hardware is still around, the only thing that would change is that C compilers supporting this hardware couldn't call themselves "ISO Compliant" anymore, the users of those compilers wouldn't lose anything.
You'd hate a compiler where this has silly overhead:
for(int i=0; i < n; i++) arr[i];
That's because 64-bit addressing modes don't overflow the same way as signed 32-bit ints. But signed overflow being UB gives compilers license to ignore the difference and generate code that makes sense.The real problem is that C is too bare-bones and lax to be able to contain or avoid UB in day to day programs. For the aforementioned overflow gotcha, there's no way to enforce use of size_t for indexing. There's no `checked_mul(a,b)` to easily avoid signed UB. You have to make the check yourself, and if you're not careful, that check will be UB too. malloc() makes you multiply numbers often, exactly where it's dangerous. And on top of that implicit integer conversions can make unsigned arithmetic surprisingly signed.
This article [0] goes into a bit more detail.
[0]: https://gist.github.com/rygorous/e0f055bfb74e3d5f0af20690759...
It's trivial to prove in that particular example because it's assumed that count is also an int (both i and count are in 32-bit registers).
> It also claims that the compiler cannot prove this in more complex examples (but gives no explicit examples).
The example doesn't even have to be that complex; you just need sizeof(n) > sizeof(i) and i to be signed (godbolt example at [0]). In that example, if the compiler can assume signed overflow is UB, then you get fairly sensible code generation. If the compiler cannot make that assumption (add the -fwrapv flag), then you get that movsxd instruction in the middle of the loop body. If you use -O2 instead of -O1, then -fwrapv prevents loop unrolling.
> What I'm saying is if the compiler cannot prove a lack of overflow in this way, that should be a warning.
I'm curious how frequently this warning might fire in real-life codebases, or when it does fire how frequently it's something the programmer can actually fix. Clang does have -Rpass-missed to emit some diagnostics when an optimization cannot be performed, but it doesn't emit anything in this particular case (though I'm not sure whether it's supposed to).
There's surely cases where it would be hard to suggest a "equally efficient without UB" replacement (eg when it wants to fuse two i64 into one), but it feels like the current position is too far on the axis of "provided with suspicious code, the compiler should just make that garbage run as fast as possible and call it a day"
Nobody during C89 formulation realized that it would end up “a license for the compiler to undertake aggressive optimizations that are completely legal by the committee's rules, but make hash of apparently safe programs” that “negates every brave promise X3J11 ever made about codifying existing practices, preserving the existing body of code, and keeping (dare I say it?) `the spirit of C.'” If they had, the response would have been that “[UB] must go. This is non-negotiable.”
I’m not quite sure what the compiler is supposed to do in that case. Insert nullchecks by itself to avoid crashes?
As soon as your first pass decides to rely on the presence of the dereference as a reason for removing the null check, the dereference needs to be flagged as critical, such that it cannot be removed by a subsequent pass.
Better still, if the compiler does try to remove something that was relied on by an earlier optimization step, alert the user that something is probably wrong with the code.
Also, this would prevent one of the main benefits of function inlining. Once you have inlined a function, the values of some of the parameters may be known at compile time, resulting in lots of those if(0) parameters. But that is something that was relied on in previous passes, even though it is known to be a static value in the current pass.
How can this be: C has a standard, when many other languages do not. Thus C is "more defined" than other languages. Yet somehow it ends up being less defined.
If C had no standard, it would be defined by its implementation, just like many other languages. But because it has a standard, it has undefined behavior.
The real trick is that without a standard, implementations have to be bug-compatible. They're too defined! The purpose of a standard is to define them less.
But for most intents and purposes Python is defined by its documentation, and it's fine.
Having said that UB in C and C++ means behavior that the standard doesn't define. If you remove the standard then UB either loses meaning or it makes all programs undefined. It definitely doesn't make all programs defined.
If C had a judge, like the legal system, he could deny attempts to use fuzziness in the standard (intended to prevent specifying bugs) to introduce bugs. Unfortunately, C doesn't have a judge with common sense, it has people who think the standard is code and anything that is undefined really is a license to do whatever you want.
I guess this problem was inevitable as soon as the words "nasal demons" were put to keyboard. Now the only thing that can save us is a reference implementation.