在函数结束,返回一个对象时,照理来说,会调用其拷贝构造函数,但这无疑是一种浪费。在语言的发展过程中,对此有很多优化,这些优化有的来自编译器的实现,有的来自标准的规定。
下面分阶段介绍一下:
第一阶段:两次拷贝(其实没有存在过)
//test.cpp
#include <iostream>
#include <vector>
using namespace std;
class Test {
public:
Test() {
cout << "Test Constructor..." << endl;
}
Test(const Test &other) {
cout << "Test Copy Constructor..." << endl;
}
};
Test getTest() {
Test t;
return t;
}
int main() {
Test t = getTest();
return 0;
}
$ g++ test.cpp -o test
$ ./test
Test Constructor...
通过执行上面程序可以发现,程序只调用了一次构造函数,并没有调用拷贝构造,这是由于编译器默认已经进行了copy elision(就是不copy了,直接在函数调用的地方构造),所有这种情况其实是下面第二阶段的情况。
要想看到原始的情况,需要手动禁用copy elision:(指定-fno-elide-constructors)
$ g++ test.cpp -o test -fno-elide-constructors
$ ./test
Test Constructor...
Test Copy Constructor...
Test Copy Constructor...
第二阶段: 编译器自己实现了copy elision(C++17之前)
上面提到了,demo就是上面的不指定-fno-elide-constructors的情况。
第三阶段:标准规定RVO要用copy elision(c++17之后)
先来说明一下什么叫RVO,RVO(return value optimation)是与NRVO(named return value optimizatioin)对应的一个概念,就我们的demo来说:
Test getTest() { //RVO
return Test();
}
Test getTest() { //NRVO
Test t;
return t;
}
也就是说,NRVO返回的对象有自己的名字,而RVO则没有。
所以C++17的规定可以解释为:如果返回对象没有名字,则必须采用copy elision,不能调用其拷贝构造函数。
这里要澄清一个事实,虽然在此之前,很多编译都已经进行了copy elision,但这并不是必须的。因为标准没规定,其实是编译器自作主张,将调用其拷贝构造的过程优化了。
下面的demo可以说明一些问题:
//test.cpp
#include <iostream>
#include <vector>
using namespace std;
class Test {
public:
Test() {
cout << "Test Constructor..." << endl;
}
Test(const Test &other) = delte;
};
Test getTest() {
Test t;
return t;
}
int main() {
Test t = getTest();
return 0;
}
[chenyifei@chenyifei-ThinkPad-L14-Gen-1 move]$ g++ test.cpp -o test
test.cpp: In function ‘Test getTest()’:
test.cpp:18:10: error: use of deleted function ‘Test::Test(const Test&)’
18 | return t;
意思是,虽然编译器按照copy elision,不会调用其拷贝构造函数,但类必须得有拷贝构造函数,这就是因为编译器必须要保证语法正确,才能进行copy elision优化。
但当C++标准规定必须要copy elision后,就可以没有拷贝构造函数了:
1 //test.cpp
2 #include <iostream>
3 #include <vector>
4
5 using namespace std;
6
7 class Test {
8 public:
9 Test() {
10 cout << "Test Constructor..." << endl;
11 }
12 Test(const Test &other) = delete;
13
14 };
15
16 Test getTest() { //RVO
17 return Test();
18 }
19 int main() {
20 Test t = getTest();
21 return 0;
22 }
$ g++ test.cpp -o test
test.cpp: In function ‘Test getTest()’:
test.cpp:17:15: error: use of deleted function ‘Test::Test(const Test&)’
17 | return Test();
| ^
test.cpp:12:5: note: declared here
12 | Test(const Test &other) = delete;
| ^~~~
test.cpp: In function ‘int main()’:
test.cpp:20:20: error: use of deleted function ‘Test::Test(const Test&)’
20 | Test t = getTest();
| ^
test.cpp:12:5: note: declared here
12 | Test(const Test &other) = delete;
$ g++ test.cpp -o test -std=c++17
$ ./test
Test Constructor...
注意:仅限于RVO。
考虑移动构造
在函数返回对象的时候,移动构造的也会涉及其中。因为返回的局部对象(无论是RVO还是NRVO)都会判定为右值,所以(如果需要拷贝的话)优先匹配移动构造。但注意copy elision是在“如果需要拷贝”前面的,也就是说由于编译器无论是RVO还是NRVO,都已经作了优化,所以在实际操作过程中,几乎不会碰到遇到需要拷贝的情况,就算指定了-fno-elide-constructors,也会优先匹配移动构造函数。只有移动构造函数不存,才会匹配拷贝构造函数。
这里还有个小情况:
warning: moving a local object in a return statement prevents copy elision [-Wpessimizing-move]
意思是如果手动给返回对象加上了std::move()则优先调用移动构造,不再进行copy elision优化。
所以除非你的语义是:必须要调用移动构造函数去做移动之外的事情。否则,给返回值加std::move()是个错误的选择。