拷贝消除
在广义上,指满足一定条件时,编译期允许省略对临时对象的拷贝或移动操作。在C++17之前,对于这一特性都是“允许但不要求”,但绝大多数主流的编译器都会启用。从C++17开始,只要满足条件,对prvalue的拷贝/移动省略都是必选的。
一般来说,拷贝消除包含
- 返回值优化 RVO(Return Value Optimization)
- 具名返回值优化(Named Return Value Optimization)
返回值优化 RVO(Return Value Optimization)
函数直接返回一个匿名临时对象时,编译器可以将这个临时对象直接构造在调用者的目标空间中,省掉拷贝。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #include <iostream> class RVOTest { public: RVOTest() { std::cout << "默认构造" << std::endl; } RVOTest(const RVOTest & other) { std::cout << "拷贝构造" << std::endl; }
~RVOTest() { std::cout << "析构" << std::endl; } };
RVOTest get() { return RVOTest(); } int main() { RVOTest b = get(); return 0; }
|
运行上面的代码,会输出
也就是说,当用get()返回的RVOTest对象来初始化b时,并没有调用拷贝构造函数用临时对象RVOTest()来初始化b,而是直接在调用者,也就是b的存储区就地构造。
具名返回值优化(Named Return Value Optimization)
当返回的对象有名字(即局部变量)时,也可能进行省略拷贝。
1 2 3 4 5 6 7 8
| RVOTest getNamed() { RVOTest obj; return obj; } int main() { RVOTest b = getNamed(); return 0; }
|
运行上面的代码,会输出
与RVO不同,NRVO的触发条件更加严苛,例如下面的情况就不会触发NRVO
1. 返回不同分支的不同命名局部对象
在某些编译器里可能无法决定统一的内存位置,从而放弃 NRVO。
1 2 3 4 5 6 7 8 9
| RVOTest flag(bool flag) { RVOTest a, b; if (flag) return a; else return b; } int main() { RVOTest c = flag(false); return 0; }
|
运行上面的代码,会输出
1 2 3 4 5 6 7
| 默认构造 默认构造 ~~~~~~ 拷贝构造 析构 析构 析构
|
2. 返回函数参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| RVOTest rParam(RVOTest param) { std::cout << "~~~~~~" << std::endl; return param; } RVOTest rParamR(RVOTest& param) { std::cout << "~~~~~~" << std::endl; return param; } int main() { RVOTest b = get(); RVOTest d = rParam(b); std::cout << "======" << std::endl; RVOTest e = rParamR(b); return 0; }
|
运行上面的代码,会输出
1 2 3 4 5 6 7 8 9 10 11
| 默认构造 拷贝构造 ~~~~~~ 拷贝构造 析构 ====== ~~~~~~ 拷贝构造 析构 析构 析构
|
3. 复杂表达式
返回复杂表达式,例如某个函数调用、三目表达式等,也可能不会触发NRVO
1 2 3 4 5 6 7 8 9
| RVOTest tri() { RVOTest a, b; return 1 > 2 ? a : b; }
int main() { RVOTest f = tri(); return 0; }
|
运行上面的代码,会输出