Undefined behaviour (UB) means the standard places no requirements on what happens. The program may crash, work by accident, or be “optimised” into something surprising. Know the usual suspects.
The usual suspects
| UB | Example | Tool that catches it |
|---|---|---|
| Out-of-bounds access | v[v.size()], arr[10] on int arr[10] | ASan, .at(), -D_GLIBCXX_ASSERTIONS |
| Use after free / dangling | Reference into a vector that grew, returning &local | ASan |
| Uninitialised read | int x; if (x) … | -Wall (sometimes), MSan, Valgrind |
| Signed integer overflow | INT_MAX + 1 | UBSan |
| Null dereference | *p with p == nullptr | Crash / UBSan |
| Data race | Two threads write a non-atomic variable | TSan |
| Shift out of range | 1 << 32 on a 32-bit int, negative shift | UBSan |
| Strict-aliasing violation | *(int*)&someFloat | -fstrict-aliasing warnings; use memcpy/bit_cast |
| Modifying a const object | const_cast then write | — |
Missing return in a non-void function | Falling off the end | -Wreturn-type (error in clang) |
Sanitizers find UB for you
#include <iostream>
#include <vector>
int main() {
std::vector<int> v(3);
int* data = v.data();
std::cout << data[3] << '\n'; // one past the end → AddressSanitizer: heap-buffer-overflow
}#include <iostream>
int main(int argc, char**) {
int shift = 31 + argc; // 32 at run time
std::cout << (1 << shift) << '\n'; // UBSan: shift exponent 32 is too large for 32-bit type 'int'
}$ clang++ -std=c++20 -g -fsanitize=address,undefined main.cpp && ./a.out # memory errors + UB
$ clang++ -std=c++20 -g -fsanitize=thread main.cpp && ./a.out # data racesClassic pitfalls (not all UB, all bugs)
#include <iostream>
#include <string>
#include <vector>
int main() {
// 1. Integer division before conversion
double avg = 7 / 2; // 3, not 3.5
std::cout << avg << '\n';
// 2. Assignment instead of comparison (clang warns; parentheses silence it)
int x = 0;
if ((x = 5)) std::cout << "always true\n";
// 3. Unsigned wrap-around
std::vector<int> empty;
std::cout << (empty.size() - 1 > 0) << '\n'; // true: 18446744073709551615 > 0
// 4. Copy in range-for
std::vector<std::string> names{"a", "b"};
for (auto n : names) n += "!"; // modifies copies
std::cout << names[0] << '\n';
// 5. Char arithmetic
std::string s = "abc";
std::cout << s[0] + s[1] << '\n'; // 195 — chars promote to int
}3
always true
1
a
195What's the difference between undefined, unspecified and implementation-defined behaviour?
Why doesn't C++ just check array bounds?