logoalt Hacker News

p1necone • today at 2:50 AM • 5 replies • view on HN

The concept of undefined behaviour specific to C/C++ has always seemed batshit insane to me, and I'm yet to read anything about it that has made it seem any less so.


Replies

nananana9 • today at 3:16 AM

It doesn't make sense to define what happens when you e.g. read from NULL because it's hardware specific - if you have virtual memory of some sort, you'd probably get a page mapping error. If you don't (embedded, WASM), you'd read back whatever value is at that address.

Does Rust define what I get when I dereference NULL in unsafe code? I doubt it, since it would require a NULL check before every pointer dereference.

The only insane thing about UB is that compiler writers took what everyone understand meant "the compiler emits what it emits and you get what you get" and turned it into "since it's undefined it means it can never happen so we can delete your null check".

➕ show 3 replies
Veserv • today at 3:37 AM

Undefined behavior is just what happens when you violate a mandatory precondition.

if (x > 0) {…}. But what if you entered the body when x <= 0?

if (false) {…}. But what if you execute the body?

These are “impossible”. What happens when the impossible occurs is “undefined”.

When the older standards said signed integer overflow for addition is undefined what they are actually saying is that the real definition of + is:

    int +(int x, int y) { 
      assert(in_range(actual_math_add(x, y), signed_int_min, signed_int_max));
      return machine_add(x, y);
    }
So of course what happens when you get signed overflow is undefined; you should hit that assert and your program should explode and die. You should “never” get to the next instruction.

But, in the interest of performance, “release mode” (which in this case is just any compilation) elides asserts since as a programmer you should not write code that asserts in much the same way that you should not write assert(false) in a normal code path that is supposed to run. Assertions are intended for “impossible” code paths and usually get compiled out in “release mode” though maybe your code is buggy and can actually hit them and then your program goes off the rails because it had a bug.

Put another way, if you did write assert(false) in a regular code path, would you find it unreasonable for the compiler to just delete the code after it? That is what undefined behavior is for.

➕ show 2 replies
mpyne • today at 3:14 AM

It's not specific to C or C++, though it is more prominent there.

Rust's unsafe mode, for instance, has undefined behavior. A whole list of them, in fact.

GrantMoyer • today at 4:46 AM

Let's take a simple example: writing past the end of an array. Allow me to argue with myself for a moment.

> Surely the compiler can just check if each access is valid.

Well, sometimes it can, but sometimes it doesn't know how long the array is. What if the array is passed as a pointer?

> Maybe each array could be annotated with its size at runtime, and accesses could be checked at runtime too.

That works, but it adds runtime cost that may legitimately be too much for some applications, for example, a Gameboy game (set aside that many Gameboy games were written in assembly).

> Fine, so we'll make the programmer promise to ensure array accesses are always valid. Maybe they'll make a mistake sometimes, but what's the worst that could happen? Throwing your hands in the air and saying the compiler is allowed to do anything, that's just stupid.

Well, maybe it's stupid, but this is one thing that could happen if you accidentally write past the end of an array: https://www.youtube.com/watch?v=Vjm8P8utT5g. I'm sure neither the programmers nor compiler writers intended that.

Ultimately, the compiler can't guarantee any behavior if its assumptions are violated. The example may seem contrived, but it demonstrates that, given the right circumstances, the results of the logical contradiction are unbounded. This is a direct consequence of the "Principle of explosion": https://en.wikipedia.org/wiki/Principle_of_explosion. On second thought, maybe the runtime costs of array bounds checking are an acceptable trade-off after all.

chasil • today at 3:14 AM

It was written for a PDP-11 with 64k of RAM.

There wasn't room for safe programming practices, and direct manipulation of the hardware was a design requirement.

It assumes that you know what you are doing.

There are also ports to the Zilog Z80, an architecture with similar limitations (UZI, FUZIX).