为什么复制省略会使形式参数例外

Why does copy elision make an exception for formal parameters?

本文关键字:参数 复制省 为什么      更新时间:2023-10-16

这里有一个完整的程序:

#include <iostream>
using std::cout;
using std::endl;
using std::move;
int count {0};  // global for monitoring

class Triple {
public:
    Triple() = default;    // C++11 use default constructor despite other constructors being declared
    Triple(Triple&&) = default;
    Triple(const Triple& t) :    // copy constructor
        Triple(t.mX, t.mY, t.mZ) {
        count++;
    }
    Triple(const int x, const int y, const int z) :
        mX{ x }, mY{ y }, mZ{ z } {
    }
    const Triple& operator +=(const Triple& rhs) {
        mX += rhs.mX;
        mY += rhs.mY;
        mZ += rhs.mZ;
        return *this;
    }
    int x() const;
    int y() const;
    int z() const;
private:
    int mX{ 0 };    // c++11 member initialization
    int mY{ 0 };
    int mZ{ 0 };
};

#if 0
inline Triple operator+(const Triple& lhs, const Triple& rhs) {
    Triple left { lhs };
    left += rhs;
    return left;
}
#else
inline Triple operator+(Triple left, const Triple& rhs) {
    left += rhs;
    return left;
}
#endif
int main()
{
    Triple a,b;
    cout << "initial value of count is: " << count << endl;
    auto result { a+b };
    cout << "final value of count is: " << count << endl;
}

令人感兴趣的是,复制构造函数有副作用,operator+有两个版本可供考虑。

案例1

inline Triple operator+(const Triple& lhs, const Triple& rhs) {
    Triple left { lhs };
    left += rhs;
    return left;
}

案例2

inline Triple operator+(Triple left, const Triple& rhs) {
    left += rhs;
    return left;
}

Visual Studio 2015为这两种情况都给出了相同的结果,即打印1的结果。然而,gcc 4.8.4给出了情况1的2

复制省略的摘要陈述了"这不是函数参数",这让我认为VS是错误的。这是正确的吗?

但是,为什么在这个规则中对形式参数名称进行特殊处理?为什么它和其他局部变量不一样

(我并不是说优化器根据调用约定以及调用者和调用者ee的单独编译来解决问题,而只是为什么不允许。)

编辑:如果输出1是正确的,那么它如何符合省略规则?


注※:我发现该文本是从公开的N3690中的§12.8第31段中复制的。

首先,要明白RVO和NRVO是标准编写者向编译器编写者提供的机会。给定的编译器可以自由忽略RVO或NRVO的可能性,如果它不能使其工作,如果它不知道它是否能使其工作、如果月亮是圆的,等等。

不过,在这种情况下,这很容易。(N)RVO的基本实现方式是通过将返回值直接构造到由返回值占用的存储器中,或者甚至构造到将被设置为该返回值的变量占用的存储器。

也就是说,(N)RVO的潜在节约不仅来自于消除复制构造的能力,还来自于总体上减少复制的能力。

但是,当返回值的来源是一个函数参数时,就太晚了。left已经在内存中的某个位置,返回值必须转到其他位置。由于已经构造了第二个对象,所以复制已经是一种给定的方法,而不是像内联一样的暴力。

如果复制省略被禁用,那么这两种情况都由1次复制和3次移动组成。(代码的2的任何输出都表明存在编译器错误)。

副本为:

  • left的初始化

动作是:

  • 初始化left返回值
  • 返回值初始化由a+b表示的临时对象
  • a+b初始化result

count替换为表示"in copy constructor""in move constructor"的输出消息将更具启发性。目前您根本没有跟踪移动。

在通过参考的情况下,所有3个移动都可以被忽略。在传递值的情况下,可以忽略其中的2个移动。无法消除的移动是从left移动到返回值

我不知道为什么不能取消这一举措的理由。如果A()一直到a都是可省略的,那么编译器可能很难做像A a = foo( A() );这样的事情。