无论复制省略如何,这段代码是否定义明确?

Is this code well-defined regardless of copy elision?

考虑这段代码:

#include <iostream>

struct Test
{
    int x;
    int y;
};

Test func(const Test& in)
{
    Test out;
    out.x=in.y;
    out.y=in.x;
    return out;
}

int main()
{
    Test test{1,2};
    std::cout << "x: " << test.x << ", y: " << test.y << "\n";
    test=func(test);
    std::cout << "x: " << test.x << ", y: " << test.y << "\n";
}

人们会期望这样的输出:

x: 1, y: 2
x: 2, y: 1

这确实是我得到的。但是由于 copy elisionout 会不会在内存中与 in 位于同一个位置并导致输出的最后一行是 x: 2, y: 2

我尝试用 -O0-O3 用 gcc 和 clang 编译,结果看起来仍然符合预期。

不,不能。优化不能破坏格式良好的代码,而这段代码是格式良好的。

编辑: 小更新。当然,我的回答是假设编译器本身没有错误,这当然是你只能祈祷的:)

EDIT2:有些人在谈论复制构造函数中的副作用并且它们很糟糕。当然,他们也不错。我的看法是,在 C++ 中,您不能保证创建了已知数量的临时对象。您保证创建的每个临时对象都将被销毁。虽然允许优化通过复制省略来减少临时对象的数量,但也可以增加它! :) 只要您的副作用在编码时考虑到这一事实,您就很好。

不,不可能!

优化并不意味着您会在编写良好(而非病态)的代码中获得未定义的行为。

检查此参考:

conforming implementations are required to emulate (only) the observable behavior of the abstract machine as explained below. ...

A conforming implementation executing a well-formed program shall produce the same observable behavior as one of the possible execution sequences of the corresponding instance of the abstract machine with the same program and the same input. ...

The observable behavior of the abstract machine is its sequence of reads and writes to volatile data and calls to library I/O functions. ...

取自此answer

在此answer中,您可以看到复制省略可能产生不同输出的情况!

唯一允许 "break" 复制省略的是当您的复制构造函数中有 副作用 时。这不是问题,因为复制构造函数应该始终没有副作用。

仅供说明,这是一个具有副作用的复制构造函数。该程序的行为确实取决于编译器优化,即是否实际制作了副本:

#include <iostream>

int global = 0;

struct Test
{
    int x;
    int y;

    Test() : x(0), y(0) {}

    Test(Test const& other) :
        x(other.x),
        y(other.y)
    {
        global = 1; // side effect in a copy constructor, very bad!
    }
};

Test func(const Test& in)
{
    Test out;
    out.x=in.y;
    out.y=in.x;
    return out;
}

int main()
{
    Test test;
    std::cout << "x: " << test.x << ", y: " << test.y << "\n";
    test=func(test);
    std::cout << "x: " << test.x << ", y: " << test.y << "\n";
    std::cout << global << "\n"; // output depends on optimisation
}

您展示的代码没有此类副作用,并且您的程序行为定义明确。

这是格式良好的代码,优化不能破坏格式良好的代码,因为它会违反 as-if rule。这告诉我们:

In particular, they need not copy or emulate the structure of the abstract machine. Rather, conforming implementations are required to emulate (only) the observable behavior of the abstract machine as explained below

复制省略例外:

[...]an implementation is allowed to omit the copy/move construction of a class object, even if the constructor selected for the copy/move operation and/or the destructor for the object have side effects.[...]

但是仍然必须遵循排序规则,如果我们查看标准草案,我们会知道赋值是在从第 5.17 节评估左右操作数之后排序的:

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

而且我们知道,函数体相对于其他求值的顺序是不确定的,而其他求值并没有用第 1.9 节中的函数调用明确排序:

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 9

和不确定排序意味着:

Evaluations A and B are indeterminately sequenced when either A is sequenced before B or B is sequenced before A, but it is unspecified which. [ Note: Indeterminately sequenced evaluations cannot overlap, but either could be executed first. —end note ]

省略是对象生命周期和身份的合并。

省略可以发生在临时(匿名对象)和它用于(直接)构造的命名对象之间,以及不是函数参数的函数局部变量和 return 值之间一个函数。

省略通勤,实际上。 (如果对象A和B一起省略,B和C一起省略,那么实际上A和C一起省略)。

要用函数外的变量省略函数的 return 值,您必须直接从 return 值构造 return 值。虽然在某些情况下构造变量可以在构造之前命名,但使用它(以类似于上述代码的方式)是未定义的行为在构造函数发生之前。

这个外部变量的构造函数在 func 的主体之后排序,因此在 func 被调用之后。所以它不可能在调用 func 之前发生。

Here 是我们在构造变量之前命名变量并将其传递给 func,然后使用 return 值初始化变量的示例func。在这种情况下,编译器似乎选择不省略,但正如下面评论中所观察到的,id 实际上做了:我对 UB 的调用隐藏了省略。 (卷积是为了防止编译器预先计算 test.x 和 test.y 的值)。