Lesson 67
Undefined Behavior
Covers what undefined behavior is, how it differs from implementation-defined and unspecified behavior, common causes, and best practices for writing safer C++ code.
Lesson content
What is Undefined Behavior in C++?
Undefined Behavior (UB) in C++ refers to situations where the C++ language standard imposes no requirements on what a program should do. When a program executes code that results in undefined behavior, anything can happen: it may appear to work correctly, produce incorrect results, crash, or behave differently on another compiler or operating system.
Think of undefined behavior like driving on a road that suddenly ends. Once you leave the defined path, there are no rules governing what happens next. Similarly, once a C++ program enters undefined behavior, the compiler is no longer required to produce predictable results.
Undefined behavior exists because the C++ standard prioritizes performance and flexibility. Instead of forcing compilers to check every possible programming error at runtime, the language assumes the programmer follows the rules. This allows compilers to generate highly optimized code.
Real-world use cases
Although undefined behavior is not something you should intentionally use, understanding it is essential for the following areas.
- Debugging difficult runtime errors
- Writing safe system software
- Game development
- Embedded programming
- High-performance computing
- Compiler optimization awareness
- Security-focused software development
Prerequisites
- Variables and data types
- Functions
- Arrays
- Pointers and references
- Dynamic memory allocation
- Basic debugging techniques
What is undefined behavior?
Undefined behavior occurs when a program performs an operation that the C++ standard does not define.
- Dereferencing a null pointer
- Accessing an array outside its bounds
- Using a dangling pointer
- Signed integer overflow
- Reading an uninitialized variable
- Modifying the same object multiple times without sequencing (in older standards)
Once undefined behavior occurs, the compiler may assume it never happened and optimize the program accordingly.
Undefined behavior vs other behaviors
- Undefined Behavior — no rules defined by the C++ standard
- Implementation-Defined Behavior — compiler documents its behavior
- Unspecified Behavior — compiler chooses one valid behavior without documentation
- Well-Defined Behavior — behavior is completely specified by the C++ standard
Why is undefined behavior dangerous?
Undefined behavior can produce different results on different compilers, appear only in optimized builds, cause intermittent crashes, corrupt memory, introduce security vulnerabilities, and make debugging extremely difficult.
Common sources of undefined behavior
- Null pointer dereference — e.g. *ptr where ptr == nullptr
- Out-of-bounds array access — e.g. arr[10] for an array of size 5
- Use-after-free — accessing memory after delete
- Uninitialized variable — reading an uninitialized local variable
- Signed integer overflow — e.g. INT_MAX + 1
- Invalid pointer arithmetic — moving a pointer outside an array
- Data races — unsynchronized access to shared data from multiple threads
Example 1: Reading an uninitialized variable
#include <iostream>
using namespace std;
int main()
{
int number;
cout << number << endl; // Undefined behavior: reading uninitialized value
return 0;
}
// Correct version:
#include <iostream>
using namespace std;
int main()
{
int number = 0;
cout << number << endl; // Safe to read
return 0;
}Example 2: Out-of-bounds array access
#include <iostream>
using namespace std;
int main()
{
int numbers[5] = {1, 2, 3, 4, 5};
cout << numbers[10] << endl; // Undefined behavior
return 0;
}
// Correct version:
#include <iostream>
using namespace std;
int main()
{
int numbers[5] = {1, 2, 3, 4, 5};
cout << numbers[4] << endl; // Access the last valid element
return 0;
}Example 3: Use-after-free
#include <iostream>
using namespace std;
int main()
{
int* ptr = new int(50);
delete ptr;
cout << *ptr << endl; // Undefined behavior
return 0;
}
// Correct version:
#include <iostream>
using namespace std;
int main()
{
int* ptr = new int(50);
cout << *ptr << endl; // Use the memory while valid
delete ptr;
ptr = nullptr;
return 0;
}Example 4: Signed integer overflow
#include <iostream>
#include <climits>
using namespace std;
int main()
{
int value = INT_MAX;
value = value + 1; // Undefined behavior
cout << value << endl;
return 0;
}
// Correct version:
#include <iostream>
#include <climits>
using namespace std;
int main()
{
long long value = static_cast<long long>(INT_MAX) + 1; // Larger type
cout << value << endl; // Defined behavior
return 0;
}Example 5: Returning the address of a local variable
#include <iostream>
using namespace std;
int* getValue()
{
int value = 100;
return &value; // Undefined behavior
}
int main()
{
int* ptr = getValue(); // Dangling pointer
cout << *ptr << endl; // Undefined behavior
return 0;
}
// Correct version:
#include <iostream>
using namespace std;
int getValue()
{
int value = 100;
return value; // Safe return
}
int main()
{
cout << getValue() << endl; // Safe usage
return 0;
}Common mistakes and pitfalls
- Reading uninitialized variables. Fix: initialize variables before use
- Accessing arrays beyond their size. Fix: validate indices
- Using pointers after delete. Fix: reset pointers or use smart pointers
- Dereferencing nullptr. Fix: check for nullptr before use
- Returning addresses of local variables. Fix: return by value or use appropriate ownership
// Wrong
int* ptr = nullptr;
std::cout << *ptr;
// Correct
int value = 10;
int* ptr = &value;
std::cout << *ptr;Best practices
- Always initialize variables before reading them
- Prefer standard library containers such as std::vector over raw arrays
- Use smart pointers (std::unique_ptr and std::shared_ptr) instead of manual memory management when appropriate
- Enable compiler warnings: -Wall -Wextra -Wpedantic
- Use runtime analysis tools such as AddressSanitizer (ASan) and UndefinedBehaviorSanitizer (UBSan) during development
- Compile debug builds with -g -O0 to simplify debugging
- Avoid relying on behavior that "works on my compiler"
- Validate array indices and pointer values before use
When not to ignore undefined behavior
Never ignore undefined behavior because it may not fail immediately, optimized builds can behave differently from debug builds, compiler updates may change program behavior, it can introduce security vulnerabilities, and small bugs can become serious production issues. Instead of working around undefined behavior, identify the root cause and correct the code.
Summary
- Undefined Behavior in C++ occurs when the language standard places no requirements on the program's behavior
- Programs exhibiting undefined behavior may crash, appear to work, or produce unpredictable results
- Common causes include null pointer dereferences, use-after-free, out-of-bounds access, and signed integer overflow
- Undefined behavior differs from implementation-defined and unspecified behavior
- Modern tools such as AddressSanitizer and UndefinedBehaviorSanitizer help detect many issues
- Following modern C++ practices greatly reduces the risk of undefined behavior
Frequently asked questions
What is undefined behavior in C++?
Undefined behavior occurs when a program performs an operation for which the C++ standard defines no required outcome, making the program's behavior unpredictable.
Why doesn't the compiler prevent undefined behavior?
The C++ language prioritizes performance and flexibility. Many forms of undefined behavior cannot be detected efficiently at compile time, and avoiding mandatory runtime checks allows compilers to generate faster code.
Is undefined behavior the same as a segmentation fault?
No. A segmentation fault is one possible consequence of undefined behavior, but undefined behavior may also produce incorrect output, appear to work, or behave differently on another system.
How can I detect undefined behavior?
You can use compiler warnings, static analysis tools, AddressSanitizer (ASan), UndefinedBehaviorSanitizer (UBSan), and debuggers such as GDB to detect many forms of undefined behavior.
Can undefined behavior change between compiler versions?
Yes. Since the C++ standard does not define the result, different compilers, optimization levels, architectures, or compiler versions may produce different behavior for the same program.