C++ Copy Elision

在写代码是发现拷贝构造函数有时候没有调用,想起C++ Primer中提到过

the compiler can omit calls to the copy constructor.

后来查到是发生了copy elision。

首先有那么一个类定义,其中静态成员c是对象编号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class C {
public:
C() {
name_ = c;
++c;
cout << "c" << name_ << ": " << __func__ << endl;
}
C(const int name) : name_(name) {
cout << "c" << name_ << ": " << __func__ << endl;
}
C(const C &rhs) : name_(c++) {
cout << "copy from c" << rhs.name_ << ", c" << name_ << ": " << __func__
<< endl;
}
C &operator=(const C &rhs) {
cout << "copy assign from c" << rhs.name_ << ", c" << name_ << ": "
<< __func__ << endl;
return *this;
}

int name_ = 0;
static int c;
};

int C::c = 10000;

然后是有下面的几个函数定义和调用,

1
2
3
4
5
6
7
8
9
10
void f(C c) {
C tmp();
tmp = c;
}
C f() { return C(); }

C c1(1);
f(c1);
f(C(2));
C c = f();

以上三个f的调用分别输出什么?

下面是结果,

1
2
3
4
5
6
7
8
9
10
c1: C
copy from c1, c10000: C
c10001: C
copy assign from c10000, c10001: operator=
-----
c2: C
c10002: C
copy assign from c2, c10002: operator=
-----
c10003: C

第一个没什么好说的,首先用c1对形参c做拷贝初始化,接着tmp进行默认初始化,然后用拷贝赋值,将c赋值给tmp

第二个的结果就有点怪了,为什么C(2)得到的临时对象直接进行了赋值,而不首先初始化形参c?而第三个,为什么返回的临时对象一次拷贝都没发生?

因为在这两种情况中发生了copy elision(1234)。Copy elision是一种优化手段,满足特定条件时会发生,当传入的参数是rvalue的时候,无需进行额外的拷贝,直接使用源对象。RVO,全称叫return value optimization,编译器会让调用函数在其栈上分配空间,被调函数返回值处的临时对象会在这块内存上构造,进而避免了return时临时对象的拷贝,是copy elision常见形式。根据返回的对象是否是临时的,有named return value optimizationreturn value optimization

Copy elision和rvo即使在有可观察的到的side-effects时,也会发生,是As-if rule的例外中的一种。Dave Abrahams写过pass by value的一系列文章。Ayman B. Shoukry在这里讨论了nrvo的局限(multiple return points和conditional initialization)。

clang++和g++可以用-fno-elide-constructors控制是否开启优化。关闭优化后,输出的结果就和期待的一致了,

1
2
3
4
5
6
7
8
9
10
11
12
13
c1: C
copy from c1, c10000: C
c10001: C
copy assign from c10000, c10001: operator=
-----
c2: C
copy from c2, c10002: C
c10003: C
copy assign from c10002, c10003: operator=
-----
c10004: C
copy from c10004, c10005: C
copy from c10005, c10006: C

stackoverflow的这个答案,给出了Standard reference和发生copy elision以及return value optimization常见的例子,下面是搬运例子,

  • nrvo
1
2
3
4
5
6
7
8
9
10
11
class Thing {
public:
Thing();
~Thing();
Thing(const Thing&);
};
Thing f() {
Thing t;
return t; // optimization
}
Thing t2 = f();
  • rvo
1
2
3
4
5
6
7
8
9
10
class Thing {
public:
Thing();
~Thing();
Thing(const Thing&);
};
Thing f() {
return Thing(); // optimization
}
Thing t2 = f();
  • pass a temporary object by value
1
2
3
4
5
6
7
8
9
class Thing {
public:
Thing();
~Thing();
Thing(const Thing&);
};
void foo(Thing t); // optimization

foo(Thing());
  • exception is thrown and caught by value
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Thing{
Thing();
Thing(const Thing&);
};

void foo() {
Thing c;
throw c; // optimization
}

int main() {
try {
foo();
}
catch(Thing c) {}
}