C++

What made i i 1 legal in C17

27 September 2026 · 9 min read

What made i  i  1 legal in C17

The world of C++ programming is constantly evolving, bringing with it both powerful new features and subtle changes to existing rules. One of the most talked-about changes in C++17 involved the relaxation of sequencing rules, which famously impacted expressions that were once considered quintessential examples of undefined behavior. Specifically, the seemingly innocent-looking statement i = i++ + 1;, a classic interview question and a source of countless bugs pre-C++17, suddenly became legal. This shift wasn’t a mere oversight; it was a deliberate and significant modification to how the C++ standard defines the order of operations and side effects within expressions. Understanding what made i = i++ + 1; legal in C++17 requires a deep dive into the nuances of sequencing, evaluation, and the crucial concept of “sequenced before” relationships that govern C++ code execution.

The Pre-C++17 Nightmare: Undefined Behavior

Before C++17, the expression i = i++ + 1; was a prime example of undefined behavior. This wasn’t due to a lack of clarity in the standard itself, but rather the strict rules surrounding “sequence points” and the modification of objects. In earlier C++ standards, if an object was modified more than once, or if it was modified and its value was accessed in an unsequenced operation within the same expression, the behavior was undefined. This meant compilers were free to do anything, from producing unexpected results to crashing the program entirely.

The problem with i = i++ + 1; lies in its multiple interactions with the variable i. Firstly, i is modified by the post-increment operator i++. Secondly, its value is accessed as an operand for the addition + 1. Thirdly, i is modified again by the assignment operator =. In C++ standards prior to C++17, the order in which these modifications and accesses happened relative to each other was not guaranteed, leading to unsequenced operations on the same variable. This ambiguity was a common pitfall for developers, leading to non-portable and unreliable code.

For instance, one compiler might evaluate i++, store its original value, then perform the addition, and finally assign the result. Another might perform the assignment first, then the increment, leading to entirely different outcomes. This lack of a defined order for side effects and value computations within a single expression was the root cause of the undefined behavior, making such code inherently dangerous and unpredictable. As expert cppreference explains, “If a side effect on a scalar object is unsequenced relative to another side effect on the same scalar object, the behavior is undefined.”

C++17’s Game Changer: Relaxed Sequencing Rules

C++17 introduced significant revisions to the rules governing expression evaluation, effectively replacing the concept of “sequence points” with a more granular and powerful “sequenced before” relationship. This change fundamentally altered how compilers interpret expressions like i = i++ + 1;. Instead of relying on specific, discrete sequence points, the C++17 standard defines a partial order of evaluations within an expression, clarifying when one operation is guaranteed to complete before another begins.

Under C++17, the key change is that the value computation of the right-hand side of an assignment, and the side effects of that computation, are now sequenced before the actual assignment to the left-hand side. This crucial redefinition ensures that all operations within the right-hand expression (i++ + 1 in our case) are completed, and their side effects (like the increment of i by i++) are fully realized, before the result is assigned back to i on the left-hand side. This deterministic ordering eliminates the ambiguity that previously caused undefined behavior, making the expression well-defined.

This subtle but powerful modification to the C++ standard resolves many long-standing issues with expressions involving multiple modifications to the same variable. It means that the value of i used in the calculation i++ + 1, and the increment of i by i++, are guaranteed to occur before the final assignment to i. This clarity allows developers to reason about such expressions with confidence, though caution is still advised for readability. For more detailed insights into the C++17 sequencing rules, you can refer to the official ISO C++ standard documentation.

Infographic here: A visual representation illustrating the pre-C++17 sequence points vs. C++17 "sequenced before" relationships for `i = i++ + 1;` would go here.
Deconstructing `i = i++ + 1;` in C++17 --------------------------------------

To fully grasp why i = i++ + 1; is now legal, let’s break down its evaluation step-by-step according to C++17’s sequencing rules. Assume i initially holds the value 5. The right-hand side of the assignment, i++ + 1, is evaluated first. Within this subexpression, the post-increment operator i++ takes the current value of i (which is 5) for its own evaluation, but also schedules a side effect: incrementing i to 6. This increment is guaranteed to happen before the final assignment to the left-hand side.

Here’s the detailed breakdown of the evaluation order in C++17:

  1. The subexpression i++ is evaluated. Its value computation yields the original value of i (e.g., 5).
  2. A side effect of i++ is scheduled: i is incremented (e.g., i becomes 6). This side effect is guaranteed to be sequenced before the final assignment to i.
  3. The value 1 is evaluated.
  4. The addition + takes its operands: the value of i++ (which is 5) and 1.
  5. The result of the addition (5 + 1 = 6) is computed.
  6. Finally, the assignment operator = takes this computed value (6) and assigns it to the left-hand side variable, i. At this point, <b>Question & Answer : </b><br></br><p>Before you start yelling undefined behaviour, this is <em>explicitly</em> listed in <a href="https://timsong-cpp.github.io/cppwp/n4659/intro.execution#17" rel="noreferrer">N4659 (C++17)</a></p> <pre> i = i++ + 1; // the value of i is incremented </pre> <p>Yet in <a href="https://timsong-cpp.github.io/cppwp/n3337/intro.execution#15" rel="noreferrer">N3337 (C++11)</a></p> <pre> i = i++ + 1; // the behavior is undefined </pre> <p>What changed?</p> <p>From what I can gather, from <a href="https://timsong-cpp.github.io/cppwp/n4659/intro.execution#17" rel="noreferrer">[N4659 basic.exec]</a></p> <blockquote> <p>Except where noted, evaluations of operands of individual operators and of subexpressions of individual expressions are unsequenced. [...] The value computations of the operands of an operator are sequenced before the value computation of the result of the operator. If a side effect on a memory location is unsequenced relative to either another side effect on the same memory location or a value computation using the value of any object in the same memory location, and they are not potentially concurrent, the behavior is undefined.</p> </blockquote> <p>Where <em>value</em> is defined at <a href="https://timsong-cpp.github.io/cppwp/n4659/basic.types#4" rel="noreferrer">[N4659 basic.type]</a></p> <blockquote> <p>For trivially copyable types, the value representation is a set of bits in the object representation that determines a <em>value</em>, which is one discrete element of an implementation-defined set of values</p> </blockquote> <p>From <a href="https://timsong-cpp.github.io/cppwp/n3337/intro.execution#15" rel="noreferrer">[N3337 basic.exec]</a></p> <blockquote> <p>Except where noted, evaluations of operands of individual operators and of subexpressions of individual expressions are unsequenced. [...] The value computations of the operands of an operator are sequenced before the value computation of the result of the operator. If a side effect on a scalar object is unsequenced relative to either another side effect on the same scalar object or a value computation using the value of the same scalar object, the behavior is undefined.</p> </blockquote> <p>Likewise, value is defined at <a href="https://timsong-cpp.github.io/cppwp/n3337/basic.types#4" rel="noreferrer">[N3337 basic.type]</a></p> <blockquote> <p>For trivially copyable types, the value representation is a set of bits in the object representation that determines a <em>value</em>, which is one discrete element of an implementation-defined set of values.</p> </blockquote> <p>They are identical except mention of concurrency which doesn't matter, and with the usage of <em>memory location</em> instead of <em>scalar object</em>, where</p> <blockquote> <p>Arithmetic types, enumeration types, pointer types, pointer to member types, std::nullptr_t, and cv-qualified versions of these types are collectively called scalar types.</p> </blockquote> <p>Which doesn't affect the example.</p> <p>From <a href="https://timsong-cpp.github.io/cppwp/n4659/expr.ass#1" rel="noreferrer">[N4659 expr.ass]</a></p> <blockquote> <p>The assignment operator (=) and the compound assignment operators all group right-to-left. All require a modifiable lvalue as their left operand and return an lvalue referring to the left operand. The result in all cases is a bit-field if the left operand is a bit-field. In all cases, the assignment is sequenced after the value computation of the right and left operands, and before the value computation of the assignment expression. The right operand is sequenced before the left operand.</p> </blockquote> <p>From <a href="https://timsong-cpp.github.io/cppwp/n3337/expr.ass#1" rel="noreferrer">[N3337 expr.ass]</a></p> <blockquote> <p>The assignment operator (=) and the compound assignment operators all group right-to-left. All require a modifiable lvalue as their left operand and return an lvalue referring to the left operand. The result in all cases is a bit-field if the left operand is a bit-field. In all cases, the assignment is sequenced after the value computation of the right and left operands, and before the value computation of the assignment expression.</p> </blockquote> <p>The only difference being the last sentence being absent in N3337.</p> <p>The last sentence however, shouldn't have any importance as the left operand i is neither <em>"another side effect"</em> nor <em>"using the value of the same scalar object"</em> as the <em>id-expression</em> is a lvalue.</p><br></br><p>In C++11 the act of "assignment", i.e. the side-effect of modifying the LHS, is sequenced after the <em>value computation</em> of the right operand. Note that this is a relatively "weak" guarantee: it produces sequencing only with relation to <em>value computation</em> of the RHS. It says nothing about the <em>side-effects</em> that might be present in the RHS, since occurrence of side-effects is not part of <em>value computation</em>. The requirements of C++11 establish no relative sequencing between the act of assignment and any side-effects of the RHS. This is what creates the potential for UB. </p> <p>The only hope in this case is any additional guarantees made by specific operators used in RHS. If the RHS used a prefix ++, sequencing properties specific to the prefix form of ++ would have saved the day in this example. But postfix ++ is a different story: it does not make such guarantees. In C++11 the side-effects of = and postfix ++ end up unsequenced with relation to each other in this example. And that is UB.</p> <p>In C++17 an extra sentence is added to the specification of assignment operator: </p> <blockquote> <p>The right operand is sequenced before the left operand.</p> </blockquote> <p>In combination with the above it makes for a very strong guarantee. It sequences <em>everything</em> that happens in the RHS (including any side-effects) before <em>everything</em> that happens in the LHS. Since the actual assignment is sequenced <em>after</em> LHS (and RHS), that extra sequencing completely isolates the act of assignment from any side-effects present in RHS. This stronger sequencing is what eliminates the above UB.</p> <p>(Updated to take into account @John Bollinger's comments.)</p>