Remix.run Logo
▲ Veserv 5 hours ago

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.

▲cesarb 35 minutes ago | parent | next [-]

> 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”.

By the way, that's exactly what happens with Spectre and its class of ghostly vulnerabilities! The processor speculatively executes these "impossible" paths, and discards the result once it detects they couldn't happen; but there are ways to "leak" information from that irreal world through side-effects like cache lines being discarded.

▲drdexebtjl 4 hours ago | parent | prev | next [-]

This isn’t a good mental model. The compiler isn’t limited to just affecting the code after the undefined behavior. It is allowed to assume undefined behavior never happens for the entire program.

For example, this code with an improper guard:

    if (!p) puts("error");
    printf("%d", *p);
Since the program dereferences p in line 2, and dereferencing null is UB, the compiler is allowed to assume p is never null, so it’s allowed to delete line 1, even though it would have executed before the point where UB would happen.

Even worse, the compiler isn't just allowed to not do things you told it to do, it's also allowed to do anything too.

▲dgrunwald an hour ago | parent | next [-]

The compiler is only allowed to do that when it can prove `puts` will return (i.e. it must prove `puts` does not call `exit`). In a world with SIGPIPE that shouldn't happen. (unless maybe the compiler can prove that there's enough space available in the stdlib IO buffers so that puts won't actually output anything. but then there's no obversable difference in behavior)

▲Veserv 3 hours ago | parent | prev [-]

The question was: “The concept of undefined behaviour specific to C/C++ has always seemed batshit insane to me”.

I was explaining why undefined behavior as a concept is a very sensible idea. Whether the expansive interpretation of the optimizations you are allowed to do when encountering the “impossible” are reasonable is a different question.

▲p1necone an hour ago | parent | next [-]

Hence the 'specific to C/C++' part of that sentence. 'Undefined behaviour' is a probably unavoidable part of programming language design. The way that C/C++ interpret/handle it is batshit insane.

▲drdexebtjl 3 hours ago | parent | prev [-]

The part that is batshit insane is precisely that it makes the entire program impossible to reason about.

If UB meant what you described, it would be sensible, but it doesn't, and it's not.

▲rramadass 2 hours ago | parent | prev [-]

Beautifully explained!

UB is a formal tool for the optimizer.

Most people who argue about UB on HN have no clue wth it means and how it differs from Unspecified and Implementation-defined behaviours. The standard already explains expressions/statements and how they relate to sequence-points/sequenced-before/sequenced-after code points which is what is needed to understand the anomalous behaviour above.

Add in a introductory class in numerical analysis w.r.t. accuracy/precision/limits/rounding and the C/C++ programmer has enough knowledge to avoid problems in practice.