What Made I = I++ + 1; Legal in C++17

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

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.)

What is C17 and what changes have been made to the language?

According to GCC reference, C17 is actually a bug-fix version of the C11 standard with DR resolutions integrated.

C17, a bug-fix version of the C11 standard with DR [Defect Report] resolutions
integrated
, will soon go to ballot. This patch adds corresponding
options -std=c17, -std=gnu17 (new default version, replacing
-std=gnu11 as the default), -std=iso9899:2017. As a bug-fix version
of the standard, there is no need for flag_isoc17 or any options for
compatibility warnings; however, there is a new __STDC_VERSION__
value, so new cpplib languages CLK_GNUC17 and CLK_STDC17 are added to
support using that new value with the new options. (If the standard
ends up being published in 2018 and being known as C18, option aliases
can be added. Note however that -std=iso9899:199409 corresponds to a
__STDC_VERSION__ value rather than a publication date.)

(There are a couple of DR resolutions needing implementing in GCC, but
that's independent of the new options.)

So, there are no new features included in C17.

The Cppreference (History of C) says:

Future development

C17 Next minor C language standard revision, will include all accepted C11 defect reports, but no new features.

UPDATE:

  • 2018: C17 (ISO/IEC 9899:2018) (ISO Store) (Final draft) Includes the deprecation of ATOMIC_VAR_INIT and the fixes to the
    following defect reports:

[DR 400], [DR 401], [DR 402], [DR 403],
[DR 404], [DR 405], [DR 406], [DR 407],
[DR 410], [DR 412], [DR 414], [DR 415],
[DR 416], [DR 417], [DR 419], [DR 423],
[DR 426], [DR 428], [DR 429], [DR 430],
[DR 431], [DR 433], [DR 434], [DR 436],
[DR 437], [DR 438], [DR 439], [DR 441],
[DR 444], [DR 445], [DR 447], [DR 448],
[DR 450], [DR 452], [DR 453], [DR 457],
[DR 458], [DR 459], [DR 460], [DR 462],
[DR 464], [DR 465], [DR 468], [DR 470],
[DR 471], [DR 472], [DR 473], [DR 475],
[DR 477], [DR 480], [DR 481], [DR 485],
[DR 487], [DR 491]

C++17 sequencing: post-increment on left side of assignment

And the question: is the warning erroneous?

It depends.

Technically, the code in question is well-defined. The right-hand side is sequenced before the left-hand side in C++17, whereas before it was indeterminately sequenced. And gcc compiles the code correctly, v[0] == 1 after that assignment.

However, it is also terrible code that should not be written, so while the specific wording of the warning is erroneous, the actual spirit of the warning seems fine to me. At least, I'm not about to file a bug report about it and it doesn't seem like the kind of thing that's worth developer time to fix. YMMV.

Difference between C++14 and C++17 using: `*p++ = *p`

Reading from and writing to the variable (via the post-increment) used to have undefined behaviour, because the = did not introduce a sequence point. You could have received either behaviour (or none, or explosions) in C++14.

Now, there is a sequencing order defined for this case and your C++17 results are reliable.

Although it's still bad, unclear code that should not be written!

Shift operands sequenced in C++17

Standard is clear about the order of evaluation of the operands of the shift operator.

n4659 - §8.8 (p4):

The expression E1 is sequenced before the expression E2.

There is no undefined behavior in the expression i++ << i, it is well defined. It is a bug in Clang and GCC both.

Why did the range based 'for' loop specification change in C++17?

Using

auto __begin = begin_expr, __end = end_expr;

requires both begin_expr and end_expr to return the same type. This means you cannot have a sentinel iterator type that is different from the beginning type. Using

auto __begin = begin_expr ;
auto __end = end_expr ;

fixes that issue while proving full backwards compatibility with C++14.

Chained compound assignments with C++17 sequencing are still undefined behaviour?

In order to follow better what is actually performed, let's try to mimic the same with our own type and add some printouts:

class Number {
int num = 0;
public:
Number(int n): num(n) {}
Number operator+=(int i) {
std::cout << "+=(int) for *this = " << num
<< " and int = " << i << std::endl;
num += i;
return *this;
}
Number& operator+=(Number n) {
std::cout << "+=(Number) for *this = " << num
<< " and Number = " << n << std::endl;
num += n.num;
return *this;
}
operator int() const {
return num;
}
};

Then when we run:

Number a {5};
(a += 1) += a;
std::cout << "result: " << a << std::endl;

We get different results with gcc and clang (and without any warning!).

gcc:

+=(int) for *this = 5 and int = 1
+=(Number) for *this = 6 and Number = 6
result: 12

clang:

+=(int) for *this = 5 and int = 1
+=(Number) for *this = 6 and Number = 5
result: 11

Which is the same result as for ints in the question. Even though it is not the same exact story: built-in assignment has its own sequencing rules, as opposed to overloaded operator which is a function call, still the similarity is interesting.

It seems that while gcc keeps the right side as a reference and turns it to a value on the call to +=, clang on the other hand turns the right side to a value first.

The next step would be to add a copy constructor to our Number class, to follow exactly when the reference is turned into a value. Doing that results with calling the copy constructor as the first operation, both by clang and gcc, and the result is the same for both: 11.

It seems that gcc delays the reference to value conversion (both in the built-in assignment as well as with user defined type without a user defined copy constructor). Is it coherent with C++17 defined sequencing? To me it seems as a gcc bug, at least for the built-in assignment as in the question, as it sounds that the conversion from reference to value is part of the "value computation" that shall be sequenced before the assignment.


As for a strange behavior of clang reported in previous version of the original post - returning different results in assert and when printing:

constexpr int foo() {
int res = 0;
(res = 5) |= (res *= 2);
return res;
}

int main() {
std::cout << foo() << std::endl; // prints 5
assert(foo() == 5); // fails in clang 11.0 - constexpr foo() is 10
// fixed in clang 11.x - correct value is 5
}

This relates to a bug in clang. The failure of the assert is wrong and is due to wrong evaluation order of this expression in clang, during constant evaluation in compile time. The value should be 5. This bug is already fixed in clang trunk.

Order of evaluation with function pointers in C++17

The C++17 rule is, from [expr.call]/8:

The postfix-expression is sequenced before each expression in the expression-list and any default argument. The initialization of a parameter, including every associated value computation and side effect, is indeterminately sequenced with respect to that of any other parameter.

In (T(f))((f=b,0));, (T(f)) is sequenced before the initialization of the parameter from (f=b, 0). All of this is well-defined and the program should print "a". That is, it should behave just like:

auto __tmp = T(f);
__tmp((f=b, 0));

The same is true even if we change your program such that this were valid:

T{f}(f=b, 0); // two parameters now, instead of one

The f=b and 0 expressions are indeterminately sequenced with each other, but T{f} is still sequenced before both, so this would still invoke a.

Filed 91974.

Is difference between two pointers legal c++17 constant expression?

The question is moot. Pointer arithmetics is only defined on the pointers belonging to the same array, which is certainly not the case there. So, the code above is not legal C++, and in fact, fails to compile with compilers available to me.



Related Topics



Leave a reply



Submit