C++ copy elision
环境
gcc version 9.4.0
c++14
示例代码
//test.cpp
#include <iostream>
class Test {
public:
Test() {
std::cout << "Test Constructor..." << std::endl;
}
Test(const Test &other) {
std::cout << "Test Copy Constructor..." << std::endl;
}
};
Test getTest() {
Test t;
return std::move(t);
}
int main() {
Test t = getTest();
return 0;
}
编译错误
编译选项为 -Werror
- 编译指令
g++ test.cpp -o test -Werror
- 报错如下:
test.cpp:766:19: error: moving a local object in a return statement prevents copy elision [-Werror=pessimizing-move]
766 | return std::move(t);
| ~~~~~~~~~^~~
test.cpp:766:19: note: remove ‘std::move’ call
cc1plus: all warnings being treated as errors
不指定 -Werror 编译选项
- 编译指令
g++ test.cpp -o test
- 报错如下:
xx.cpp:761:19: warning: moving a local object in a return statement prevents copy elision [-Wpessimizing-move]
761 | return std::move(t);
| ~~~~~~~~~^~~
xx.cpp:761:19: note: remove ‘std::move’ call
报错解释
错误(警告):返回语句中使用std::move移动一个本地对象,会阻止copy elision优化。
提示:移除 ‘std::move’ 调用
解决办法
就如编译器提示的,在函数中返回一个本地对象时,直接返回即可,不要加上std::move。
深入理解copy elision
RVO 和NRVO
在C++17之前,标准并没有规定copy elision,编译器帮我们实现copy elision
在C++17及之后,标准从编译器借取经验,实现了copy elision。并规定RVO(return value optimization)必须要用copy elision,而NRVO(named return value optimization)则可以不用(由编译器决定)。
其中RVO指返回无名字的对象,NRVO正好相反表示返回有名字的对象。
Test getTest() { // RVO
return Test();
}
Test getTest() { // NRVO
Test t;
return t;
}
手动禁用编译器的copy elision
编译时,通过编译选项-fno-elide-constructors
告诉编译器不要帮我们 copy elision。
测试:C++14中copy elision
NROV
- 代码
Test getTest() { // NRVO
Test t;
return t;
}
编译器参与copy elision
- 编译
g++ test.cpp -o test -std=c++14
- 运行
Test Constructor...
- 现象
按照正常的程序执行流程,getTest函数在返回时执行一次拷贝构造函数,在main函数中将getTest的返回值赋值给 t 时再执行一次拷贝构造函数。
期待的运行结果应该是调用一次构造函数,两次拷贝构造函数,即:
Test Constructor...
Test Copy Constructor...
Test Copy Constructor...
但通过上面测试我们发现,程序只调用了一次构造函数,并没有调用拷贝构造函数。这是由于编译器默认已经进行了copy elision(就是不copy了,直接在函数调用的地方执行构造)。
这是一种非常高效的编译器优化,帮助我们省了两次拷贝构造操作。
编译器不参与copy elision
- 编译
g++ test.cpp -o test -std=c++14 -fno-elide-constructors
- 运行
Test Constructor...
Test Copy Constructor...
Test Copy Constructor...
- 现象
正如前面期待的,程序进行了两次拷贝构造函数的调用。
ROV
- 代码
Test getTest() { // RVO
return Test();
}
编译器参与copy elision
- 编译
g++ test.cpp -o test -std=c++14
- 运行
Test Constructor...
编译器不参与copy elision
- 编译
g++ test.cpp -o test -std=c++14 -fno-elide-constructors
- 运行
Test Constructor...
Test Copy Constructor...
Test Copy Constructor...
小结
在C++14中,无论是RVO还是NRVO,copy elision都是由编译器实现的。
测试:C++17中copy elision
NRVO
- 代码
Test getTest() { // NRVO
Test t;
return t;
}
编译器参与copy elision
- 编译
g++ test.cpp -o test -std=c++17
- 运行
Test Constructor...
编译器不参与copy elision
g++ test.cpp -o test -std=c++17 -fno-elide-constructors
- 运行
Test Constructor...
Test Copy Constructor...
RVO
- 代码
Test getTest() { // RVO
return Test();
}
编译器参与copy elision
- 编译
g++ test.cpp -o test -std=c++17
- 运行
Test Constructor...
编译器不参与copy elision
g++ test.cpp -o test -std=c++17 -fno-elide-constructors
- 运行
Test Constructor...
小结
在C++17中,标准实现了copy elision,编译器终于可以退居二线。
在返回RVO临时对象时,必须采用copy elision,不能调用其拷贝构造函数,前面测试可以看出无论是否禁用编译器的copy elistion功能,程序都不会调用拷贝构造函数。
但在返回NRVO临时对象时,C++好像有些偷懒,不像编译器会把所有的拷贝构造函数copy elision掉,只copy elision掉了一次。
那么NRVO情况下,究竟copy elision掉的是哪次的拷贝构造函数?下面通过测试来一探究竟:
NRVO copy elision探究
我们去掉最后一次拷贝构造,即在函数返回临时对象时,我们不接收。
- 代码
Test getTest() {
Test t;
return std::move(t);
}
int main() {
getTest();
return 0;
}
- C++17编译
g++ test.cpp -o test -std=c++17 -fno-elide-constructors
- 运行
Test Constructor...
Test Copy Constructor...
- 现象
我们发现运行结果与代码修改前一致。 - 结论
C++17 copy elision掉的就是最后一次拷贝构造函数,即将函数返回的临时对象拷贝到主函数临时对象的这一次。
因此在,NRVO情况下,在将临时对象进行返回前还是会进行一次复制,只在将复制后的对象用来构造主函数的临时对象时才采用copy elision。
既然编译器会帮我们copy elision,那么是不是就可以删除掉拷贝构造函数?
测试NRVO,删除掉拷贝构造函数
- 代码
//test.cpp
#include <iostream>
class Test {
public:
Test() {
std::cout << "Test Constructor..." << std::endl;
}
Test(const Test &other) = delete;
};
Test getTest() {
Test t;
return t;
}
int main() {
Test t = getTest();
return 0;
}
- 编译运行 C++17
david@DESKTOP-J901GTO:~$ g++ test.cpp -o test -std=c++17
test.cpp: In function ‘Test getTest()’:
test.cpp:21:10: error: use of deleted function ‘Test::Test(const Test&)’
21 | return t;
| ^
test.cpp:10:5: note: declared here
10 | Test(const Test &other) = delete;
| ^~~~
- 编译运行 C++14
david@DESKTOP-J901GTO:~$ g++ test.cpp -o test -std=c++14
test.cpp: In function ‘Test getTest()’:
test.cpp:20:18: error: use of deleted function ‘Test::Test(const Test&)’
20 | Test t; return t;
| ^
test.cpp:10:5: note: declared here
10 | Test(const Test &other) = delete;
| ^~~~
- 现象
编译报错,这是NRVO情况下,那么在RVO情况下呢
测试RVO,删除掉拷贝构造函数
将上面代码改为RVO
- 代码
Test getTest() {
return Test();
}
- 编译运行 C++17
david@DESKTOP-J901GTO:~$ g++ test.cpp -o test -std=c++17
david@DESKTOP-J901GTO:~$ ./test
Test Constructor...
- 编译运行 C++14
david@DESKTOP-J901GTO:~$ g++ test.cpp -o test -std=c++14
test.cpp: In function ‘Test getTest()’:
test.cpp:21:15: error: use of deleted function ‘Test::Test(const Test&)’
21 | return Test();
| ^
test.cpp:10:5: note: declared here
10 | Test(const Test &other) = delete;
| ^~~~
结论
针对RVO情况,如果在C++17及以上,可以删除拷贝构造函数。
其它所有情况,不能删除拷贝构造函数。