Copy Elision 中的返回值优化和右值拷贝优化
本文最后更新于:5 years ago
1.引子
1.1 问题
对于一个类 MyString
,可以用字符串来实例化一个对象,比如:
1 |
|
但是当我们用下面的方法去实例化一个对象时,会经历了什么?是只调用一次构造函数,还是调用一次构造函数 + 一次拷贝构造呢?
1 |
|
答案是:只调用了一次构造函数。
1.2 完整过程
MyString s1 = “123a”;
的完整过程应该是以下两步:
1 |
|
所以讲道理,应该是先调用一次构造函数,再调用一次拷贝构造。为什么编译器只调用了一次构造函数?
2.Copy Elision
原因就是编译器优化了这一过程,优化方式就是拷贝省略(Copy Elision)中的右值拷贝优化,以避免对 临时对象 无谓的拷贝。
1 |
|
具体来说,Copy Elision 包含两个方面:
- 右值拷贝优化。
- 返回值优化(RVO 和 NRVO)。
2.1 右值拷贝优化
当一个类的 临时对象 被拷贝赋予同一类型的另一个对象时,通过直接利用该临时对象的方法来避免拷贝操作。
比如有个类 A,有构造函数、拷贝构造、和析构函数:
1 |
|
我们用临时对象初始化新的对象:
1 |
|
打印的结果是:
1 |
|
所以上述过程均不会发生拷贝构造,这就是右值拷贝优化。
2.2 返回值优化(RVO)
RVO:return value optimization
NRVO:Named Return Value Optimization
返回值优化包括具名返回值优化(NRVO)与无名返回值优化(URVO),两者的区别在于返回值是具名的局部变量还是无名的临时对象。
但实际上,无论是具名的局部变量还是无名的临时对象,被当作返回值时,都应该拷贝构造一个新的临时对象,并返回新的临时对象。
依旧是类A:
1 |
|
我们返回 A类的对象:
1 |
|
打印结果为:
1 |
|
按道理来说,函数返回对象应该是:
- 先将返回的对象拷贝到一个临时对象中
- 并析构掉返回的对象,
- 再将临时对象返回。
但是由于编译的返回值优化,省去了中间的拷贝构造。
2.3 举个例子
这个例子使用了 右值拷贝优化返回值优化。
1 |
|
打印结果为:
1 |
|
-fno-elide-constructors
选项可以关闭 Copy Elision ,加上选项,编译运行 g++ main.cc -o out -fno-elide-constructors && ./out
打印的结果为:
1 |
|
3.返回值优化的特例
RVO是把 将要返回的局部变量 直接构造在临时对象所在的内存中,达到少调用一次copy ctor的目的。
但是,如果编译器不知道他要返回那个变量,就无法优化,比如:
1 |
|
打印结果为:
1 |
|
4.总结
右值拷贝优化:优化用 临时变量 初始化新对象的拷贝构造。
返回值优化:优化 用临时变量 做返回值时的拷贝构造。
值得注意的是,以上均是对临时变量的优化。
除拷贝省略(Copy Elision)之外,还有移动语义(Move Semantic)、完美转发(Perfect Forwarding)来消除对象交互之间的无谓的拷贝。有机会来填坑…👋👋