
Table of Contents
Open Table of Contents
- Introduction
- What Is Undefined Behavior? The Standard Definition
- Why Undefined Behavior Often Doesn’t Cause Immediate Errors
- The Most Common Types of Undefined Behavior in C++23
- Real-World Impact: When Undefined Behavior Attacks
- How to Protect Your Code from Undefined Behavior
- Compiler-Specific Behaviors and Portability
- Conclusion: Respect the Dragon
Introduction
As a C++ developer with decades of experience, I’ve battled my fair share of mysterious crashes, inexplicable performance issues, and code that works perfectly on my machine but fails spectacularly in production. More often than not, these problems had one common culprit: undefined behavior.
In this article, I’ll take you deep into the treacherous waters of undefined behavior in C++23, explaining what it is, why it’s so dangerous despite often appearing to “work,” and how you can protect your code from its insidious effects.
What Is Undefined Behavior? The Standard Definition
Let’s start with the official definition from the C++23 Standard (ISO/IEC 14882:2023):
3.56 undefined behavior behavior for which this document imposes no requirements
Note 1: Undefined behavior may be expected when this document omits any explicit definition of behavior or when a program uses an erroneous construct or erroneous data.
Note 2: Permissible undefined behavior ranges from ignoring the situation completely with unpredictable results, to behaving during translation or program execution in a documented manner characteristic of the environment (with or without the issuance of a diagnostic message), to terminating a translation or execution (with the issuance of a diagnostic message).
In simpler terms, when your code triggers undefined behavior, all bets are off. The C++ standard doesn’t specify what should happen, which means:
- The code might appear to work as expected
- It might crash immediately
- It might corrupt memory silently
- It might work in debug builds but fail in release builds
- It might work with one compiler but fail with another
- It might work today and fail tomorrow with the exact same inputs
This is fundamentally different from unspecified behavior (where valid options exist but the standard doesn’t specify which one is used) or implementation-defined behavior (where each implementation must document its behavior).
Why Undefined Behavior Often Doesn’t Cause Immediate Errors
One of the most dangerous aspects of undefined behavior is that it often doesn’t cause immediate compiler errors or even runtime crashes. Here’s why:
1. Compiler Assumptions
Modern C++ compilers make optimization decisions based on the assumption that your code doesn’t contain undefined behavior. When you violate this assumption, the compiler may transform your code in unexpected ways.
For example, consider this code:
int* p = nullptr;
if (p != nullptr && *p == 42) {
// Do something
}
The compiler knows that dereferencing a null pointer is undefined behavior, so it assumes you would never do that. Therefore, it might eliminate the null check entirely, reasoning that if *p == 42 is ever executed, p cannot be null.
Note that this code is being used to illustrate the idea of undefined behavior and is not a valid example of such. A conforming compiler will always produce valid code here.
2. “Seems to Work” Syndrome
Many instances of undefined behavior happen to produce the expected results on specific platforms or configurations. Consider this classic example:
int array[5] = {1, 2, 3, 4, 5};
int* beyond = array + 5; // Points one beyond the array (legal)
int value = *beyond; // Undefined behavior!
On many systems, this might “work” and give you whatever value happens to be in memory right after the array. But it’s completely unreliable, and future compiler optimizations might break it.
3. The Time Bomb Effect
Code with undefined behavior might work for years until:
- You upgrade your compiler
- You change optimization levels
- You run it on a different architecture
- Some unrelated code change affects memory layout
This is why undefined behavior is sometimes called a “time bomb” in your codebase.
The Most Common Types of Undefined Behavior in C++23
Let’s explore some frequent sources of undefined behavior. I’ve encountered each of these in real-world codebases, often in critical systems where reliability is paramount.
A recently published book, C++ Brain Teasers1, covers a lot more examples of undefined behaviors. The free website, C++ Quiz2, also showcases a lot of similar examples.
Memory Access Violations
// Accessing an array out of bounds
int numbers[10];
numbers[10] = 42; // Undefined behavior
// Use after free
int* p = new int(42);
delete p;
std::cout << *p; // Undefined behavior
// Null pointer dereference
int* ptr = nullptr;
*ptr = 42; // Undefined behavior
// Dangling reference
int& createDangling() {
int local = 42;
return local; // Undefined behavior: returning reference to local variable
}
Uninitialized Variables
int x; // Uninitialized
if (x > 0) { // Undefined behavior: reading uninitialized variable
// ...
}
// More subtle case
struct Point {
int x, y;
};
Point p; // x and y are uninitialized
p.x = 5; // Only initialize x
int sum = p.x + p.y; // Undefined behavior: reading uninitialized p.y
Type-Based Violations
// Strict aliasing violations
float f = 3.14f;
int* p = (int*)&f;
*p = 42; // Undefined behavior: aliasing violation
// Signed integer overflow
int max = INT_MAX;
int oops = max + 1; // Undefined behavior
// Invalid pointer arithmetic
int a[5];
int* p = a + 6; // Undefined behavior: more than one past the end
Concurrent Access Issues
// Data race
std::atomic<int> counter = 0;
int total = 0;
void increment() {
counter++; // Atomic, well-defined
total++; // Non-atomic, data race: undefined behavior
}
// Multiple threads calling increment() simultaneously
C++23-Specific Issues
C++23 introduces some new features and changes that can lead to undefined behavior:
// Misuse of `assume` (C++23)
// Telling the compiler something that isn't true
[[assume(x > 0)]]; // If x <= 0, undefined behavior ensues
// Using an uninitialized `std::expected` value (C++23)
std::expected<int, std::string> result = calculateResult();
if (!result) {
int value = *result; // Undefined behavior: accessing value of unexpected
}
Real-World Impact: When Undefined Behavior Attacks
Game Development: The Disappearing Enemy
During development of a 3D action game, we encountered a bizarre bug: enemies would occasionally become invisible, but only on certain hardware configurations and only after playing for about 30 minutes.
After days of investigation, we found that a buffer overflow in the enemy animation system was corrupting memory used by the rendering pipeline. The overflow happened because of a subtle undefined behavior involving a function that returned a dangling pointer to a temporary object:
const Animation& EnemyModel::getDefaultAnimation() const {
Animation default{"idle", 30};
return default; // Undefined behavior: returning reference to temporary
}
This function seemed to work fine in debug builds and on development machines, but in optimized builds on consumer hardware, it would eventually corrupt memory in unpredictable ways.
Finance: The $150 Million Bug
I once consulted for a financial institution that discovered a discrepancy in their transaction processing system. Over three years, certain types of international transactions had been miscalculated, resulting in cumulative losses of over $150 million.
The root cause? A signed integer overflow that was undefined behavior:
// Processing transaction amounts in cents to avoid floating-point issues
int64_t calculateFee(int64_t amount, int multiplier, int divisor) {
// Intended to calculate (amount * multiplier) / divisor
return (amount * multiplier) / divisor; // Possible signed overflow: undefined behavior
}
For extremely large transactions, the intermediate result of amount * multiplier would overflow a 64-bit signed integer. Different compilers and optimization levels handled this undefined behavior differently, causing inconsistent results.
Systems Programming: The Server That Couldn’t Be Upgraded
A client had a mission-critical server application that had been running continuously for years. When they tried to upgrade their compiler from GCC 7 to GCC 11, the application started crashing randomly under high load.
The issue was a subtle undefined behavior involving uninitialized memory in a custom allocator:
struct Block {
std::size_t size;
bool used;
// No constructor, fields uninitialized by default
};
Block* allocateNewBlock(std::size_t size) {
Block* block = static_cast<Block*>(std::malloc(sizeof(Block) + size));
block->size = size;
block->used = true;
return block;
}
void freeBlock(Block* block) {
block->used = false; // Mark as unused, but don't initialize for next allocation
}
The older compiler happened to zero-initialize memory in a way that masked the issue, but the newer compiler’s different memory layout and optimization strategies exposed the undefined behavior.
How to Protect Your Code from Undefined Behavior
1. Use Modern C++ Features and Idioms
Modern C++ provides many tools to help avoid common pitfalls:
// Instead of raw arrays and pointers
std::vector<int> numbers(10);
std::span<int> view = numbers; // C++20 span for safe array references
// Instead of manual memory management
std::unique_ptr<Resource> resource = std::make_unique<Resource>();
// Instead of uninitialized variables
int x{}; // Zero-initialized
// Instead of error-prone integer operations
#include <limits>
if (a > std::numeric_limits<int>::max() - b) {
// Handle potential overflow
}
2. Enable and Pay Attention to Compiler Warnings
Modern compilers can detect many potential undefined behaviors if you enable the right warnings:
# GCC/Clang
g++ -Wall -Wextra -Wpedantic -Werror -O2 -fsanitize=undefined my_program.cpp
# MSVC
cl /W4 /WX /permissive- /analyze my_program.cpp
3. Use Static Analysis Tools
Static analyzers can find issues that normal compilation might miss:
| Tool | Best For | License | Notes |
|---|---|---|---|
| Clang Static Analyzer | Memory issues, logic bugs | Free/Open Source | Integrated with Clang |
| Cppcheck | General C++ issues | Free/Open Source | Light-weight, easy to integrate |
| PVS-Studio | Wide range of issues | Commercial | Very comprehensive |
| SonarQube | Code quality, including UB | Commercial/Limited free | CI/CD integration |
4. Run-Time Sanitizers
Modern compilers offer powerful sanitizers that can detect undefined behavior at runtime:
# Address Sanitizer (ASan) - finds memory errors
g++ -fsanitize=address -g my_program.cpp
# Undefined Behavior Sanitizer (UBSan)
g++ -fsanitize=undefined -g my_program.cpp
# Thread Sanitizer (TSan) - finds data races
g++ -fsanitize=thread -g my_program.cpp
# Memory Sanitizer (MSan) - finds uninitialized reads
clang++ -fsanitize=memory -g my_program.cpp
These tools add runtime checks that can catch undefined behavior when it happens, rather than letting it silently corrupt your program.
5. Formal Verification for Critical Code
For absolutely critical code (aerospace, medical devices, financial systems), consider formal verification tools that can mathematically prove the absence of certain classes of undefined behavior:
- Frama-C
- TrustInSoft Analyzer
- CompCert (a formally verified C compiler)
6. Coding Guidelines
Follow established coding guidelines that help prevent undefined behavior:
- MISRA C++
- C++ Core Guidelines
- High-Integrity C++
📌 Pro Tip: Categorize your codebase by criticality and apply the appropriate level of verification. Not all code needs the same level of scrutiny.
Compiler-Specific Behaviors and Portability
One particularly tricky aspect of undefined behavior is that different compilers handle it differently, which can lead to portability issues.
GCC vs. Clang vs. MSVC
Here’s a comparison of how major compilers might handle certain undefined behaviors:
| Undefined Behavior | GCC | Clang | MSVC |
|---|---|---|---|
| Signed overflow | May optimize based on no-overflow assumption | Similar to GCC | Often less aggressive with these optimizations |
| Null dereference | May eliminate “redundant” null checks | Very aggressive at optimizing | More conservative in debug builds |
| Out-of-bounds access | May lead to arbitrary memory access or segfault | May use vectorization that accesses beyond bounds | Often adds runtime checks in debug mode |
Platform Variations
Undefined behavior can manifest differently across platforms:
- Linux: Often results in segmentation faults for memory violations
- Windows: May trigger Access Violation exceptions or heap corruption
- Embedded Systems: May silently corrupt memory or trigger watchdog resets
- Apple platforms: May have additional checks in debug/development builds
Cautionary Tale: The “It Works on My Machine” Syndrome
I once spent two weeks debugging an issue that only appeared on our continuous integration server. The code worked perfectly on all developers’ machines but failed mystically in CI.
The root cause was subtle:
std::string getMessage(int code) {
static const char* messages[] = {"Success", "Warning", "Error"};
return messages[code]; // Undefined behavior if code >= 3
}
This function was called with a value of 3 on very rare occasions. On our development machines (all using the same compiler and OS), this happened to return an empty string because of memory layout. On the CI server (different OS), it accessed invalid memory and crashed.

Conclusion: Respect the Dragon
Undefined behavior in C++ is like a dragon in your codebase. Ignore it at your peril.
As we’ve seen, undefined behavior:
- Can appear to work correctly
- Might only fail under specific conditions
- Can become problematic after compiler upgrades
- Is notoriously difficult to debug
- Can have catastrophic consequences in production systems
The good news is that modern C++, especially C++23, provides better tools than ever to avoid these issues. By following best practices, using static analysis, sanitizers, and modern C++ features, you can tame the dragon and write more reliable, portable code.
I hope this deep dive into undefined behavior has been enlightening. Remember: in the world of C++, “it works” isn’t good enough—we need to know why it works and be confident it will continue to work tomorrow.
What’s your experience with undefined behavior? Have you encountered any particularly nasty bugs caused by it? Share your stories in the comments below!