8

考虑这段代码:

#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

这确实是我得到的。但是由于复制省略,可能out与内存中的最后一行输出位于同一位置in并导致输出为x: 2, y: 2?

我试过用 gcc 和 clang 编译-O0and -O3,结果仍然看起来像预期的那样。

4

5 回答 5

4

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

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

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

于 2015-10-23T14:50:22.050 回答
3

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

特别是,它们不需要复制或模仿抽象机器的结构。相反,需要符合要求的实现来模拟(仅)抽象机的可观察行为,如下所述

复制省略除外:

[...]允许实现省略类对象的复制/移动构造,即使为复制/移动操作选择的构造函数和/或对象的析构函数具有副作用。[...]

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

在所有情况下,赋值顺序在左右操作数的值计算之后,赋值表达式的值计算之前

并且我们知道函数的主体是不确定的,相对于其他评估没有明确排序的函数调用从 1.9 节:

调用函数(包括其他函数调用)中的每个求值,如果在被调用函数的主体执行之前或之后没有特别排序,则相对于被调用函数的执行是不确定的排序 9

和不确定的顺序意味着:

当 A 在 B 之前排序或 B 在 A 之前排序时,评估 A 和 B 的排序不确定,但未指定哪个。[注意:不确定顺序的评估不能重叠,但可以先执行。——尾注]

于 2015-10-23T15:00:37.550 回答
3

不,它不能!

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

检查此参考:

需要符合要求的实现来模拟(仅)抽象机的可观察行为,如下所述。...

执行格式良好的程序的一致实现应产生与具有相同程序和相同输入的抽象机的相应实例的可能执行序列之一相同的可观察行为。...

抽象机的可观察行为是它对易失性数据的读取和写入顺序以及对库 I/O 函数的调用。...

取自这个答案

在这个答案中,您可以看到复制省略可能产生不同输出的情况!

于 2015-10-23T14:52:13.740 回答
2

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

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

#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
}

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

于 2015-10-23T14:58:26.533 回答
1

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

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

Elision 通勤,实际上。(如果对象 A 和 B 一起省略,并且 B 和 C 一起省略,则实际上 A 和 C 一起省略)。

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

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

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

于 2015-10-23T19:20:55.397 回答