As-If Rule
The compiler may transform code in any way that preserves the observable behavior of the abstract machine, enabling aggressive optimization.
As-If Rulesince C++98A conforming C++ implementation may transform, reorder, or eliminate any code as long as the observable behavior of the resulting program is identical to that of the abstract machine executing the original source.
Overview
The C++ standard does not specify machine code β it specifies the behavior of an abstract machine. The as-if rule, stated at [intro.abstract] in the standard, is the license that frees compilers from literal execution of that abstract machine: any transformation is permissible provided the program's observable behavior remains intact.
This is not a narrow permission. It is the foundation on which every modern C++ optimizer stands. Dead-store elimination, loop unrolling, inlining, constant folding, instruction reordering, common subexpression elimination β all are justified by the as-if rule. A compiler that faithfully executed every source statement in written order would be nearly useless for performance-critical code.
What Counts as Observable
The standard defines observable behavior with precision. Only three categories of effects are protected:
- Volatile accesses. Reads from and writes to
volatile-qualified objects must occur in the order specified by the source, with no additions, elisions, or reorderings across sequence points. - I/O and interactive devices. Prompting text sent to interactive output must be flushed before the program waits for input on those same devices.
- Floating-point environment (when
#pragma FENV_ACCESS ONis active). Changes to floating-point exception flags and rounding modes must be visible to subsequent floating-point operations as if no optimization had occurred. The count and order of individual exceptions may still change, as long as the state observed by the next floating-point operation is correct.
Everything else β register contents, stack layout, heap fragmentation, the number of constructor invocations on temporaries, the order of intermediate arithmetic, instruction count β belongs entirely to the implementation.
Examples
Dead Store and Redundant Computation Elimination
volatile int sink;
int expensive(); // external, unknown side effects
void example() {
int scratch = expensive(); // may be called β has unknown effects
int local = 42 * 7; // C++98: no observable effect, folded at compile time
local = 0; // dead store: local never read after this point
sink = 1; // volatile write β MUST occur
}The write to local after it is last read is removed entirely. The write to sink is mandatory. The call to expensive() is retained because the compiler cannot prove it has no observable effects (I/O, volatile access, etc.).
Constant Folding Across Call Boundaries
// C++98: inline gives the compiler latitude to substitute
inline int square(int x) { return x * x; }
int g = square(7); // 49 appears directly in the binary; the call may not existSince C++11, constexpr formalises this β evaluation at compile time is guaranteed for constant arguments:
constexpr int cube(int x) { return x * x * x; } // C++11
static_assert(cube(3) == 27, ""); // C++11: evaluated entirely at compile timeReordering and Fusion
// volatile input prevents the compiler folding the entire program away
volatile int input = 7;
volatile int result;
int& preinc(int& n) { return ++n; }
int add(int a, int b) { return a + b; }
int main() {
int n = input;
// Note: ++n + ++n on built-in int is UB (unsequenced modification).
// Routing through named functions sequences the calls, making this well-defined.
int m = add(preinc(n), preinc(n)); // n incremented twice: 7β8β9; m = 8+9 = 17
result = m;
}A typical x86-64 compiler reduces this to three instructions: read input, compute 2 * input + 3, write result. Both calls and both increments disappear. Observable behavior β the final volatile write β is preserved.
The Copy Elision Exception
Copy elision (RVO and NRVO) is an explicit, named exception to the as-if rule. Compilers are permitted to elide copy and move constructor calls β along with the corresponding destructor calls β even when those constructors have observable side effects.
struct Tracer {
Tracer() { std::puts("default"); }
Tracer(const Tracer&) { std::puts("copy"); }
Tracer(Tracer&&) { std::puts("move"); } // C++11
~Tracer() { std::puts("~Tracer"); }
};
Tracer make() {
return Tracer{};
}
int main() {
Tracer t = make();
// C++17 guaranteed copy elision: prints only "default" and "~Tracer".
// Pre-C++17: compilers were permitted (not required) to elide the moves.
}C++17 made this mandatory for prvalues: in Tracer t = make(), no temporary object exists β the constructor is called directly into t. Prior standards permitted elision but could not guarantee it. Code that depends on the count of constructor calls for correctness (not diagnostics) was fragile before C++17 and should still be avoided after it.
Interaction with Undefined Behavior
The as-if rule's guarantee presupposes a well-formed program with defined behavior. When undefined behavior is present, the rule's precondition fails: the abstract machine has no single defined execution sequence to preserve, so the implementation may produce any output.
This has counterintuitive consequences in practice:
// Signed integer overflow is UB in all C++ standards.
bool will_overflow(int x) {
return x + 1 > x; // UB when x == INT_MAX
}A compiler may constant-fold this to return true;. For every defined value of int, x + 1 > x is mathematically certain β so the branch that was intended to detect overflow is silently deleted. Programs that appear correct at -O0 but fail at -O2 almost always contain UB that the as-if rule permits the optimizer to exploit.
// Correct overflow detection:
#include <climits>
bool will_overflow(int x) {
return x == INT_MAX;
}
// Or with C++20 standard library:
#include <numeric>
// std::add_sat, std::cmp_equal in <numeric>/<utility> for saturating/checked arithmeticMultithreading and the As-If Rule
The as-if rule applies per-thread. Within a single thread, the compiler may freely reorder memory operations provided that thread's observable behavior is unchanged. Across threads, the C++11 memory model ([atomics], [thread]) defines which operations establish synchronization boundaries that the as-if rule cannot cross.
#include <atomic> // C++11
std::atomic<bool> ready{false}; // C++11
int data = 0;
// Thread A:
void producer() {
data = 42;
ready.store(true, std::memory_order_release); // C++11
}
// Thread B:
void consumer() {
while (!ready.load(std::memory_order_acquire)) {} // C++11
assert(data == 42); // guaranteed: happens-before established
}The release/acquire pair prevents the compiler from hoisting the data = 42 write past the store to ready. Without the atomic, the as-if rule would permit that reordering, and the consumer could observe ready == true with data == 0.
Without synchronization, data races are themselves undefined behavior β meaning the as-if rule's guarantee disappears entirely, not just the ordering guarantee.
Common Pitfalls
Treating volatile as a threading primitive. Volatile guarantees ordered access within a single thread of execution. It makes no guarantee about visibility across threads. Use std::atomic<T> (C++11) for inter-thread communication.
Relying on constructor side effects of temporaries. Because copy elision is permitted (pre-C++17) or mandated (C++17 for prvalues), code whose correctness depends on a specific number of constructor or destructor invocations is unreliable. Diagnostics are fine; logic must not be.
Signed integer "wraparound" assumptions. Signed overflow is UB; the optimizer is entitled to assume it never happens. Use unsigned arithmetic where wraparound semantics are needed, or use compiler-specific builtins (__builtin_add_overflow) / C++26 std::add_overflow.
Expecting zeroed bytes after a move. A moved-from object is left in a valid but unspecified state (C++11). The implementation is not required to write zeros to the source; doing so would be a dead store in many cases, and the as-if rule allows it to be dropped.
See Also
reference/language/copy-elisionβ the named exception to the as-if rule; guaranteed prvalue elision in C++17reference/language/undefined-behaviorβ when the as-if guarantee disappears and the optimizer acts on impossible assumptionsreference/language/volatileβ the primary mechanism that creates observable-access obligationsreference/language/constant-expressionsβ constexpr and compile-time evaluation as a consequence of as-if folding