22
#include <iostream>

int& addOne(int& x)
{
    x += 1;
    return x;
}

int main()
{
    int x {5};
    addOne(x) = x;
    std::cout << x << ' ' << addOne(x);
}

I'm currently in the middle of learning about lvalues and rvalues and was experimenting a bit, and made this which seems to be getting conflicting results. https://godbolt.org/z/KqsGz3Toe produces an out put of "5 6", as does Clion and Visual Studio, however https://www.onlinegdb.com/49mUC7x8U produces a result of "6 7"

I would think that because addOne is calling x as a reference, it would explicitly change the value of x to 6 despite being called as an lvalue. What should the correct result be?

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
Dappster
  • 322
  • 2
  • 8
  • 10
    It was UB pre-C++17. Stopped being UB due to [rules 19 and 20 here](https://en.cppreference.com/w/cpp/language/eval_order). – HolyBlackCat Jan 29 '22 at 20:12
  • 6
    @HolyBlackCat I don't think it was UB, but unspecified. The access and modification here are not unsequenced but indeterminately sequenced, since one is inside a function. A distinction with little difference, admittedly. – Igor Tandetnik Jan 29 '22 at 20:14
  • 9
    Quite soon nobody will understand what produces UB and what not in C++ – Slava Jan 29 '22 at 20:20
  • @IgorTandetnik I think both operations need to be inside functions for it to be indeterminately sequenced. – HolyBlackCat Jan 29 '22 at 20:20
  • [P0145](https://wg21.link/P0145) – Ted Lyngmo Jan 29 '22 at 20:20
  • 2
    @HolyBlackCat Not so. "**[intro.execution]/15** Every evaluation in the **calling** function (including other function calls) that is not otherwise specifically sequenced before or after the execution of the body of the **called** function is indeterminately sequenced with respect to the execution of the called function." Emphasis mine – Igor Tandetnik Jan 29 '22 at 20:23
  • @Dappster If you change from `C++` to `C++17` you should get `5 6` in that onlinegdb.com example. – Ted Lyngmo Jan 29 '22 at 20:29
  • @TedLyngmo You're right! But according to rule 20 in the order of evaluation, wouldn't x's value be changed to 6 since E2(addOne(x))'s computation should be taking precedence, though right? I could be misunderstanding it though. – Dappster Jan 29 '22 at 20:33
  • @Dappster E2 in the assignment is `x` in your case. `addOne(x)` is E1. – user17732522 Jan 29 '22 at 20:40
  • @IgorTandetnik Nice find! It seems the rule 11 at cppreference was incomplete, [I tried to fix it](https://en.cppreference.com/mwiki/index.php?title=cpp%2Flanguage%2Feval_order&diff=137683&oldid=136264). – HolyBlackCat Jan 29 '22 at 20:55
  • 2
    The question is academical. It has nothing to do with reality. Such code would never pass any code review. And the developer would be removed from the team. It is an artificial example, with no real value. Even not for demonstrating some rules. It is only important to start some lengthy discussions here on SO . . . But please continue. – A M Jan 29 '22 at 22:10
  • 1
    Related question [Does this code from "The C++ Programming Language" 4th edition section 36.3.6 have well-defined behavior?](https://stackoverflow.com/q/27158812/1708801) – Shafik Yaghmour Jan 30 '22 at 04:34
  • @ArminMontigny according to Hyrum's law there's likely some production code out there exactly like this. – JHBonarius Feb 27 '22 at 09:06

1 Answers1

36

Since C++17 the order of evaluation is specified such that the operands of = are evaluated right-to-left and those of << are evaluated left-to-right, matching the associativity of these operators. (But this doesn't apply to all operators, e.g. + and other arithmetic operators.)

So in

addOne(x) = x;

first the value of the right-hand side is evaluated, yielding 5. Then the function addOne is called and it doesn't matter what it does with x since it returns a reference to it, to which the right-hand value 5 is assigned.

Formally, evaluating the right-hand side first means that we replace the lvalue x by the (pr)value it holds (lvalue-to-rvalue conversion). Then we call addOne(x) to modify the object that the lvalue x refers to.

So, imagining temporary variables to hold the results of the individual evaluations, the line is equivalent to (except for extra copies introduced by the new variables, which don't matter in the case of int):

int t = x;
int& y = addOne(x);
y = t; // same as x = t, because y will refer to x

Then in the line

std::cout << x << ' ' << addOne(x);

we first evaluate and output x, resulting in 5, and then call addOne, resulting in 6.

So the line is equivalent to (simplified, knowing that operator<< will return std::cout again):

int t1 = x;
std::cout << t1 << ' ';
int t2 = addOne(x);
std::cout << t2;

The output 5 6 is the only correct one since C++17.


Before C++17, the evaluation order of the two sides of the assignment operator was unsequenced.

Having a scalar modification unsequenced with a value computation on the same scalar (on the right-hand side of your assignment) causes undefined behavior normally.

But since you put the increment of x into a function, an additional rule saying that the execution of a function body is merely indeterminately sequenced with other evaluations in the calling context saves this. It means that the line wont have undefined behavior anymore, but the order in which the evaluations of the two sides of the assignment happen could be either left-first or right-first.

This means we won't know whether x is evaluated first and then addOne(x) or the other way around.

Therefore after the line, x may be 5 or 6.

6 would be obtained if the evaluation happened equivalently to

int& y = addOne(x);
int t = x;
y = t;

Then in the line

std::cout << x << ' ' << addOne(x);

pre-C++17 the same issue applied. The evaluations of the arguments to << were indeterminately sequenced, rather than left-to-right and so addOne(x) could be evaluated before the left-hand x, i.e. in addition to the previous order, the evaluation could also be equivalent to

int t2 = addOne(x);
int t1 = x;
std::cout << t1 << ' ' << t2;

In this case x is first incremented and then its new value is printed twice.

Therefore possible program output could be either of the following:

5 6
6 6
6 7
7 7

(Technically the int t2 = addOne(x) are two evaluations: One call to addOne returning a reference and then the lvalue-to-rvalue conversion. These could happen interleaved with the other evaluations, but this doesn't give any new program outputs.)


You can specify to use C++17 (or newer versions like C++20) with the -std=c++17 flag to the compiler if you are using GCC or Clang and /std:c++17 if you are using MSVC. Which standard version is chosen by-default depends on the compiler and compiler version.

user17732522
  • 53,019
  • 2
  • 56
  • 105
  • When you say "Then the function addOne is called and it doesn't matter what it does with x since it returns a reference to it, to which the right-hand value 5 is assigned." I'm a bit confused by that, since I've always been told that a reference is an alias to the actual object. So when we're passing in 'x' by reference, we are mutating x's value with **x+=1** which changes it to 6, and then we are returning x (which should be 6 in the addOne function) into the `x` defined in main. – Dappster Jan 29 '22 at 20:43
  • 2
    @Dappster Yes, `addOne(x)` will set the value of `x` to `6`. However, the right-hand side of the assignment has been evaluated _before_ `addOne(x)` is called. Evaluating the name of a scalar variable means that it is replaced by its value. Therefore the assignment that will happen after the call to `addOne(x)` is not `x = x`, but `x = 5`. – user17732522 Jan 29 '22 at 20:45
  • @Dappster By-the-way since you said you are learning about lvalues and rvalues: The evaluation that replaces the lvalue `x` with the prvalue `5` is called a _lvalue-to-rvalue conversion_ which is done here since the built-in assignment to a scalar type requires the right-hand side to be a prvalue. – user17732522 Jan 29 '22 at 20:51
  • Ah okay that starts to make more sense. That's really cool! This was my first exposure to right-to-left evaluation, and was rather confusing as I don't have a thorough technical understanding of how C++'s order of evaluation works. I didn't really think of **x** on the right hand side, as something to be evaluated. – Dappster Jan 29 '22 at 20:57
  • "You can specify to use C++17" it is interesting to check, but logically they should make compiler produce the same code in this case irrelevant if C++17 key is specified or not. – Slava Jan 29 '22 at 22:25
  • 2
    @Slava It might make sense to not vary such behavior between flags, but compilers still do that: https://godbolt.org/z/dn53cvoMs. I could imagine that the order of evaluation was different in older versions than the standard specifies now and they kept that order to avoid breaking old code that mistakenly relied on it. – user17732522 Jan 29 '22 at 22:31
  • @Slava - "logically they should make compiler produce the same code in this case irrelevant if C++17 key is specified or not." - that would cause optimization regressions in pre-C++17 codebases. TANSTAAFL - restricting the compiler from reordering this sort of thing _does_ prevent some optimizations that otherwise would be allowed. C++17 judged that the tradeoff was worth it. – TLW Jan 30 '22 at 18:10
  • I wish it were possible to follow users, to get a notification on all their excellent answers. Oh wait though...this users's answers are always listed in the Hot Network Questions, so never mind. :P –  Jan 31 '22 at 00:11
  • I've often found it helpful to explain sequencing like this by showing equivalent code using temporary variables. `temp = x; addOne(x) = temp;`. – Barmar Feb 01 '22 at 23:51
  • And when the spec is indeterminate, I show multiple possible equivalences. – Barmar Feb 01 '22 at 23:56
  • That's exactly the point. I use temporaries to demonstrate variables that have been converted to rvalues, while I keep the variable for the lvalues. – Barmar Feb 01 '22 at 23:57
  • Yeah, you got it now. +1 – Barmar Feb 02 '22 at 00:09