undefined behaviour — common traps
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
- UB is not an 'error': UB gives the compiler legal permission to do absolutely anything, including deleting your code.
- Compilers assume perfection: Optimization algorithms are mathematically built on the assumption that your code contains zero UB.
Common mistakes
- Testing UB locally and assuming it's safe: Developers often trigger UB, observe that it returns a predictable garbage value or a 0 locally, and think 'I can live with that.' UB is non-deterministic. It might work locally, but a different compiler version, an OS update, or turning on `-O3` optimization flags will suddenly cause the program to format your hard drive or skip execution entirely.
Recall questions
- What is the difference between signed and unsigned integer overflow in C++?
- Why doesn't C++ perform automatic bounds checking on arrays to prevent UB?
- What happens if a non-void function finishes execution without hitting a `return` statement?
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.