Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What made i = i++ + 1; legal in C++17?

People also ask

What is C17 language?

C17 is the informal name for ISO/IEC 9899:2018, the most recent standard for the C programming language, prepared in 2017 and published in June 2018. It replaced C11 (standard ISO/IEC 9899:2011). C17 will be superseded by C2x.

How many versions of C are there?

Let's continue with a discussion of all the five different standards of C — K&R C, ANSI C, C99, C11 and Embedded C. For the purposes of our discussion, the compiler used is the gcc C compiler from the GNU Compiler Collection (GCC).

What is the difference between C99 and C11?

C11 looked to address the issues of C99 and to more closely match the C++ standard, C++11. It changes some C99 features required to optional. Some of the features include variable length arrays and complex numbers. This makes it easier for compiler vendors to meet C11's required function set.

What is new in C11?

C11 eliminates these hacks by introducing two new datatypes with platform-independent widths: char16_t and char32_t for UTF-16 and UTF-32, respectively (UTF-8 encoding uses char, as before). C11 also provides u and U prefixes for Unicode strings, and the u8 prefix for UTF-8 encoded literals.


In C++11 the act of "assignment", i.e. the side-effect of modifying the LHS, is sequenced after the value computation of the right operand. Note that this is a relatively "weak" guarantee: it produces sequencing only with relation to value computation of the RHS. It says nothing about the side-effects that might be present in the RHS, since occurrence of side-effects is not part of value computation. 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.

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.

In C++17 an extra sentence is added to the specification of assignment operator:

The right operand is sequenced before the left operand.

In combination with the above it makes for a very strong guarantee. It sequences everything that happens in the RHS (including any side-effects) before everything that happens in the LHS. Since the actual assignment is sequenced after 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.

(Updated to take into account @John Bollinger's comments.)


You identified the new sentence

The right operand is sequenced before the left operand.

and you correctly identified that the evaluation of the left operand as an lvalue is irrelevant. However, sequenced before is specified to be a transitive relation. The complete right operand (including the post-increment) is therefore also sequenced before the assignment. In C++11, only the value computation of the right operand was sequenced before the assignment.


In older C++ standards and in C11, definition of the assignment operator text ends with the text:

The evaluations of the operands are unsequenced.

Meaning that side-effects in the operands are unsequenced and therefore definitely undefined behavior if they use the same variable.

This text was simply removed in C++11, leaving it somewhat ambiguous. Is it UB or is it not? This has been clarified in C++17 where they added:

The right operand is sequenced before the left operand.


As a side note, in even older standards, this was all made very clear, example from C99:

The order of evaluation of the operands is unspecified. If an attempt is made to modify the result of an assignment operator or to access it after the next sequence point, the behavior is undefined.

Basically, in C11/C++11, they messed up when they removed this text.


This is further information to the other answers and I'm posting it as the code below is often asked about as well.

The explanation in the other answers is correct and also applies to the following code which is now well-defined (and does not change the stored value of i):

i = i++;

The + 1 is a red herring and it's not really clear why the Standard used it in their examples, although I do recall people arguing on mailing lists prior to C++11 that maybe the + 1 made a difference due to forcing early lvalue conversion on the right-hand side. Certainly none of that applies in C++17 (and probably never applied in any version of C++).