当将变量分配给对其自身的引用时,在修改然后由函数调用返回之间,结果应该是什么?

Dap*_*ter 22 c++ sequence-points

#include <iostream>

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

int main()
{
    int x {5};
    addOne(x) = x;
    std::cout << x << ' ' << addOne(x);
}
Run Code Online (Sandbox Code Playgroud)

我目前正在学习左值和右值,并进行了一些实验,并做出了这似乎得到了相互矛盾的结果。 https://godbolt.org/z/KqsGz3Toe产生的输出为“5 6”,Clion 和 Visual Studio 也是如此,但是https://www.onlinegdb.com/49mUC7x8U产生的结果为“6 7”

我认为,因为addOne作为引用调用,所以尽管被称为左值,x它仍会显式地将 的值更改为 6。x正确的结果应该是什么?

use*_*522 36

从 C++17 开始,指定了求值顺序,使得 的操作数=从右到左求值,而 的<<操作数从左到右求值,与这些运算符的结合性相匹配。(但这并不适用于所有运算符,例如+和其他算术运算符。)

所以在

addOne(x) = x;
Run Code Online (Sandbox Code Playgroud)

首先评估右侧的值,产生5。然后调用该函数addOne,它做什么并不重要,x因为它返回对其的引用,并将右侧值5分配给该引用。

形式上,首先评估右侧意味着我们x用它所保存的 (pr)值替换左值(左值到右值转换)。然后我们调用修改左值引用的addOne(x)对象。x

因此,想象临时变量来保存各个评估的结果,该行相当于(除了新变量引入的额外副本,这在 的情况下并不重要int):

int t = x;
int& y = addOne(x);
y = t; // same as x = t, because y will refer to x
Run Code Online (Sandbox Code Playgroud)

然后在行中

std::cout << x << ' ' << addOne(x);
Run Code Online (Sandbox Code Playgroud)

我们首先计算并输出x,得到5,然后调用addOne,得到6。

所以该行相当于(简化后,知道operator<<会再次返回std::cout):

int t1 = x;
std::cout << t1 << ' ';
int t2 = addOne(x);
std::cout << t2;
Run Code Online (Sandbox Code Playgroud)

该输出5 6是自 C++17 以来唯一正确的输出。


在 C++17 之前,赋值运算符两侧的求值顺序是无序的。

在同一标量(在赋值的右侧)上进行未排序的标量修改和值计算通常会导致未定义的行为。

但是,由于您将 的增量x放入函数中,因此一条附加规则表明函数体的执行仅与调用上下文中的其他计算不确定地排序,从而保存了这一点。这意味着该行将不再具有未定义的行为,但赋值两侧的求值发生的顺序可以是左优先或右优先。

这意味着我们不知道是x先评估然后再评估addOne(x)还是反之亦然。

因此,在该行之后,x可能是5或6。

6如果评估发生等同于

int& y = addOne(x);
int t = x;
y = t;
Run Code Online (Sandbox Code Playgroud)

然后在行中

std::cout << x << ' ' << addOne(x);
Run Code Online (Sandbox Code Playgroud)

C++17 之前的版本也存在同样的问题。对 的参数的评估<<是不确定地排序的,而不是从左到右,因此addOne(x)可以在左侧 之前评估x,即除了之前的顺序之外,评估还可以等效于

int t2 = addOne(x);
int t1 = x;
std::cout << t1 << ' ' << t2;
Run Code Online (Sandbox Code Playgroud)

在这种情况下x,首先递增,然后将其新值打印两次。

因此,可能的程序输出可以是以下之一:

5 6
6 6
6 7
7 7
Run Code Online (Sandbox Code Playgroud)

(从技术上讲,int t2 = addOne(x)有两种评估:一次调用addOne返回引用,然后调用左值到右值转换。这些可能与其他评估交错发生,但这不会给出任何新的程序输出。)


-std=c++17如果您使用的是 GCC 或 Clang 并且使用的是 MSVC,则可以通过编译器标志指定使用 C++17(或 C++20 等较新版本)/std:c++17。默认选择哪个标准版本取决于编译器和编译器版本。

  • @Dappster是的,`addOne(x)`会将`x`的值设置为`6`。但是,在调用“addOne(x)”之前,已对赋值的右侧进行了求值。计算标量变量的名称意味着它被它的值替换。因此,调用“addOne(x)”后发生的赋值不是“x = x”,而是“x = 5”。 (2认同)
  • @Slava 不改变标志之间的这种行为可能是有意义的,但编译器仍然这样做:https://godbolt.org/z/dn53cvoMs。我可以想象旧版本中的评估顺序与现在标准指定的不同,并且他们保留了该顺序以避免破坏错误地依赖它的旧代码。 (2认同)