Copy elision & RVO

由于C++标准允许编译器对代码进行任何优化,所以了解这两者如何产生以及如何防止可以让我们更好的掌控所写的代码

Optimizes out copy- and move- constructor, resulting in zero-copy pass-by-value semantics.
优化复制和移动构造函数的结果是产生值传递的零拷贝语义

以下几种情景下,编译器必须忽略复制和移动构造函数,即使复制和移动构造函数以及析构函数有明显的side-effects

  T x = T(T(T())); // 只会有一个调用T的默认构造函数来初始化x
 T f() { return T(); }
 T x = f(); // 只会有一个调用T的默认构造函数来初始化x
 T* p = new T(f()) // 只会有一个调用T的默认构造函数来初始化*p

下面几种情况,编译器被允许忽略复制和移动构造函数,即使复制和移动构造函数以及析构函数有明显的side-effects

C++11起,又多了两种情况

C++14起,又多了种情况

constexpr A g() { A a; return a; }

constexpr A a; // a.p points to a constexpr A b = g(); // b.p points to b (NRVO guaranteed)

void g() { A c = g(); // c.p may point to c or to an ephemeral temporary }

##### 接下来看如下代码:   
```c++
#include <iostream>

using std::cout;

struct Obj
{
  Obj() { cout << "constructor.\n"; }
  Obj(const Obj&) { cout << "copy constructor.\n"; }
};

int main()
{
  Obj obj = Obj();
}

如果编译器不做任何优化(gcc -fno-elide-constructors),那么以上代码会和我们期望的那样输出

./a.out
constructor.
copy constructor.

但是,实际上编译之后只输出了

./a.out
constructor.

根据上面的说明,这个列子是初始化情况中的第一种,另外有一点要说明:这里的copy constructor必须是可访问的(不能为private或者delete)

接下来看两种RVO
Obj foo()
{
  return Obj(); // RVO
}

Obj bar()
{
  Obj res;
  return res; // NRVO
}

int main()
{
  Obj obj(foo());
  cout << "-----------------\n";
  Obj obj1(bar());
}

很明显,两个局部变量都直接分别的构造成了objobj1

注意

Copy elision is the only allowed form of optimization that can change the observable side-effects. Because some compilers do not perform copy elision in every situation where it is allowed (e.g., in debug mode), programs that rely on the side-effects of copy/move constructors and destructors are not portable.

有一个极其常见的例外,编译器不会进行copy elision:

Obj baz(int i)
{
  Obj res;
  if(i > 0)
    return res;
  else
    return Obj();
}

这里多出了一个判断,并且存在两个临时的对象,这种情况,编译器不会进行copy elision,也不知道如何进行(判断的结果出现再运行期),所以这里会调用copy constructor
在这个例外中,如果对象Obj有一个移动构造函数的话,那么baz()函数将会先后触发默认构造函数和移动构造函数! copy_elision_move > In a return statement or a throw-expression, if the compiler cannot perform copy elision but the conditions for copy elision are met or would be met, except that the source is a function parameter, the compiler will attempt to use the move constructor even if the object is designated by an lvalue; see return statement for details. – since C++11

下面这个例子中,本意是使用一个静态变量来统计对象有多少副本

struct Obj
{
  static int count;
  Obj(){}
  Obj(const Obj&) { count += 1; }
};
int Obj::count = 0;
int main()
{
  Obj obj = Obj();
  Obj obj1 = Obj();
  cout << Obj::count << endl;
}

然而,由于copy elision,打印出的是0。


最后,有关RVO和std::move查看下面的[RVO vs std::move]()。

参考和引用