In Bjarne Stroustrup's The C++ Programming Language 4th edition section 36.3.6
STL-like Operations the following code is used as an example of chaining:
void f2()
{
std::string s = "but I have heard it works even if you don't believe in it" ;
s.replace(0, 4, "" ).replace( s.find( "even" ), 4, "only" )
.replace( s.find( " don't" ), 6, "" );
assert( s == "I have heard it works only if you believe in it" ) ;
}
The assert fails in gcc
(see it live) and Visual Studio
(see it live), but it does not fail when using Clang (see it live).
Why am I getting different results? Are any of these compilers incorrectly evaluating the chaining expression or does this code exhibit some form of unspecified or undefined behavior?
The code exhibits unspecified behavior due to unspecified order of evaluation of sub-expressions although it does not invoke undefined behavior since all side effects are done within functions which introduces a sequencing relationship between the side effects in this case.
This example is mentioned in the proposal N4228: Refining Expression Evaluation Order for Idiomatic C++ which says the following about the code in the question:
Details
It may be obvious to many that arguments to functions have an unspecified order of evaluation but it is probably not as obvious how this behavior interacts with chained functions calls. It was not obvious to me when I first analyzed this case and apparently not to all the expert reviewers either.
At first glance it may appear that since each
replace
has to be evaluated from left to right that the corresponding function argument groups must be evaluated as groups from left to right as well.This is incorrect, function arguments have an unspecified order of evaluation, although chaining function calls does introduce a left to right evaluation order for each function call, the arguments of each function call are only sequenced before with respect to the member function call they are part of. In particular this impacts the following calls:
and:
which are indeterminately sequenced with respect to:
the two
find
calls could be evaluated before or after thereplace
, which matters since it has a side effect ons
in a way that would alter the result offind
, it changes the length ofs
. So depending on when thatreplace
is evaluated relative to the twofind
calls the result will differ.If we look at the chaining expression and examine the evaluation order of some of the sub-expressions:
and:
Note, we are ignoring the fact that
4
and7
can be further broken down into more sub-expressions. So:A
is sequenced beforeB
which is sequenced beforeC
which is sequenced beforeD
1
to9
are indeterminately sequenced with respect to other sub-expressions with some of the exceptions listed below1
to3
are sequenced beforeB
4
to6
are sequenced beforeC
7
to9
are sequenced beforeD
The key to this issue is that:
4
to9
are indeterminately sequenced with respect toB
The potential order of evaluation choice for
4
and7
with respect toB
explains the difference in results betweenclang
andgcc
when evaluatingf2()
. In my testsclang
evaluatesB
before evaluating4
and7
whilegcc
evaluates it after. We can use the following test program to demonstrate what is happening in each case:Result for
gcc
(see it live)Result for
clang
(see it live):Result for
Visual Studio
(see it live):Details from the standard
We know that unless specified the evaluations of sub-expressions are unsequenced, this is from the draft C++11 standard section
1.9
Program execution which says:and we know that a function call introduces a sequenced before relationship of the function calls postfix expression and arguments with respect to the function body, from section
1.9
:We also know that class member access and therefore chaining will evaluate from left to right, from section
5.2.5
Class member access which says:Note, in the case where the id-expression ends up being a non-static member function it does not specify the order of evaluation of the expression-list within the
()
since that is a separate sub-expression. The relevant grammar from5.2
Postfix expressions:C++17 changes
The proposal p0145r3: Refining Expression Evaluation Order for Idiomatic C++ made several changes. Including changes that give the code well specified behavior by strengthening the order of evaluation rules for postfix-expressions and their expression-list.
[expr.call]p5 says: