Understanding & Avoiding Undefined Behavior
Learn what undefined behavior is, why it exists, how compilers exploit it for optimization, and how to detect it with sanitizers and compiler warnings.
What Is Undefined Behavior and Why Does It Exist?
Undefined behavior (UB) means the C++ standard places no requirements on the program's behavior. The program might crash, produce wrong results, appear to work correctly, or format your hard drive — all are "valid" outcomes.
UB exists because it enables aggressive optimization. When the compiler knows that certain situations "cannot happen" (because they would be UB), it can generate faster code by omitting checks for those situations. For example:
- Signed integer overflow is UB allows the compiler to assume x + 1 > x is always true, enabling loop optimizations.
- Dereferencing a null pointer is UB allows the compiler to eliminate null checks after a dereference.
- Accessing out-of-bounds memory is UB allows the compiler to omit bounds checks, making array access as fast as raw memory.
The fundamental bargain: C++ trusts the programmer to avoid UB in exchange for maximum performance. When that trust is violated, all bets are off.
Common Sources of Undefined Behavior
Here are the most frequent sources of UB in real-world C++ code. Every C++ programmer encounters these.
#include <vector>
#include <iostream>
#include <cstdint>
// 1. SIGNED INTEGER OVERFLOW
void signed_overflow() {
int x = INT32_MAX;
int y = x + 1; // UB! Signed overflow is undefined
// unsigned overflow is well-defined (wraps around)
unsigned u = UINT32_MAX;
unsigned v = u + 1; // OK: v == 0 (well-defined wrap)
}
// 2. NULL POINTER DEREFERENCE
void null_deref(int* ptr) {
int val = *ptr; // UB if ptr is null
if (ptr == nullptr) { // Compiler may remove this check!
// The compiler reasons: "ptr was already dereferenced above,
// which would be UB if null. Since UB cannot happen in a
// correct program, ptr must be non-null. This check is dead code."
std::cout << "null!\n";
}
}
// 3. OUT-OF-BOUNDS ACCESS
void out_of_bounds() {
int arr[5] = {1, 2, 3, 4, 5};
int bad = arr[10]; // UB! No bounds checking on raw arrays
// std::vector::operator[] is also UB if out of bounds
// Use .at() for bounds-checked access (throws std::out_of_range)
}
// 4. USE-AFTER-FREE / DANGLING REFERENCES
int& dangling_reference() {
int local = 42;
return local; // UB! Reference to destroyed local
}
// 5. DATA RACES
// int shared = 0;
// Thread 1: shared++; // UB if concurrent with Thread 2
// Thread 2: shared++; // UB! Data race on non-atomic variableThe "Time Travel" Problem
One of the most counterintuitive aspects of UB is that it can affect code before the UB point. Compilers optimize based on the assumption that UB never happens in a correct program, and these optimizations can reorder or eliminate code that appears before the UB.
Consider:
``cpp``
bool checked = false;
void process(int* p) {
int value = *p; // UB if p is null
checked = true; // This line is BEFORE the use of 'value'
if (p == nullptr) { // Compiler: "p was dereferenced, so it's not null"
abort(); // Compiler: "This is dead code, remove it"
}
use(value);
}
The compiler may set checked = true before the dereference (reordering for the pipeline), and then remove the null check entirely. The result: checked is set to true even when p is null, and the null dereference proceeds without the abort. The UB "traveled back in time" to corrupt the checked flag.
This is not a compiler bug — it is a direct consequence of the optimization freedoms that UB grants.
Detecting UB with Sanitizers
Sanitizers are compiler-provided tools that instrument your code to detect UB at runtime. They add runtime checks that catch UB with clear error messages. Always run your test suite with sanitizers enabled.
# AddressSanitizer (ASan) — detects:
# - Buffer overflow (stack, heap, global)
# - Use-after-free, use-after-return
# - Double-free, memory leaks
g++ -fsanitize=address -fno-omit-frame-pointer -g -O1 program.cpp -o program
# UndefinedBehaviorSanitizer (UBSan) — detects:
# - Signed integer overflow
# - Null pointer dereference
# - Out-of-bounds array access
# - Misaligned memory access
# - Type punning violations
g++ -fsanitize=undefined -fno-omit-frame-pointer -g program.cpp -o program
# ThreadSanitizer (TSan) — detects:
# - Data races between threads
# - Lock order inversions (potential deadlocks)
g++ -fsanitize=thread -g program.cpp -o program
# MemorySanitizer (MSan) — detects:
# - Reads of uninitialized memory
# (Clang-only, not available in GCC)
clang++ -fsanitize=memory -fno-omit-frame-pointer -g program.cpp -o program
# Combine ASan + UBSan (common in CI pipelines)
g++ -fsanitize=address,undefined -fno-omit-frame-pointer -g -O1 \
-Wall -Wextra -Werror program.cpp -o program
# CMake integration
# In CMakeLists.txt:
# target_compile_options(myapp PRIVATE -fsanitize=address,undefined)
# target_link_options(myapp PRIVATE -fsanitize=address,undefined)Compiler Warnings as a Safety Net
Modern compilers can detect many potential UB situations at compile time. Treat warnings as errors (-Werror) in CI to ensure no warning is ignored.
# Recommended warning flags for all C++ projects
g++ -std=c++23 \
-Wall # Enable most warnings \
-Wextra # Enable additional warnings \
-Wpedantic # Strict ISO C++ compliance \
-Werror # Treat warnings as errors \
-Wconversion # Warn on implicit narrowing conversions \
-Wshadow # Warn when a variable shadows another \
-Wnon-virtual-dtor # Warn on classes with virtual functions but non-virtual dtor \
-Wold-style-cast # Warn on C-style casts \
-Wcast-align # Warn on potentially misaligned casts \
-Wunused # Warn on unused variables/functions \
-Woverloaded-virtual # Warn when a derived class hides a base virtual function \
-Wnull-dereference # Warn on potential null dereferences (GCC) \
-Wdouble-promotion # Warn when float is implicitly promoted to double \
-Wformat=2 # Warn on printf/scanf format string issues \
program.cpp -o program
# For Clang, add:
# -Weverything # Enable ALL warnings (then selectively disable)
# -Wno-c++98-compat # Disable C++98 compatibility warningsFollow these guidelines to minimize UB in your codebase.
- Enable all warnings and treat them as errors:
-Wall -Wextra -Wpedantic -Werror - Run sanitizers in CI: Combine ASan + UBSan for every test run; TSan for multithreaded code
- Use
.at()instead of[]for bounds-checked container access during development - Use smart pointers (
unique_ptr,shared_ptr) instead of rawnew/deleteto eliminate use-after-free - Use
std::atomicfor variables shared between threads — never use plain types for cross-thread data - Prefer unsigned types for bit manipulation but signed types for arithmetic (unsigned arithmetic wraps, which can cause subtle bugs in loop bounds)
The most dangerous UB is code that appears to work perfectly in testing but fails in production. Here is why.
- UB can "work" for years and then break when you upgrade the compiler, change optimization levels, or run on a different CPU
- Signed overflow in loops:
for (int i = 0; i < n; i++)— ifnisINT_MAX,i++overflows. The compiler may assume the loop terminates and optimize accordingly - Reading uninitialized variables: The value is not "random" — the compiler may optimize away the variable entirely or propagate whatever bits happen to be in the register
- Strict aliasing violations: Casting
int*tofloat*and dereferencing is UB (except throughchar*orstd::byte*). Usestd::bit_cast(C++20) for type punning std::string_viewoutliving the string: Returns a dangling view that appears to work until the memory is reused
- Undefined behavior exists to enable aggressive compiler optimizations — C++ trades safety for performance
- UB can "time travel" — the compiler may reorder or eliminate code that precedes the UB point
- The most common UB sources are signed overflow, null dereference, out-of-bounds access, use-after-free, and data races
- Sanitizers (ASan, UBSan, TSan, MSan) detect UB at runtime — run them in every CI pipeline
- Compiler warnings (
-Wall -Wextra -Werror) catch many potential UB situations at compile time - UB that "works" today can break silently with a new compiler, optimization level, or platform
Quiz — Test Your Knowledge
(15 XP)1. Why is signed integer overflow undefined behavior in C++?
2. What does AddressSanitizer (ASan) detect?
3. What is the 'time travel' problem with undefined behavior?