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

时间:2015-10-23 14:48:53

标签: c++ undefined-behavior 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 elision out可能与in位于内存中的相同位置,导致最后一行输出为x: 2, y: 2

我尝试使用gcc和clang同时使用-O0-O3进行编译,结果仍然符合预期。

5 个答案:

答案 0 :(得分:4)

不,它不能。优化不能破坏格式良好的代码,并且此代码格式正确。

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

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

答案 1 :(得分:3)

不,它不能!

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

检查此ref:

  

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

     

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

     

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

取自answer

在这个answer中,您可以看到copy-elision可能产生不同输出的情况!

答案 2 :(得分:3)

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

  

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

复制省略除外:

  

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

但是仍然必须遵循排序规则,如果我们转到标准草案,我们就知道在从第5.17节评估左右操作数之后,分配是按顺序进行的:

  

在所有情况下,分配都在值之后排序   计算右和左操作数,并在赋值表达式的值计算之前

我们知道函数的主体与其他评估没有不确定的顺序,这些评估没有用第1.9节中的函数调用专门排序:

  

调用函数中的每个评估(包括其他函数调用)都没有特别说明   在执行被调用函数体之前或之后对序列进行测序是不确定的   关于被调用函数的执行9

并且不确定地排序意味着:

  

评估A和B在A之前对B进行测序或B之前对A进行测序时进行不确定测序,但未指定哪一种。 [注意:不确定顺序的评估不能重叠,但可以先执行。 - 后注]

答案 3 :(得分:2)

复制省略的唯一内容是允许&#34;打破&#34;是你的拷贝构造函数中有副作用的时候。这不是问题,因为复制构造函数应始终没有副作用。

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

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

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

答案 4 :(得分:1)

Elision是对象生命周期和身份的融合。

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

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

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

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

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