Why i = i++ Doesn't Increment i: Undefined Behavior Explained
Learn why i = i++ doesn't increment the variable i due to undefined behavior in C/C++. Understand post-increment temporary values and side-effects.
Why doesn’t i = i++; increment the variable i?
i = i++
I understand that the post-increment operation uses a temporary value and isn’t atomic, but why isn’t the incremented value ultimately assigned back to the variable?
You’ve run into one of the trickiest gotchas in C and C++: i = i++ often leaves i unchanged, even though post-increment should bump it up. That’s because the post-increment operator returns the original value of i as a temporary, which gets assigned back, overwriting any side-effect from the increment—and the whole thing triggers undefined behavior due to unsequenced modifications to the same variable. Compilers have total freedom here, so results can flip between no change, double increments, or worse, depending on optimization and standards version.
Contents
- What Happens in
i = i++? - Why
i = i++Causes Undefined Behavior in C/C++ - The Role of Post-Increment: Temporary Value and Side-Effect
- Possible Execution Sequences and Compiler Freedom
- Changes in C++17: From UB to Defined Behavior
- Comparisons: Java, C#, and Other Languages
- Best Practices: How to Avoid
i = i++Pitfalls - Sources
- Conclusion
What Happens in i = i++?
Picture this: int i = 0; i = i++;. You expect i to end up as 1, right? Post-increment does increment i, but not in the way intuition suggests. The i++ part grabs i’s current value (0), queues up an increment as a side-effect, and hands back that original 0 for the assignment. Then i = 0 slams it right back to zero.
Why the overwrite? Post-increment isn’t atomic—it’s two jobs: return old value, then increment later. In cppreference’s breakdown, they show it plain: for i=0; i=i++;, i stays 0 in modern C++. But pre-C++17? Total chaos.
Ever coded this in a loop or function? It bites when you’re rushing a counter. Compilers like GCC or Clang might warn you (-Wall flags it as suspicious), but they won’t always save you from the mess.
Postfix vs. Prefix: Quick Reality Check
Don’t mix up i++ and ++i. Prefix (++i) increments first, then returns the new value. So i = ++i; reliably doubles it (to 2 here). Postfix defers the increment, prioritizing the old value for the expression. Simple, yet deadly in compounds like this.
Why i = i++ Causes Undefined Behavior in C/C++
Here’s the killer: C and C++ standards call this undefined behavior (UB) in most cases. Why? You’re modifying i twice without a sequence point (pre-C++11) or proper “sequenced before” relation (C++11+).
From the Stack Overflow deep dive on increment UB, C99 §6.5¶2 nails it: “Between the previous and next sequence point an object shall have its stored value modified at most once by the evaluation of an expression. Furthermore, the prior value shall be read only to determine the value to be stored.”
Translation: Assignment modifies i. Post-increment modifies i again (side-effect). No guaranteed order? UB. Your program can crash, corrupt memory, or (ironically) “work” on one compiler but fail on another.
C++11 formalized this with [expr.seq]/4: Multiple modifications to the same scalar without sequencing = UB. Debug builds might preserve order; release optimizations? Forget it—i could end as 0, 1, or 42.
But wait—does it always fail? Nope. That’s UB’s fun part: nasal demons await, but often it “just doesn’t increment” due to common evaluation patterns.
The Role of Post-Increment: Temporary Value and Side-Effect
Post-increment i++ is sneaky. It creates a temporary holding i’s original value, schedules i += 1 as a side-effect (happening “after” the value is used), and yields the temp for the expression.
In i = i++;:
- Evaluate right-hand side:
i++→ temp = oldi(say 5), schedulei = 6. - But assignment
i = temp(5) runs, clobbering the scheduled increment.
Or does it? Sequencing decides. No guarantee the side-effect applies before assignment overwrites. This Stack Overflow thread unpacks it: The increment timing is flexible—compiler reorders freely if UB.
Think assembly: Load i to register (temp), inc i in memory, store temp back. Or store first? UB lets compilers pick. Result: Original value wins, side-effect lost.
User-defined operators? They might behave (if you sequence manually), but stick to builtins—safer.
Possible Execution Sequences and Compiler Freedom
Compilers explore paths like these (pseudocode from SO discussion and cplusplus forum):
Sequence A (common, no increment):
temp = i; // temp=5
i = temp; // i=5 (overwrites any pending inc)
apply side-effect: i++ // but i already reset? Nah, sequenced wrong → i=5
Sequence B (double inc):
temp = i; // temp=5
apply i++: i=6
i = temp; // i=5 → back to 5? Or i=6 if inc after.
Wild UB variants:
- Inc twice: i=7
- No inc: i=5
- i=3 (?? optimizer magic)
GCC 4.x might give 1; Clang 14 gives 0. Test it: int i=5; printf("%d\n", i=i++);. Flip flags (-O0 vs -O3), watch it change. Freedom = frustration.
Rhetorical question: Why risk this when i++; is three characters safer?
Changes in C++17: From UB to Defined Behavior
Good news! C++17 tightened rules via [stmt.expr]/10 in the standard. Assignment now sequences: right operand fully sequenced before left-hand side modification.
Per cppreference: In i = i++;, right i++ (temp=old i, inc i) completes before left i = temp. But temp is old value, so:
i++: temp = i (5), i becomes 6 (side-effect).- Then
i = 5→ i=5.
Defined: i unchanged. No UB! Pre-C++17? Still gamble.
C++20+ inherits this. Check SO on C++17 evolution: Now predictable across compilers.
Caveat: C stays UB forever (no equivalent change). Mixed codebase? Pain.
Comparisons: Java, C#, and Other Languages
Java/C# sidestep this trap—strict left-to-right evaluation.
In C# (from SO example):
int i = 0;
i = i++; // Evaluates: target=i, rhs=i++ (temp=0, i=1), assign temp→i=0
Defined: i=0. Java mirrors: postfix returns copy, incs original, assign copy.
Python? i = i + 1 only— no operator tricks.
Rust: Borrow checker prevents unsequenced mods outright.
Lesson? C/C++'s flexibility enables speed but demands care. Others prioritize sanity.
Best Practices: How to Avoid i = i++ Pitfalls
Steer clear entirely. Use:
++i;ori += 1;— clear, no UB.i = i + 1;— explicit.- Functions:
void increment(int& i) { i++; }
Tools help:
- Clang/GCC:
-Wsequence-point,-fsanitize=undefined. - Static analyzers: Coverity flags it.
- Modern C++:
std::exchange(i, i+1)for swaps, but simple incs suffice.
In loops? for (int i=0; i<10; i++)— postfix fine there (value used before side-effect). Context matters.
Debug tip: Godbolt.org—paste code, see assembly divergences.
Sources
- Why are these constructs (using pre and post increment) undefined behavior? — Explains C/C++ standards on unsequenced side-effects: https://stackoverflow.com/questions/949433/why-are-these-constructs-using-pre-and-post-increment-undefined-behavior
- Is the behaviour of i=i++ really undefined? — Details execution sequences and C++17 changes: https://stackoverflow.com/questions/4968854/is-the-behaviour-of-i-i-really-undefined
- Increment and decrement operators — C++ post-increment semantics and C++17 assignment sequencing: https://en.cppreference.com/w/cpp/language/operator_incdec.html
- i=i++ doesn’t increment i. Why? — Step-by-step C# evaluation contrasting C++ UB: https://stackoverflow.com/questions/6716189/i-i-doesnt-increment-i-why
- i = i++ behaviour — Forum discussion of possible outcomes: https://cplusplus.com/forum/beginner/135021/
- What is the difference between i++ and ++i? — Post-increment temporary value mechanics: https://stackoverflow.com/questions/24853/what-is-the-difference-between-i-and-i
Conclusion
The post-increment in i = i++ hands back a temporary original value that assignment dutifully stores, often nuking the side-effect amid undefined behavior from unsequenced mods—though C++17 pins it to “unchanged.” Languages like Java/C# define it predictably as no-op too, but always dodge this idiom. Stick to i++ standalone or i += 1;, crank warnings, and test across compilers—you’ll sleep better.