Copy elision & RVO


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

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

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

  • 在初始化中,如果初始化表达式是一个prvaluecv-unqualified且他们的源类型和目的类型相同:
  T x = T(T(T())); // 只会有一个调用T的默认构造函数来初始化x
  • 在函数调用中,如果return语句的操作数是prvalue并且这个prvalue和函数的返回类型相同。
 T f() { return T(); }
 T x = f(); // 只会有一个调用T的默认构造函数来初始化x
 T* p = new T(f()) // 只会有一个调用T的默认构造函数来初始化*p

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

  • 如果一个函数以值的形式返回一个类且满足如下所有情况,那么复制和移动构造将被忽略,这时,当这个局部对象被构造时,它被直接构造成函数的返回值;否则就使用复制或移动构造。这个copy elision的变种被称为NRVO,“named return value optimization”。
    • 这个类不是带有自动储存时间的volatile对象
    • 不是函数参数或者catch语句的参数
    • 和返回类型相同(忽略顶层cv标识)
  • 当一个未绑定到任何引用的无名临时对象可以移动或复制到一个有相同类型(忽略顶层cv标识)的对象Object时,那么复制和移动构造将被忽略,这时,当这个临时对象被构造时,它被直接构造成Object;否则使用复制或移动构造。当这个临时对象是return语句的参数时,这个copy elision的变种被称为RVO,"return value optimization"。

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

  • In a throw-expression, if the operand is the name of a non-volatile object with automatic storage duration, which isn't a function parameter or a catch clause parameter, and whose scope does not extend past the innermost try-block (if there is a try-block), then copy/move is omitted. When that local object is constructed, it is constructed directly in the storage where the exception object would otherwise be moved or copied to.

  • When handling an exception, if the argument of the catch clause is of the same type (ignoring top-level cv-qualification) as the exception object thrown, the copy is omitted and the body of the catch clause accesses the exception object directly, as if caught by reference. This is disabled if such copy elision would change the observable behavior of the program for any reason other than skipping the copy constructor and the destructor of the catch clause's parameter.

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

  • In constant expression and constant initialization, all copy elision is guaranteed (note: this is by post-C++14 defect report CWG 2022).
  struct A {
      void *p;
      constexpr A(): p(this) {}
  };

  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
  }
接下来看如下代码:
#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

参考和引用


转载请注明:Serenity » Copy elision & RVO