右值引用和emplace 这篇文章初衷是好奇push_back
和emplace_back
的区别,了解之后发现绕不开右值引用,在此一并记录一下。
右值引用 右值引用(rvalue reference
)是C++11中引入的新特性,显然是与C++11之前普通左值引用相对的一个概念。下面的右值引用的介绍很多参考自这篇文章 。
左值与右值 一个粗略的定义如下:
左值是一个可以出现在赋值符号(=
)左边或者右边的表达式, 可以理解为对一块内存的引用;
右值是一个只能出现在赋值符号右边的表达式, 注意右值不是对内存的引用,因此不能进行取地址操作。
如:
1 2 3 4 5 6 7 8 9 10 11 int a = 42 ;int b = 43 ;a = b; b = a; int c = a + b;a + b = 42 ; a = foo(); int * p = &foo();
为什么要右值引用? move语义 假设类X中包括一个指向资源的指针m_pResource
,我们想实现一个接收临时对象 作为参数的拷贝赋值操作符,其实现可能如下:
1 2 3 4 5 X& X::operator =(const X &rhs) { }
不难看出,上面对资源m_pResource
的拷贝和析构十分低效,我们可以直接和临时实例交换指针(此所谓move语义)。另外,对于非临时对象的拷贝,我们可能不想析构其资源。因此我们需要一个类型来标识这样的临时对象,对它进行单独的处理,例如:
1 2 3 X& X::operator=( <Desired type>rhs) { //1. swap this->m_pResource and rhs.m_pResource }
其实右值引用就是我们想要的类型,上面代码中的<Desired type>
我们可以用X&&
替代,表示是X的右值引用。
emplace_back or push_back? 一个小实验 猜猜看下面的代码会进行几次复制操作?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <iostream> #include <vector> struct Point { float x, y; Point(float x, float y):x(x), y(y){} Point(const Point& point ):x(point .x), y(point .y) { std ::cout << "copying!" << std ::endl ; } }; int main () { std ::vector <Point> points; points.push_back(Point(1 , 2 )); points.push_back(Point(3 , 4 )); points.push_back(Point(5 , 6 )); return 0 ; }
答案是六次,其中三次是将临时Point
对象拷贝至容器,有一次是容器容量从1到2的拷贝,有两次是容器容量从2到4的拷贝。
消除扩容拷贝 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <iostream> #include <vector> struct Point { float x, y; Point(float x, float y):x(x), y(y){} Point(const Point& point ):x(point .x), y(point .y) { std ::cout << "copying!" << std ::endl ; } }; int main () { std ::vector <Point> points; points.reserve(3 ); points.push_back(Point(1 , 2 )); points.push_back(Point(3 , 4 )); points.push_back(Point(5 , 6 )); return 0 ; }
现在只用进行三次插入容器时的拷贝
move语义消除插入容器时的拷贝 直接将参数forward给容器,直接在容器中构造。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <iostream> #include <vector> struct Point { float x, y; Point(float x, float y):x(x), y(y){} Point(const Point& point ):x(point .x), y(point .y) { std ::cout << "copying!" << std ::endl ; } }; int main () { std ::vector <Point> points; points.emplace_back(1 , 2 ); points.emplace_back(3 , 4 ); points.emplace_back(5 , 6 ); return 0 ; }
现在拷贝的次数为0次!
接口 首先看看C++11中两者的接口:
1 2 3 4 5 template < class ... Args >void emplace_back ( Args &&... args );void std ::vector <T, Allocator>::push_back( const T& value );void std ::vector <T, Allocator>::push_back( T&& value );
注意到接收右值的接口的差别,push_back只能接收Vector中存储类型的右值作为参数,而emplace可以接收变长模板作为参数,尝试为变长模板找到最合适的构造函数直接在容器中构建。
NOTE:实验中的例子,emplace_back的变长参数也可以接收Point
对象,此时会先调用Point的默认构造函数然后调用复制构造函数。对应代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <iostream> #include <vector> struct Point { float x, y; Point(float x, float y):x(x), y(y){} Point(const Point& point ):x(point .x), y(point .y) { std ::cout << "copying!" << std ::endl ; } }; int main () { std ::vector <Point> points; points.reserve(3 ); points.emplace_back(Point(1 , 2 )); points.emplace_back(Point(3 , 4 )); points.emplace_back(Point(5 , 6 )); return 0 ; }