百科问答小站 logo
百科问答小站 font logo



C++ make_tuple返回值类型,会有效率问题吗? 第1页

  

user avatar   guodongxiaren 网友的相关建议: 
      

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构造函数。




  

相关话题

  怎样理解C++中“如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题”? 
  c#中虽然异步和多线程是两码事,但是是否异步微软提供的async函数内部还是多线程去实现的? 
  C++ 有多难? 
  如果你们想要自创一个编程语言,你们希望它有什么特色? 
  什么时候用C而不用C++? 
  C++ 是否适合做 GUI? 
  C++ 中,std::atomic 是真正的「原子」吗? 
  对同一个套接字,在两个线程中都对其调用recv函数,需要对其互斥保护吗? 
  为什么有些编程语言写完一句后要加分号? 
  为什么下面程序递归计算斐波那契数列java比c++要快? 

前一个讨论
猛犸象灭绝了多少万年了?
下一个讨论
为什么不少老师们说喜欢学生提问,可是学生问多他解答不了的问题,他就会说这个学生爱钻牛角尖?





© 2024-12-26 - tinynew.org. All Rights Reserved.
© 2024-12-26 - tinynew.org. 保留所有权利