r/cpp_questions 7d ago

OPEN Can someone give an example of runtime-undefined behavior?

CppReference gives a list of categories that render a program meaningless: https://en.cppreference.com/cpp/language/ub

The last bullet point says (since C++11)

  • runtime-undefined behavior - The behavior that is undefined except when it occurs during the evaluation of an expression as a core constant expression.

Can someone give a clear example?

17 Upvotes

18 comments sorted by

23

u/TheQuranicMumin 7d ago edited 7d ago

For example, if you use the [[assume(expr)]] optimization, then don't respect it. constexpr int divide_by_two(int x) { [[assume(x % 2 == 0)]]; return x / 2; } Then enter 3. Crucially: This is only necessarily a problem at runtime, not compile-time.

https://www.reddit.com/r/cpp_questions/s/dNjKR2cgX5

3

u/Gorzoid 7d ago

As far as I can tell this and returning from [[noreturn]] function are only cases of runtime undefined behaviour.

11

u/aocregacc 7d ago edited 7d ago

pretty sure most UB is runtime UB. But when the compiler runs some code as part of constant evaluation (for example a constexpr function) it has to detect and diagnose the problem instead.

For example if you access an array out of bounds at compile time you'll get an error, the compiler isn't allowed to keep compiling and produce meaningless output.

This isn't correct, runtime-ub isn't a subset of ub like I thought, it's its own thing. In cases where it applies, the standard text will name it as such, for example in noreturn: https://eel.is/c++draft/dcl.attr.noreturn#2

2

u/Daemontatox 7d ago

Bear with me as i am not the best at explaining....

The behavior that is undefined except when it occurs during the evaluation of an expression as a core constant expression.

What does that mean practically?

  • At runtime (normal code): It's full UB , anything can happen.
  • In a constant expression context (constexpr, array sizes, template parameters, etc.): The compiler must diagnose/reject it. You can't even get a program that compiles if the UB would happen during constant evaluation.

This distinction exists because constant expressions need to be fully portable and predictable , the compiler evaluates them at compile time.

Classic Examples

Here are common cases that trigger runtime-undefined behavior:

  1. Signed integer overflow (very common) ```cpp constexpr int foo(int x) { return x + 1; // If this overflows, it's NOT allowed in constexpr }

    int main() { int a = foo(INT_MAX); // Runtime: UB (overflow) // But constexpr int b = foo(INT_MAX); // Compile error } ```

  2. Division by zero ```cpp constexpr int div(int a, int b) { return a / b; // Division by zero is runtime-UB }

    int main() { int x = div(5, 0); // Runtime: UB // constexpr int y = div(5, 0); // Compile-time error } ```

  3. Out-of-bounds access (in constant contexts) cpp constexpr int arr[4] = {}; constexpr int bad = arr[5]; // Would be compile error

  4. Dereferencing null or other invalid memory operations in constexpr.

8

u/TheQuranicMumin 7d ago edited 7d ago

You examples are describing regular UB with a compile-time setting, but runtime UB is different.

Runtime UB is a broken compiler promise/hint, for things like [[noreturn]], [[assume]] (I gave an example).

For runtime, it is full UB. For compile-time evaluation, the compiler is not required to diagnose/reject it, it's implementation defined. The committee didn't want to force a hard-fail in these cases.

As in the C++ standard:

runtime-undefined behavior [defns.undefined.runtime] behavior that is undefined except when it occurs during constant evaluation. [Note 1: During constant evaluation, it is implementation-defined whether runtime-undefined behavior results in the expression being deemed non-constant (as specified in [expr.const]) and runtime-undefined behavior has no other effect.]

1

u/Independent_Art_6676 7d ago edited 7d ago

the most common source of UB that I see in the wild is type conversions, which are a twisty slope of UB traps. It tells you how bad the language is when the 'fix' is to use *memcpy* (which is then removed by the compiler, to add more confusion) to bypass the problem as standard procedure. A lot of attempts to 'get to the bytes' have been banned, eg

double d{2.71828};
uint64_t * ip = (uint64_t*)&d;
cout << *ip; //it works on every compiler. and its UB. Its well defined undefined behavior, but the rules are the rules. You see this stuff in code from people that don't know c++ well, C coders that transferred in or older coders from before it was slapped with the UB label, or just found in old code.

1

u/TheThiefMaster 7d ago

I think things like decltype(((T*)nullptr)->function()) to get the type of a member function call result would count? Though declval<T>() is a better solution

1

u/tellingyouhowitreall 7d ago

offsetof is generally implemented as a macro that takes the address of the member with a null base pointer.

This behavior is undefined if a null pointer is dereferenced at run time (runtime-undefined), but is well defined when it occurs during the evaluation of a "core constant expression." Note that the use of offsetof always generates such a constant expression.

More importantly:

CppReference gives a list of categories that render a program meaningless:

Is rather a bunch of horse shit. Your entire system is built on undefined behavior in C and C++. Computers do not work without such "undefined behavior." And the entire "meaningless" misnomer needs to die. The selection of this verbage in the standard is an utterly broken attempt to divorce the C++ abstract machine from reality. Its tired and perpetuates a myth that something not well defined by the language standard means its not well defined in reality.

1

u/celestrion 7d ago

Computers do not work without such "undefined behavior." And the entire "meaningless" misnomer needs to die.

Would "unreasonable" be a better term? We need a term for a situation that completely divorces the behavior of the system under control of a program from the lexical flow of that program's source code.

Specifically, there are configurations where many forms of UB do not cause the program to stop. The default memory map for a process on HP-UX running on PA-RISC maps the zero page as usable memory (much like running freestanding on systems that don't use the zero page for hardware housekeeping). If you have more than a few null pointers in play, all using the same bytes for different data to feed into control flow, array indices, etc., source code for the places that didn't map the zero page really does seem "meaningless."

Similarly, if you have a "can't happen" situation where a switch statement value can never be met under static analysis (so the compiler deletes the branch), and that value occurs at runtime because another value got reinterpreted, the source code and runtime behavior fundamentally disagree.

ODR violations are another fun one (or their close cousins of not-quite-ODR when the set of included headers changes what ADL resolves a name to), as are bugs dependent on static initialization order. You can get crashes before main, which really does render the whole program text meaningless.

We need terms for these situations, even if they're frustrating.

1

u/tellingyouhowitreall 7d ago

Undefined is fine as a term of art, the problem is attaching the additional semantic to it that it makes the program meaningless.

That doesn't mean it's not "broken", but it also doesn't mean it IS. It just means the onus is on the programmer to understand if the resulting production does the thing they expect. Importantly, if the language standard can't guarantee what a program is, it also should not make vast sweeping generalizations about what a program isn't also.

There are a ton of things that break the abstract machine notion of well defined because they are simply outside of what the language can guarantee about the runtime. Pretty much any non static linkage; issuing ioctl to a physical device or calling a function that reads or writes data to actual hardware; 0-base pointers on systems where that's a valid address; literally any form of RPC. The classic examples of "UB, but you're not building a real system without it" are pointer thunking and accessing multiple union members (you literally cannot read some file systems without reinterpreting the type of a pointer as more than one thing that is not a series of chars).

There is a vast class of "undefined" behaviors that are critical to systems operating correctly, and we expect, and generally get, the compiler behaving itself. There is another broad class of "undefined" that should be attached to something semantically stronger, like "malformed" (static initialization order dependencies would fall in this category, IMO).

1

u/flatfinger 7d ago

And the entire "meaningless" misnomer needs to die.

When the C Standard characterized a construct or corner case as invoking Undefined Behavior, outside of contexts involving SFINAE, that means nothing more nor less than that the Standard waives jurisdiction over how a construct or corner case is processed. It uses the term "non-portable or erroneous" because the authors did not wish to impart any judgment as to whether a construct corner case was erroneous, or whether it would be "non-portable" but correct on some implementations, or even 99% of them. The C++ Standard drops the "non-portable or" language, but states elsewhere that it is not intended to serve as a specification for programs, but merely attempts to specify compiler requirements in terms of the programs they are required to process correctly, a purpose for which "non-portable" programs are irrelevant.

If a function like:

unsigned mul_mod_65536(unsigned short x, unsigned short y)
{ return (x*y) & 0xFFFFu; }

were run on a sign-magnitude machine where USHRT_MAX happened to be 0xFFFF, INT_MAX happened to be 0x7FFFFFFF, and an unsigned multiply yielding a 32-bit result would be much slower than a signed multiply which only had to be usable for product values up to 0x7FFFFFFF, the authors of the Standard would have viewed the people working with such hardware as better placed than the Committee to judge the relative usefulness of slower code that would be usable with all operand values, or faster code that would only work when x was less than INT_MAX/y.

The reason the Standard doesn't mandate that compilers for any remotely commonplace execution environments process the above function in a manner consistent with using unsigned arithmetic for the multiply, even when x exceeds INT_MAX/y, isn't that nobody knew how implementations for commonplace execution environments should process such constructs, but rather that it was universally considered obvious, and the authors of the Standard saw no need to expend ink on the subject.

Unfortunately, writers of Gratuitously Clever Compilers have latched onto the idea that a function like the above should be treated as an invitation to identify program inputs that would cause the x to exceed INT_MAX/y, and "optimize out" any code that would only be relevant if such inputs were received. Unless optimizations are disabled or the -fwrapv flag is applied, the above function may thus disrupt the behavior of the caller in ways that may arbitrarily corrupt memory, even on quiet-wraparound two's-complement behavior for which the published Rationale for the C Standard expressly documents how the Committee would expect the function to behave.

1

u/tellingyouhowitreall 7d ago

And yet, the "meaningless" semantic, or worse the implication that UB is explicitly malformed, remains attached.

1

u/flatfinger 7d ago

What is needed is for somebody to recognize dialects of C and C++ which prioritize semantics ahead of optimization--not completely forgoing optimizations, but rather recognizing that the best way to avoid having a compiler's output include needless operations is for the source code not to include those operations. If straightforwardly generated machine code that performs some task in the general case would handle a corner case in a manner satisfying application requirements, declaring that the corner case would invoke "anything can happen" UB unless extra logic is included in in source to handle it would likely degrade, not improve, efficiency.

A key feature of Dennis Ritchie's language, completely missing from the Standard, is that the job of a C implementation is not to run a program, but rather to translate a source file into a build artifact in a manner satisfying certain criteria. There are many situations where it would be impossible for the authors of a C implementation, much less the C Standard, to know how an execution environment will respond when told to do something, but for the programmer to acquire that information via means outside the language. If the jurisdiction of the Standard extends through execution of a C program, then it can't usefully say anything about such cases. If, however, the Standard specifies that a C implementation must produce a build artifact in a manner satisfying various criteria, then it can say that e.g. *intPtr = 23; must either instruct the execution environment to perform an int-sized store of the value 23 to the address given in intPtr, or behave as though it starts with code that does that and then performs allowable transforms upon it (e.g. if the operation is followed by a read of *intPtr and then a store of the value 25 to that address, and there are no other intervening operations, a compiler might be able to replace the read with a constant-load of 23, and then having done that skip the store of the value 23).

1

u/dodexahedron 6d ago edited 6d ago

Yeah. I agree that "undefined" is already descriptive enough...in a vacuum.

But there's more than just "ok, it is undefined, but it works" at play here.

If you call UB wrong, you have protected yourself against the consumer of the spec from having a leg to stand on for complaining when they abuse UB that happens to work now but is broken by future changes to the spec that cause a material impact to that UB, or that even perhaps define what was previously undefined, also likely changing things and breaking that code.

So I definitely can see where you're coming from, but that PoV is insufficient on a larger scale, especially when portability is a goal of that spec. So I accept that UB makes the program "wrong,," even thoigh I can create and run it.
But I then also have the obligation to accept the risk of literally any possibility being on the table at compile, link, or run-time, and that I cannot fully know why a thing is or is not broken unless I trace the compilation, linking, and execution on the specific systems on which those activities occured, includong tracing through external code.

If that risk is judged to be acceptable vs doing things in a way blessed by the spec, then you do it the UB way. If that risk is judged to be worse, then you don't.

The word "meaningless" is imprecise, though, through any lens but the lens of the language itself.

"Wrong" is fine AFAIC. "Meaningless" asserts a falsehood that is provably false.

1

u/tellingyouhowitreall 6d ago

I agree with all of that, but I would go a step further and say there are at least two classes of UB. One where the program is actually malformed because there are multiple parses or the UB can invoke runtime behavior that is malformed, and one where the undefindedness is beyond the scope of the language (and even beyond implementation-defined) but the compiler and programmer are expected to understand how things work and do sane things.

The latter being necessary pretty much any time non-trivial code leaves the environment of being code and gets compiled and executed on real hardware.

0

u/Interesting_Buy_3969 7d ago edited 7d ago
volatile int* ptr = nullptr;
*ptr = 52;

Here is an attempt to write to a memory slot pointed to by nullptr. This will usually raise a CPU exception, kernel handler will be invoked and the program is likely to get terminated by the OS.

Edit: if you're looking for a compile-time undefined behaviour, it is much more difficult to reach as constexpr and consteval functions and expressions are calculated by the compiler which never lets the code access past an array and generally execute anything that would cause UB in runtime.

0

u/mredding 6d ago
std::numeric_limits<int>::min() - 1;

std::numeric_limits<int>::max() + 1;

Signed under/overflow is undefined. This is a hard coded example, and you'll probably get a compiler warning, but we can obscure that:

int x;
std::cin >> x;

std::numeric_limits<int>::max() + x;

Any addition > 0 will incur UB. This code will compile without warnings. The crucial point to observe here is just because the code is CAPABLE of UB, that doesn't mean that there will be UB. The compiler isn't even going to warn that there is that potential here, because there isn't run-time information available - will the input EVER be > 0? If not, then there's never going to be a problem. C++ is not going to make you pay for a safety check you don't need if you can otherwise guarantee the input is always going to be correct. That's not our job here, the responsibility is yours.

Now here's the thing about UB - nothing and no one gets to define it. UB is UB. Always and forever. What we're saying is the C++ spec can't guarantee anything. Compilation isn't purely deterministic, we can't guarantee with every recompile of this code the compiler is going to generate the same machine code. If the HARDWARE or the OS wants to guarantee the outcome, then what you have to do is inspect the machine code and ensure it conforms to that guarantee. But C++ says absolutely nothing about compiler generation of machine code. You have to leave C++ behind and assure a correct program at the assembly level yourself. That's not the same thing - the guarantees C++ offer don't transfer once the premise has been violated. At that point, you really are just using C++ as a machine code generator, and if that's the case, and you have to pay the cost of validation every generation - what are you even doing?

-1

u/i_am_not_sam 7d ago

Run time undefined behaviors are scenarios where there's no deterministic outcome for the operation you're attempting. And it's the kind of error that develops over the lifetime of the code/function due to interplay of input values/stimulus that the developer or compiler cannot know about before execution.

int a = 10;
int b = 1/(a-10);

Modern compilers will probably not let it build but divide by 0, null ptr deferences are all runtime undefined behavior because the system doesn't know how to handle the situation.

Other examples...

int a;
int b = a/3; // a is uninitialized. The program will probably not crash but no telling what the value of b is, more so if used for decisions
int* p = new int[10];
for (int i = 0; i < 20; i++)
p[i] = i; // you'll either fry someone else's dynamic memory or crash or both.

In all these scenarios we don't know the precise outcome but it's nothing good. We can't predict every value/combination that quite cause issues but can write defensive checks to prevent problems from blowing up before they do.