Unsequenced Modifications in C++ Undefined Behavior

This article provides a comprehensive overview of undefined behavior resulting from multiple unsequenced modifications to the same variable in C++. It explains the concepts of sequencing and evaluation order, analyzes why the C++ standard designates these operations as undefined behavior, highlights classic code patterns that trigger the issue, and details how modern C++ standards regulate expression evaluation to prevent subtle runtime bugs.

Sequencing and Evaluation Order in C++

In C++, the execution of an expression involves both value computations (determining the result of an expression) and side effects (modifications to the state of an object, such as writing to memory). Prior to C++11, the language relied on the concept of "sequence points" to define boundaries where all previous side effects were guaranteed to be complete. Starting with C++11, the standard formalized this into a partial ordering model called the sequenced-before relationship.

An evaluation can be:

  • Sequenced before another evaluation: The first completes before the second begins.
  • Sequenced after another evaluation: The reverse of sequenced before.
  • Unsequenced relative to another: The evaluations can occur in any order, can overlap, or can be interleaved.
  • Indeterminately sequenced: One happens before the other, but the compiler chooses which one runs first; they cannot overlap.

The Undefined Behavior Rule

The C++ standard specifies that:

  1. If a side effect on a scalar object is unsequenced relative to another side effect on the same scalar object, the behavior is undefined.
  2. If a side effect on a scalar object is unsequenced relative to a value computation that uses the value of the same scalar object, the behavior is undefined.

When behavior is undefined (UB), the compiler is not obligated to generate predictable machine instructions, issue a warning, or fail to compile. The resulting program may produce erratic output, crash unpredictably, or behave differently across different optimization levels and compiler versions.

Common Examples of Unsequenced Modifications

A classic demonstration of this violation occurs when applying increment or decrement operators multiple times to a scalar variable within a single expression:

int i = 0;
i = ++i; // Undefined behavior prior to C++17

In the assignment i = ++i, ++i modifies i, and the assignment itself attempts to write a new value to i. Without an explicit sequencing rule between the assignment's side effect and the pre-increment's side effect, the order of writes is unresolved.

Another prevalent scenario involves function arguments:

void execute(int a, int b);

int x = 1;
execute(++x, ++x); // Undefined behavior

In C++, function arguments are unsequenced (or indeterminately sequenced since C++17) relative to one another. Because both subexpressions modify x without an intervening sequence point or sequenced-before relationship, modifying x twice leads directly to undefined behavior.

A related violation involves reading and modifying the same variable:

int arr[5] = {0};
int index = 0;
arr[index] = index++; // Undefined behavior prior to C++17

Here, index is being read for the array subscript while simultaneously undergoing a side effect via index++.

The Impact of C++17

C++17 refined the rules governing expression evaluation to eliminate some of the ambiguities that historically caused accidental undefined behavior:

  • Assignment expressions (=, +=, etc.): The right-hand side operand is sequenced before the left-hand side operand.
  • Subscript expressions (E1[E2]): The expression E1 is sequenced before E2.
  • Shift operators (<<, >>): The left operand is sequenced before the right operand.

Because of these revisions, an expression like arr[i] = i++ or i = ++i has well-defined sequencing in C++17 and later. However, expressions like execute(++x, ++x) remain non-deterministic or problematic, and multiple unsequenced writes inside subexpressions where no order is mandated remain strictly undefined.

Why Compilers Treat This as Undefined

Modern compilers heavily optimize code using pipelining, instruction reordering, and register allocation. If the standard mandated an arbitrary evaluation order for subexpressions, compilers would be forced to emit redundant store and reload instructions, severely degrading performance. By classifying unsequenced modifications as undefined behavior, the language allows the optimizer to assume that such conflicting operations never occur in a well-formed program.

How to Prevent Unsequenced Modifications

To avoid undefined behavior from unsequenced modifications:

  • Avoid modifying a variable more than once within the same full expression.
  • Do not read and modify the same variable in different operands of an operator unless a strict sequencing guarantee applies.
  • Separate complex, side-effect-heavy expressions into multiple distinct statements.
  • Enable comprehensive compiler warnings (such as -Wall and -Wextra in GCC and Clang), which routinely detect and flag unsequenced operations.