The Debugger's Secret Weapon: What C++ Programmers Overlook
\n\nHave you ever written something like i = i++ + ++i; and gotten different results on different compilers? Or caught a strange bug that only reproduced on the server but not on your laptop? Meet undefined behavior — one of the most insidious enemies of a C++ developer. And at the center of this chaos are sequence points.
In this article, we'll break down what sequence points are, how they relate to the order of evaluation in C++, and — most importantly — how to avoid stepping on these rakes. The material is based on a classic Stack Overflow question that has garnered thousands of views. Let's go!
\n\nWhat are sequence points?
\n\nImagine you are an orchestra conductor. Each musician (processor instruction) must enter at a strictly defined moment. A sequence point is a point in time when all previous "side effects" (variable changes, function calls) are guaranteed to be complete, and the next ones have not yet begun.
\n\nIn the C++ 98/03 standard, the term sequence point was used. In C++11, it was replaced with the stricter "sequenced before / sequenced after" relationships, but the essence remains: between two sequence points, the compiler has full right to reorder operations, as long as it doesn't change the result (according to the "as-if" rule).
\n\nSimply put: if you try to modify the same variable twice between two sequence points — you get undefined behavior. The compiler can do anything: from a correct result to formatting your hard drive (joking, but almost true).
\n\nClassic examples of sequence points
\n- \n
- The semicolon
;— end of an expression \n - The comma
,as an operator (but not in function arguments!) \n - The end of each expression in a
forloop (after initialization, condition, and increment) \n - Logical operators
&&and||\n - The ternary operator
?:\n - A function call (before the function body begins execution) \n
How do sequence points cause undefined behavior?
\n\nHere is a classic example that every C++ developer should know:
\n\nint i = 0;\ni = i++ + ++i; // UB! Undefined behavior\n\nWhy is this UB? Because between two sequence points (in this case, between the beginning and end of the expression), the variable i is modified twice:
- \n
- The postfix increment
i++\n - The prefix increment
++i\n - The assignment
i = ...\n
The compiler doesn't know in which order to perform these operations. The result? One compiler will output 2, another — 3, a third — 0. And in optimization mode, it might even discard this code entirely.
\n\nAnother tricky case: function arguments
\n\nint foo(int a, int b) { return a + b; }\n\nint x = 5;\nint result = foo(++x, ++x); // UB!\n\nHere, the comma between the arguments is not a comma operator, but a separator. There is no sequence point between the evaluation of the ++x arguments! Both increments happen "in parallel" (from the standard's perspective), and we again get undefined behavior.
Practical examples: what UB looks like in real code
\n\nExample 1: A non-obvious bug with indexing
\n\nint arr[5] = {0, 10, 20, 30, 40};\nint idx = 1;\narr[idx] = idx++; // UB!\n\nAt first glance, it seems simple: we want to write 1 into arr[1], and then increment idx. But the standard does not guarantee the order of evaluating the index idx and the right-hand side idx++. The result? It might write to arr[1], or it might write to arr[2].