The difference between C and C++ regarding the ++

2019-01-16 05:12发布

问题:

I have been fooling around with some code and saw something that I don't understand the "why" of.

int i = 6;
int j;

int *ptr = &i;
int *ptr1 = &j

j = i++;

//now j == 6 and i == 7. Straightforward.

What if you put the operator on the left side of the equals sign?

++ptr = ptr1;

is equivalent to

(ptr = ptr + 1) = ptr1; 

whereas

ptr++ = ptr1;

is equivalent to

ptr = ptr + 1 = ptr1;

The postfix runs a compilation error and I get it. You've got a constant "ptr + 1" on the left side of an assignment operator. Fair enough.

The prefix one compiles and WORKS in C++. Yes, I understand it's messy and you're dealing with unallocated memory, but it works and compiles. In C this does not compile, returning the same error as the postfix "lvalue required as left operand of assignment". This happens no matter how it's written, expanded out with two "=" operators or with the "++ptr" syntax.

What is the difference between how C handles such an assignment and how C++ handles it?

回答1:

In both C and C++, the result of x++ is an rvalue, so you can't assign to it.

In C, ++x is equivalent to x += 1 (C standard §6.5.3.1/p2; all C standard cites are to WG14 N1570). In C++, ++x is equivalent to x += 1 if x is not a bool (C++ standard §5.3.2 [expr.pre.incr]/p1; all C++ standard cites are to WG21 N3936).

In C, the result of an assignment expression is an rvalue (C standard §6.5.16/p3):

An assignment operator stores a value in the object designated by the left operand. An assignment expression has the value of the left operand after the assignment, but is not an lvalue.

Because it's not an lvalue, you can't assign to it: (C standard §6.5.16/p2 - note that this is a constraint)

An assignment operator shall have a modifiable lvalue as its left operand.

In C++, the result of an assignment expression is an lvalue (C++ standard §5.17 [expr.ass]/p1):

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.

So ++ptr = ptr1; is a diagnosable constraint violation in C, but does not violate any diagnosable rule in C++.

However, pre-C++11, ++ptr = ptr1; has undefined behavior, as it modifies ptr twice between two adjacent sequence points.

In C++11, the behavior of ++ptr = ptr1 becomes well defined. It's clearer if we rewrite it as

(ptr += 1) = ptr1;

Since C++11, the C++ standard provides that (§5.17 [expr.ass]/p1)

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. With respect to an indeterminately-sequenced function call, the operation of a compound assignment is a single evaluation.

So the assignment performed by the = is sequenced after the value computation of ptr += 1 and ptr1. The assignment performed by the += is sequenced before the value computation of ptr += 1, and all value computations required by the += are necessarily sequenced before that assignment. Thus, the sequencing here is well-defined and there is no undefined behavior.



回答2:

In C the result of pre and post increment are rvalues and we can not assign to an rvalue, we need an lvalue(also see: Understanding lvalues and rvalues in C and C++) . We can see by going to the draft C11 standard section 6.5.2.4 Postfix increment and decrement operators which says (emphasis mine going forward):

The result of the postfix ++ operator is the value of the operand. [...] See the discussions of additive operators and compound assignment for information on constraints, types, and conversions and the effects of operations on pointers. [...]

So the result of post-increment is a value which is synonymous for rvalue and we can confirm this by going to section 6.5.16 Assignment operators which the paragraph above points us to for further understanding of constraints and results, it says:

[...] An assignment expression has the value of the left operand after the assignment, but is not an lvalue.[...]

which further confirms the result of post-increment is not an lvalue.

For pre-increment we can see from section 6.5.3.1 Prefix increment and decrement operators which says:

[...]See the discussions of additive operators and compound assignment for information on constraints, types, side effects, and conversions and the effects of operations on pointers.

also points back to 6.5.16 like post-increment does and therefore the result of pre-increment in C is also not an lvalue.

In C++ post-increment is also an rvalue, more specifically a prvalue we can confirm this by going to section 5.2.6 Increment and decrement which says:

[...]The result is a prvalue. The type of the result is the cv-unqualified version of the type of the operand[...]

With respect to pre-increment C and C++ differ. In C the result is an rvalue while in C++ the result is a lvalue which explains why ++ptr = ptr1; works in C++ but not C.

For C++ this is covered in section 5.3.2 Increment and decrement which says:

[...]The result is the updated operand; it is an lvalue, and it is a bit-field if the operand is a bit-field.[...]

To understand whether:

++ptr = ptr1;

is well defined or not in C++ we need two different approaches one for pre C++11 and one for C++11.

Pre C++11 this expression invokes undefined behavior, since it is modifying the object more than once within the same sequence point. We can see this by going to a Pre C++11 draft standard section 5 Expressions which says:

Except where noted, the order of evaluation of operands of individual operators and subexpressions of individual expressions, and the order in which side effects take place, is unspecified.57) Between the previous and next sequence point a scalar object shall have its stored value modified at most once by the evaluation of an expression. Furthermore, the prior value shall be accessed only to determine the value to be stored. The requirements of this paragraph shall be met for each allowable ordering of the subexpressions of a full expression; otherwise the behavior is undefined. [ Example:

 i = v[i ++]; / / the behavior is undefined
 i = 7 , i++ , i ++; / / i becomes 9
 i = ++ i + 1; / / the behavior is undefined
 i = i + 1; / / the value of i is incremented

—end example ]

We are incrementing ptr and then subsequently assigning to it, which is two modifications and in this case the sequence point occurs at the end of the expression after the ;.

For C+11, we should go to defect report 637: Sequencing rules and example disagree which was the defect report that resulted in:

i = ++i + 1;

becoming well defined behavior in C++11 whereas prior to C++11 this was undefined behavior. The explanation in this report is one of best I have even seen and reading it many times was enlightening and helped me understand many concepts in a new light.

The logic that lead to this expression becoming well defined behavior goes as follows:

  1. The assignment side-effect is required to be sequenced after the value computations of both its LHS and RHS (5.17 [expr.ass] paragraph 1).

  2. The LHS (i) is an lvalue, so its value computation involves computing the address of i.

  3. In order to value-compute the RHS (++i + 1), it is necessary to first value-compute the lvalue expression ++i and then do an lvalue-to-rvalue conversion on the result. This guarantees that the incrementation side-effect is sequenced before the computation of the addition operation, which in turn is sequenced before the assignment side effect. In other words, it yields a well-defined order and final value for this expression.

The logic is somewhat similar for:

++ptr = ptr1;
  1. The value computations of the LHS and RHS are sequenced before the assignment side-effect.

  2. The RHS is an lvalue, so its value computation involves computing the address of ptr1.

  3. In order to value-compute the LHS (++ptr), it is necessary to first value-compute the lvalue expression ++ptr and then do an lvalue-to-rvalue conversion on the result. This guarantees that the incrementation side-effect is sequenced before the assignment side effect. In other words, it yields a well-defined order and final value for this expression.

Note

The OP said:

Yes, I understand it's messy and you're dealing with unallocated memory, but it works and compiles.

Pointers to non-array objects are considered arrays of size one for additive operators, I am going to quote the draft C++ standard but C11 has almost the exact same text. From section 5.7 Additive operators:

For the purposes of these operators, a pointer to a nonarray object behaves the same as a pointer to the first element of an array of length one with the type of the object as its element type.

and further tells us pointing one past the end of an array is valid as long as you don't dereference the pointer:

[...]If both the pointer operand and the result point to elements of the same array object, or one past the last element of the array object, the evaluation shall not produce an overflow; otherwise, the behavior is undefined.

so:

++ptr ;

is still a valid pointer.