The C++ standard describes the behaviour of an abstract machine. A compiler may generate any code that keeps the observable behaviour of that machine, a principle known as the as-if rule [intro.abstract]. Where the standard imposes no requirements, a program has no defined meaning at all.
Terminology
| Term | Meaning | Section |
|---|---|---|
| Ill-formed | Breaks a rule of the language. A compiler must issue a diagnostic | [defns.ill.formed] |
| Ill-formed, no diagnostic required | Breaks a rule that compilers need not detect, such as the one-definition rule across translation units | [intro.compliance] |
| Implementation-defined | The compiler chooses, and must document it: for example, the size of int | [defns.impl.defined] |
| Unspecified | One of several outcomes, not necessarily documented: for example, the order in which arguments are evaluated | [defns.unspecified] |
| Undefined | The standard imposes no requirements | [defns.undefined] |
A program with undefined behaviour has no requirements placed on it, even for operations before the undefined one [intro.abstract]. Compilers use this to optimise: they may assume it never happens. Undefined behaviour during constant evaluation is a compile-time error, since such an expression is not a constant expression [expr.const].
Common undefined behaviour
| Action | Section |
|---|---|
| Signed integer overflow | [expr.pre] |
| Division or remainder by zero | [expr.mul] |
| Shifting by a negative count, or by at least the operand’s width | [expr.shift] |
| Dereferencing a null or dangling pointer, or reading past the end of an array | [expr.add], [basic.life] |
| Reading an uninitialised object, with narrow exceptions | [basic.indet] |
| Two unsequenced side effects on one object | [intro.execution] |
Modifying an object defined const | [dcl.type.cv] |
Flowing off the end of a non-void function other than main | [stmt.return] |
| Deleting a derived object through a base pointer with a non-virtual destructor | [expr.delete] |
Mismatching new with delete[], or the reverse | [expr.delete] |
| Accessing an object through a type that breaks the aliasing rule | [basic.lval] |
| Any data race | [intro.races] |
std::unreachable() (C++23) and
[[assume(expr)]]
(C++23) deliberately invoke undefined behaviour, to tell the optimiser something. Sanitisers such as
GCC’s and Clang’s -fsanitize=address,undefined and -fsanitize=thread find many of these at
run time.
Object lifetime
An object’s lifetime begins when suitable storage is obtained and its initialisation is complete. It ends when its destructor call begins, or, for a type with a trivial destructor, when its storage is released or reused [basic.life].
Storage may outlast the object, or be obtained before it. Between these points, only limited use of a pointer to the storage is allowed. After the end of its lifetime, using an object through a pointer or reference is undefined behaviour.
const std::string& bad()
{
std::string local = "gone";
return local; // dangling reference: local dies at the return
}
int* p = new int(1);
delete p;
*p = 2; // undefined: use after free
std::vector<int> v{1, 2, 3};
auto it = v.begin();
v.push_back(4); // may reallocate
*it = 0; // undefined if it did
std::launder (C++17), in <new>, gives a usable pointer to an object
created in storage that held another object of the same type, in the cases where the old pointer
may not be used directly [ptr.launder].
Implicit-lifetime types
Since C++20, some operations implicitly begin the lifetime of objects of
implicit-lifetime types in the storage they produce [intro.object]. These operations
include operator new, std::malloc, std::memcpy and starting the
lifetime of an array of unsigned char or std::byte. Implicit-lifetime
types are scalars, arrays, aggregates without a user-provided destructor, and classes with a
trivial eligible constructor and a trivial, non-deleted destructor [class.prop].
struct Point { int x, y; };
auto* p = static_cast<Point*>(std::malloc(sizeof(Point)));
p->x = 1; // fine since C++20
alignas(Point) std::byte buffer[sizeof(Point)];
auto* q = std::start_lifetime_as<Point>(buffer); // C++23: keeps the bytes
Strict aliasing
An object may be accessed only through a type similar to one of these [basic.lval]:
- The object’s dynamic type.
- The signed or unsigned type corresponding to its dynamic type.
char,unsigned charorstd::byte.
Two types are similar if they differ only in const and volatile
at any level [conv.qual]. Anything else is undefined behaviour, which lets the compiler assume
that a pointer to float and a pointer to int do not point to the
same object.
float f = 1.0f;
int a = *reinterpret_cast<int*>(&f); // undefined: int is not similar to float
int b;
std::memcpy(&b, &f, sizeof b); // fine: copies the object representation
auto c = std::bit_cast<int>(f); // fine (C++20)
The memory model
A memory location is one object of scalar type, or one maximal run of adjacent bit-fields of non-zero width [intro.memory]. Two threads may update different memory locations without interfering. Since C++11 the language has a defined model for how threads interact [intro.multithread].
| Relation | Meaning |
|---|---|
| Sequenced before | Within one thread, the order given by the evaluation rules |
| Synchronizes with | An operation in one thread makes effects visible to another: unlocking and then locking a mutex, or a release store read by an acquire load |
| Happens before | Combines sequenced-before and synchronizes-with. It decides which writes a read may see |
Data races
Two evaluations conflict if one modifies a memory location and the other reads or modifies the same one. A program has a data race if it has two conflicting actions in different threads, at least one not atomic, and neither happening before the other. Any data race is undefined behaviour [intro.races].
int counter = 0;
std::mutex m;
void racy() { ++counter; } // undefined if run by two threads
void safe() { std::lock_guard g(m); ++counter; }
std::atomic<int> shared{0};
void atomic_inc() { ++shared; } // defined
volatile does not make access safe between threads. It concerns the
relation of a program with hardware, and adds neither atomicity nor ordering.
Atomic operations
std::atomic<T> in <atomic> gives operations that do not race.
Each operation takes a std::memory_order, which sets how much ordering it imposes
[atomics.order].
| Order | Guarantees |
|---|---|
relaxed | Atomicity only; no ordering with other operations |
consume | Ordering of operations dependent on the loaded value. Compilers treat it as acquire |
acquire | On a load: later operations in this thread cannot move before it |
release | On a store: earlier operations in this thread cannot move after it |
acq_rel | Both, for read-modify-write operations |
seq_cst | The default: acquire and release, plus one total order across all such operations |
Forward progress
An implementation may assume that every thread eventually does one of the following
[intro.progress]: terminates, calls a library I/O function, accesses a
volatile object, or performs an atomic or synchronisation operation. A loop that does none
of these can therefore be assumed to finish, and a compiler may remove it.