For instance, the following code is not valid C code:
int a[2], b;
a[2] = 5;
The compiler is free to do anything it wants, because the standard has not defined what it does. It could stop. Most likely it will just overwrite whatever memory is after a[1] with 5, because that's easiest.If you want the compiler to do something specific, you need to define it. In some cases the standard has language for otherwise-defined behavior that is carved out as undefined (e.g., strict aliasing), and it's easy to go "Never mind"; in some cases it does not (e.g., writing to a string literal). For these ones, the behavior needs to be defined.
Simply saying "Don't optimize this" is just saying "I'm not going to define the behavior, but don't implement it this way," which isn't a particularly useful way to define a standard (e.g., for the null checks example, is the compiler permitted to leave the null check in, but insert a segfault right before it?).
No the compiler is not entitled to insert a segfault. Segaults come from architectures and operating systems, not from compilers. The compiler is free to leave in the dereference of the null pointer and then, who knows what happens. But the compiler can't break the code by deleting a perfectly conforming null check on the theory that a previous dereference means that the pointer is not null.
The original Bourne shell used signal to catch memory faults so it could operate dynamic paging! If your OS/implementation permits, C is supposed to allow you do do things like that. The compiler is NOT supposed to insulate the programmer from bumps. (Disclaimer: I am not endorsing the horrible design of the Bourne shell which was also written in a macro style that made C into pseudo Algol).
But when you are programming in C, you are programming against the C abstract machine, not against an architecture or operating system. It's this machine that you are breaking when you invoke ub.
Passive voice! You expect C code to go beyond the territory of the abstract machine. The authors of the standard, quite clearly, do not.
I can see both of these arguments. I think there is a place in the world for a C that is machine-specific. But it's clearly not what the standard authors have in mind, there are clearly people who find the genericness of C useful (cf. "portable assembler"), and piecemeal solutions like skipping individual bad optimizations won't solve the underlying problem that there's no way in C to say "Dude, I'm writing for a reasonable UNIX on a reasonable modern machine with a single flat address space for code and data and non-exception-raising two's-complement arithmetic and the ability to do nonaligned reads/writes and other normal things, please stop thinking I might also be writing for a PDP-11 bootloader."
I think you should go to the standards committee with a single proposal, which is that C should stop trying to be portable to weird / old architectures (and people who need them can use weird / old compilers, anyway). Among other things, you can now enable certain types of optimizations if the compiler is permitted to assume that arithmetic is two's-complement and nonaligned reads/writes work and so forth.
That is 100% wrong.
>Strictly conforming programs are intended to be maximally portable among conforming implementations. Conforming programs may depend upon nonportable features of a conforming implementation.
The whole concept of "undefined behavior" is to cope with the reality that C programs routinely go beyond the bounds of the abstract machine.
One of the most popular architectures for "a reasonable UNIX on a reasonable modern machine with a single flat address space for code and data" is the x86, and on it, some nonaligned reads/writes do trap. Certain optimizations can be done only if the compiler is permitted to assume that the data is aligned.
As for deleting null checks, time-travelling UB etc. the compiler always assumes the code is valid, bug-free for the compiler-specific definition of valid. This is at least the standard definition but it can be expanded by flags and extensions. Then they optimize that code without regard to what happens if the code actually had programming bugs.
If you want to add a definition for signed integer overflow (as C++ is considering, see http://wg21.link/P0907r0 ) then it would be defined and the compiler would no longer get to consider "if signed overflow" and "if false" the same.
Why is this different from letting the compiler optimize out the second check here?
int foo(unsigned int i) {
if (i < 5)
return this();
if (i < 4)
return that();
return those();
}
The C standard knows that NULL is special, a marker value for an invalid pointer. (The implementation does not have to translate it as the pointer with numeric value zero; the abstract machine just needs some value.) Absent an Optional/Maybe/etc. type, C needs some invalid pointer.What about the following—are you okay with compilers optimizing it?
struct pointer {
int *value;
char valid;
}
void *deref(struct pointer p) {
if (!p.valid)
abort();
return *p.value;
}
void stuff(struct pointer p) {
int n = deref(p);
if (!p.valid) {
fprintf(stderr, "Hey, that pointer wasn't valid!\n");
return;
...
}
No NULLs involved.> The original Bourne shell used signal to catch memory faults so it could operate dynamic paging! If your OS/implementation permits, C is supposed to allow you do do things like that.
I'd believe that (especially given the state of optimizers in Bourne's era) C does allow you to do things like this. I'd also believe that with sufficient -fno-unwanted-cleverness flags. C is the best way to do things like this.
But asking C to be designed to support things like this is quite a change. I get that this is exactly the change you're pushing for, but, the only way it can work is if the C abstract machine becomes hardware-dependent enough that you can embed platform assumptions in your C—and so C is no longer a "portable assembler."
There's precedent for this. One that comes to mind is the ability to cast a data pointer to a function pointer (which doesn't have to be valid; the abstract machine doesn't know if your physical machine uses different address spaces and different representations for the two, and even the brand new architecture WebAssembly uses different address spaces, but compatible representations). dlsym() implicitly requires this, and POSIX.1-2013 finally made an explicit requirement that a C implementation conforming to POSIX must define it in a meaningful way. http://pubs.opengroup.org/onlinepubs/9699919799/functions/dl...
I would certainly support a variant of C that did things like assume a flat address space, validity of all pointer values besides NULL (and an expectation that signals can catch and fix invalid pointer dereferences), char = unsigned char, 8-bit bytes, two's complement arithmetic, etc. It would not support every architecture C supports. It wouldn't be a good fit as a C standard, but as a variant for people who are happy to never target segmented real-mode x86 or the PDP-11, it seems pretty great.
That doesn't seem to be an argument against, or even related to, what the proposal entails. All of these proposals are reductive attempts to curb the instability that UB introduces, for little value (see the linux kernel).
> because there was nothing to transform them _from_
Yes there was. Anything that can result in UB.
> For C, that’s not something we can realistically do in every case.
That's why the proposal is not exhaustive, as described.