top | item 40788795

(no title)

Ontonator | 1 year ago

> Somewhat expectedly, gcc remains faithful to its crash approach, though note that it only inserts the crash when it compiles the division-by-zero, not earlier, like at the beginning of the function. […] The mere existence of UB in the program means all bets are off and the compiler could chose to crash the function immediatley upon entering it.

GCC leaves the print there because it must. While undefined behaviour famously can time travel, that’s only if it would actually have occurred in the first place. If the print blocks indefinitely then that division will never execute, and GCC must compile a binary that behaves correctly in that case.

discuss

order

Sesse__|1 year ago

Don't worry; a function blocking indefinitely (i.e., there is some point where it stops giving off side effects, and never returns) is also UB. C++ attempts to guarantee a certain amount of forward progress, for now.

ynik|1 year ago

But a function blocking indefinitely while repeatedly writing to a volatile variable is well-defined. So the compiler cannot remove a function call followed by UB unless it knows that the function won't do that.

In theory, the compiler could know that since `printf` is a well-known standard function. In practice, `printf` might even exit the program via SIGPIPE, so I don't think any compiler will assume that it definitely will return.

Ontonator|1 year ago

Be careful: it’s been a while since I used C and I haven’t used much C++, but I think forward progress is only guaranteed by C++, not C.

uecker|1 year ago

In C, undefined behavior can not time travel. This was never supported by the wording and we clarified this in C23.

Ontonator|1 year ago

Do you have a specific reference for this? I’d love to know more.

nlewycky|1 year ago

> If the print blocks indefinitely then that division will never execute, and GCC must compile a binary that behaves correctly in that case.

Is `printf` allowed to loop infinitely? Its behaviour is defined in the language standard and GCC does recognize it as not being a user-defined function.

Ontonator|1 year ago

I’m not sure it can loop indefinitely, but it can block (e.g. if the reader of the pipe is not reading from it and the buffer is full).

nayuki|1 year ago

Your reasoning is incorrect. Here is how I reason about it.

Division by zero is undefined behavior. The compiler can assume that it will not happen.

If the divisor is not zero, then the calculation has no side effects. The compiler may reorder the division above the print, because it would have no observable difference in behavior. This could be useful because division has a high latency, so it pays to start the operation as soon as the operand values are known.

If the divisor is zero, the UB says that there is no requirement on how it's compiled, so reordering the division above the print is legal.

gpderetta|1 year ago

  const int div = 0;
  if(div) { 
    return 1/div;
  }
  return 0;
The statement at line 3 would have undefined behaviour, yet is never reached so this is a perfectly legal program and any transformation that hoists it above the check is invalid.

If you replace 'if(div)' with an opaque function call, that doesn't change anything as the function might never exit the program, never return, long jump or return via an exception.

asdfaoeu|1 year ago

How can it both have no side effects and have undefined behaviour?

LoganDark|1 year ago

> While undefined behaviour famously can time travel, that’s only if it would actually have occurred in the first place.

I've always been told that the presence of UB in any execution path renders invalid all possible execution paths. That is, your entire program is invalid once UB exists, even if the UB is not executed at runtime.

Are you saying this isn't quite true?

ynik|1 year ago

That's not true.

If you do `5 / argc`, that's only undefined behavior if your program is called without any arguments; if there are arguments then the behavior is well defined.

Instead, the presence of UB in the execution path that is actually taken, renders invalid the whole execution path (including whatever happens "before" the UB). That is, an execution path has either defined or undefined behavior, it cannot be "defined up to point-in-time T". But other execution paths are independent.

Thus, UB can "time-travel", but only if it would also have occurred without time travel. It must be caused by something happening at runtime in the program on the time-travel-free theoretical abstract machine; it cannot be its own cause (no time travel paradoxes).

So the "time-travel" explanation sounds a lot more scary than it actually is.

masklinn|1 year ago

> Are you saying this isn't quite true?

It is not. The presence of UB in an execution path renders that execution path invalid. UBs are behaviours, essentially partial functions which are allowed to arbitrarily corrupt program state rather than error.

However "that execution path" can be extensive in the face of aggressive advanced optimisations.

The "time travel" issue is generally that the compiler can prove some paths can't be valid (they always lead to UB), so trims out those paths entirely, possibly leaving just a poison (a crash).

Thus although the undefined behaviour which causes the crash "should" occur after an observable side-effect, because the program is considered corrupt from the point where it will inevitably encounter an UB the side-effect gets suppressed, and it looks like the program executes non linearly (because the error condition which follows the side effect triggers before the side effect executes).

protomolecule|1 year ago

By this logic the function below would be UB. It isn't.

  void f()
  {
    int d;
    bool divide;
    std::cin >> d >> divide;
    std::cout << (divide ? 1/d : d);
  }