undefined behaviour — common traps

RoadmapsC++

Scenario

You write a function that occasionally forgets to return a value on a certain `if` branch. During local debug testing, it silently returns `0` and seems fine. In production, the compiler entirely deletes the function and the surrounding `if` statements, causing catastrophic logic failure.

Why does the C++ compiler intentionally rewrite and destroy your logic when you make a mistake?

Why it exists

Undefined Behavior is the secret to C++'s unrivaled speed. By refusing to define edge cases (like overflow or out-of-bounds), compiler writers are freed from emitting sluggish safety guards. They can generate hyper-optimized, assumption-driven machine code, placing the ultimate responsibility for program safety squarely onto the developer.

Mental model

Undefined Behavior (UB) is like violating the laws of physics in a simulation. Once you break a fundamental rule, the simulation doesn't just give you an error; it mathematically destabilizes. The compiler assumes you *never* break the rules, and it aggressively optimizes your code based on that absolute assumption.

Undefined Behavior (UB) means the C++ standard imposes no requirements on how the program should behave. Compilers exploit UB for extreme optimization: they assume UB never happens. If your code contains UB, the compiler is legally allowed to generate code that crashes, returns garbage, or entirely deletes your security checks.

Explanation

**The Philosophy of UB:**

Unlike Java or Python, which add runtime checks (like array bounds checking) to throw safe exceptions, C++ refuses to add hidden overhead. It strictly adheres to the principle of **zero-overhead abstractions**, putting the burden of correctness entirely on the programmer.

**Common Traps that trigger UB:**

1. **Signed Integer Overflow:** `INT_MAX + 1` is UB. The compiler assumes signed integers never overflow, allowing it to optimize loops heavily. (Note: Unsigned integer overflow is perfectly defined; it wraps around to 0).

2. **Out of Bounds Array Access:** `arr[10]` on a 5-element array is UB. It might crash, or it might silently overwrite another variable, leading to impossible-to-track bugs.

3. **Dereferencing Null or Dangling Pointers:** Using a pointer after it has been deleted or using a `nullptr` is UB.

4. **Missing Return Statement:** If a non-void function (other than `main`) reaches the end without returning a value, it is **always** UB, regardless of whether the caller uses the return value.

5. **Uninitialized Variables:** Reading an uninitialized local variable (e.g., `int x; cout << x;`) is UB. It might print 0, it might print garbage, or it might crash.

6. **Shift out of bounds:** Shifting a 32-bit integer by 32 or more bits (`x << 32`) is UB.

**Time Travel Optimization & Assumption Propagation:**

If the compiler sees that an `if` statement leads to UB, it assumes that branch can never be taken and deletes it. Similarly, if you perform a potentially UB operation (like dereferencing a pointer), the compiler assumes it must be valid (not null). It will then propagate this assumption forward and aggressively delete any subsequent `if (ptr == nullptr)` safety checks, leading to a hard crash.

Code examples

UB and Compiler Optimization

bool verify_password(int attempt) {
    // Signed integer overflow is UB!
    // The compiler assumes attempt + 1 > attempt is ALWAYS true.
    if (attempt + 1 < attempt) {
        return false; // Security check for overflow
    }
    return true;
}

Because `attempt + 1 < attempt` implies signed integer overflow (which is UB), the compiler assumes it can never happen. The compiler will optimize this function to simply `return true;`, completely deleting your security check.

Key points

Common mistakes

Recall questions

Questions & answers

A developer writes: `int x; if (x == x) doWork();`. Using `-O3` optimization, `doWork()` is occasionally skipped. How is `x == x` evaluating to false?

Because `x` is an uninitialized local variable, reading its value triggers Undefined Behavior. The compiler is not required to evaluate `x == x` as mathematically true; it is allowed to assume the code path is invalid or aggressively optimize it away entirely.

Approach: Recognize that logic laws break down when applied to uninitialized memory.

To prevent null pointer crashes, a junior engineer writes: `int val = *ptr; if (ptr == nullptr) return -1;`. The program crashes anyway. Why did the compiler delete the null check?

The pointer is dereferenced *before* the null check. Since dereferencing a null pointer is UB, the compiler assumes `ptr` can never be null (because the program would be invalid). It optimizes by completely removing the `if (ptr == nullptr)` check, causing a hard crash.

Approach: Understand that performing an operation (dereferencing) forces the compiler to assume validity, allowing it to delete subsequent safety checks via assumption propagation.

Continue learning

Previous: Memory leaks, dangling pointers & double free

Return to C++ Roadmap