tuple是C++11开始引入的新的STL容器。其实从C++11开始,不止make_tuple(),其他函数你都不太需要再也把STL容器的临时对象按值返回会不会有性能问题。
类似的问题在StackOverflow上早有讨论。
我再来稍微展开一下,C++11开始当按值返回的时候,自动尝试使用move语义,而非拷贝语义,被称为copy elision(复制消除)。和楼主提到的具名返回值优化(NRVO或RVO)目的类似,都是提高C++函数返回时的效率,减少冗余的拷贝。RVO或NRVO在C++11之前便存在,C++11以后也同样存在。举个例子这段代码:
#include <iostream> #include <vector> using namespace std; vector<int> foo(int n) { vector<int> v; for (int i = 1; i <= n; i++) { v.push_back(i); } cout <<&v<<endl; return v; } int main() { vector<int> v = foo(10); cout <<&v<<endl; }
使用C++98和C++11分别编译:
g++ rvo.cpp -std=c++98 -o 98.out
g++ rvo.cpp -std=c++11 -o 11.out
分别运行:
./98.out
0x7ffc680bf490
0x7ffc680bf490
./11.out
0x7ffc5e871300
0x7ffc5e871300
可以看出函数内的临时对象和函数外接收这个返回值的对象是同一个地址,也就是说没有产生拷贝构造(按C++11之前标准应该是拷贝构造)这一优化就是NRVO,这属于编译器厂商们自己做的优化(即使不开O1、O2这种优化,也会默认做)。广义上讲RVO和NRVO也是copy elision,但并不是C++标准要求的(C++17开始RVO和NRVO从优化建议,变成了标准中的强制要求)。而C++11标准开始要求另外一种copy elision(以下描述的copy elision特指这种)。
来我们关闭NRVO来看看,给g++加上一个参数 -fno-elide-constructors即可。
g++ rvo.cpp -std=c++98 -fno-elide-constructors -o 98.out
g++ rvo.cpp -std=c++11 -fno-elide-constructors -o 11.out
再执行看看:
./98.out
0x7ffc0988eac0
0x7ffc0988eb00
./11.out
0x7fff39efc750
0x7fff39efc790
去掉NRVO后,可以看到二者不是同一个对象了。但其实对于C++11的代码而言,这其中仍然有copy elision,也就是说会自动执行move语义,我们改下测试代码:
#include <iostream> #include <vector> using namespace std; vector<int> foo(int n) { vector<int> v; for (int i = 1; i <= n; i++) { v.push_back(i); } cout << "obj stack addr: "<< &v << " in foo" <<endl; cout << "obj data addr: "<< v.data() << " in foo" <<endl; return v; } int main() { vector<int> v = foo(10); cout << "obj stack addr: "<< &v << " in main" <<endl; cout << "obj data addr: "<< v.data() << " in main" <<endl; }
然后重新携带 -fno-elide-constructors参数分别编译执行。
./98.out
obj stack addr: 0x7ffc1301c090 in foo
obj data addr: 0x55b81763af20 in foo
obj stack addr: 0x7ffc1301c0d0 in main
obj data addr: 0x55b81763b380 in main
./11.out
obj stack addr: 0x7ffeb4acac30 in foo
obj data addr: 0x556ecd26ef20 in foo
obj stack addr: 0x7ffeb4acac70 in main
obj data addr: 0x556ecd26ef20 in main
可以看出,尽管C++11去掉了NRVO以后,main函数中的对象v和foo函数中的对象v不是同一个。但他们中的data()指向的数据地址是同一个。也就是说C++11开始,你用函数按值返回一个STL容器,即使没有显式地加move,也会自动按move语义走,进行数据指针的修改,而不会拷贝全部的数据。
当然copy elision并不是只针对STL容器类型啦,所有有move语义的对象类型都可以。但当没有move语义时,如果去掉NRVO还是会执行拷贝的。
再看个自定义类型的代码:
#include <iostream> #include <vector> using namespace std; class A { public: A() { cout << this << " construct " <<endl; _data = new int[size]; } A(const A& a) { cout << this << " copy from " <<&a <<endl; _data = new int[a._len]; for (size_t i = 0; i < a._len; i++) { this->_data[i] = a._data[i]; } } ~A() { if (_data) { delete[] _data; } } bool push_back(int e) { if (_len == size) { return false; } _data[_len++] = e; return true; } int* data() { return _data; } size_t length() { return _len; } private: static const int size = 100; int* _data = nullptr; size_t _len = 0; }; A foo(int n) { A a; for (int i = 1; i <= n; i++) { a.push_back(i); } cout << "obj stack addr: "<< &a << " in foo" <<endl; //cout << "obj data addr: "<< a.data() << " in foo" <<endl; return a; } int main() { A a = foo(10); cout << "obj stack addr: "<< &a << " in main" <<endl; //cout << "obj data addr: "<< a.data() << " in main" <<endl; }
去掉NRVO用C++11编译。
g++ rvo.cpp -std=c++11 -fno-elide-constructors -o 11.out
执行:
./11.out
0x7ffcdca8fe80 construct
obj stack addr: 0x7ffcdca8fe80 in foo
0x7ffcdca8fec0 copy from 0x7ffcdca8fe80
0x7ffcdca8feb0 copy from 0x7ffcdca8fec0
obj stack addr: 0x7ffcdca8feb0 in main
可以看到由于我们自定义的类型A没有move语义,所以这里调用了拷贝构造函数,并且调用了两次。第一次是在foo函数内从具名的对象a,拷贝到临时变量作为返回值。第二次是从该返回值拷贝到main函数中的对象a。
我们来给他加上move构造函数:
class A { public: A() { cout << this << " construct " <<endl; _data = new int[size]; } A(const A& a) { cout << this << " copy from " <<&a <<endl; _data = new int[a._len]; for (size_t i = 0; i < a._len; i++) { this->_data[i] = a._data[i]; } } A(A&& a) { cout << this << " move data from " <<&a <<endl; _data = a._data; a._data = nullptr; // 或使用交换 // swap(_data, a._data); } ~A() { if (_data) { delete[] _data; } } ...
重新编译:
g++ rvo.cpp -std=c++11 -fno-elide-constructors -o 11.out
然后运行:
0x7ffe84ad74c0 construct
obj stack addr: 0x7ffe84ad74c0 in foo
0x7ffe84ad7510 move data from 0x7ffe84ad74c0
0x7ffe84ad7500 move data from 0x7ffe84ad7510
obj stack addr: 0x7ffe84ad7500 in main
可以看调用到了move构造函数。