带你一起探索 c++11 中右值引用、移动构造、&&、move、forward
本文将介绍带你一步步的了解 c++11 中:
右值、右值引用
移动构造函数
&& 解密
move 移动语义
forward 完美转发
产生原由
class Object{public://无参构造函数Object() : m_num(new int(10)){std::cout << "contr function..." << std::endl;printf("m_num 地址:%p\n", m_num);}//拷贝构造函数Object(const Object& o) : m_num(new int(*o.m_num)){std::cout << "copy contr function..." << std::endl;}private:int* m_num;};

getObj 函数初始化一个对象 oo1, 分析其执行过程:getObj 函数中初始化一个临时对象 temp, 调用构造函数;oo1, 调用拷贝构造函数;class Object{...}Object getObj() {//1、初始化一个临时对象 temp, 调用构造函数;Object temp;return temp;}int main() {//2、将临时对象赋值给 `oo1`, 调用拷贝构造函数;Object oo1 = getObj();return 0;};
contr function...m_num 地址:00E7F6D8copy contr function...
getObj时,创建的临时对象 temp 在构造过程中如果要进行大量的初始化工作(特别耗时),并且其用完后将被释放;第二步中,将临时对象拷贝给 oo1 时,也需要进行大量的拷贝工作。oo1 在生命周期结束后也将释放。temp 只起到赋值作用,极大的耗费性能。有什么方式可以避免产生这个中间的临时变量呢?怎么去优化它呢?c++ 11 的右值引用。右 值
int x = 1000;int y = 2000;x = y;
右值引用
int&& data = 1000; //必须进行初始化class Object{public:Object(){std::cout << "contr function..." << std::endl;}Object(const Test& a){std::cout << "copy contr function..." << std::endl;}};Object getObj(){return Object();}int main(){int a1;int &&a2 = a1; // errorObject& t = getObj(); // errorObject && t = getObj();const Object& t = getObj();return 0;}
int &&a2 = a1;a1 具有名字,其为左值,左值赋值给右值引用 错误
Object& t = getObj();getObj 函数返回一个没有名字的右值,将一个右值赋值给左值引用 错误
Object && t = getObj();右值赋值给右值引用 正确
const Object& t = getObj();常量左值引用被成为万能引用,既可以引用左值也可以引用右值 正确
性能优化
中间产生了临时对象temp只起到赋值作用,极大的耗费性能。有什么方式可以避免产生这个中间的临时变量呢?怎么去优化它呢?class Object{...}Object getObj() {//1、初始化一个临时对象 temp, 调用构造函数;Object temp;return temp;}int main() {//2、将临时对象赋值给 `oo1`, 调用拷贝构造函数;Object oo1 = getObj();return 0;};
getObj 函数中创建的临时对象(堆上)构建完成后,还没有使用,就释放掉了,那么如果可以复用这个临时对象,将会对性能有很大帮助。class Object{...//移动构造函数Object(Object&& o) {m_num = o.m_num;o.m_num = nullptr;std::cout << "move contr function..." << std::endl;}private:int* m_num;}Object getObj() {//1、初始化一个临时对象 temp, 调用构造函数;Object temp;return temp;}int main() {//2、 调用移动构造函数Object oo1 = getObj();return 0;};
contr function...m_num 地址:00CCF5B0move contr function...
//移动构造函数Object(Object&& o) {m_num = o.m_num;o.m_num = nullptr;std::cout << "move contr function..." << std::endl;}...Object oo1 = getObj();
Object oo1 = getObj(); 这条语句是,调用移动构造函数将临时对象 temp 所指的内存直接赋值给 oo1 对象的指针(m_num = o.m_num;); 然后避免 temp 出了作用域销毁内存,则将 temp 指向的内存置空(o.m_num = nullptr;)。oo1 对象直接拥有了 构造 temp 时分配的内存。
右值符号 && 解密
在很多代码模板函数中经常会出现诸如以下的代码:
1、 static_cast<typename remove_reference<_Ty>::type&&>(_Arg)2、 typename<class T> void function(T&& t)
代码中的 && 会不会让你晕头转向?如果是,那么和我一起解密吧!
c++ 中有一种叫做未定义的引用类型,通常有以下两种方式:
自动类型推导的 auto&&
模板类型推导的 T&&
有一种特列 const  T&&, 表示右值引用,不属于未定义类型引用。
那么接下来记住两个规则即可,不对,是一个规则(引用折叠):
使用右值推导 T&& 和 auto&& 得到的是一个右值引用类型;其他的都是左值引用类型。
int a = 10;int b = 250;auto&& x = a; //a 是一个左值, auto&& 表示左值引用auto&& y = 100; //100 是右值, auto&& 表示右值引用int&& a1 = 5;auto&& b1 = a1; //a1是右值引用,不是右值,所以b1 是左值引用int a2 = 10;int& a3 = a2;//a2是左值,a3为左值引用auto&& c1 = a3;// a3是左值引用, c1 即是左值引用类型auto&& c2 = a2; // a2是左值, c2 即是左值引用类型const int& d1 =3;const int&& d2 = 4;auto&& e1 = d1; //d1是常量左值引用, e1 即为常量左值引用auto&& e2 = d2; //d2是常量右值引用, e2 即为常量左值引用
void printX(int &x){std::cout << "l-value: " << x << std::endl;}void printX(int &&x){cout << "r-value: " << x << endl;}void forward(int &&x){printX(x);}int main(){int a = 100;printX(a);printX(20);forward(500);return 0;system("pause");};
上面定义了两个重载函数 printX, 先对上面的输出结果进行推导:
1、 printX(a); 其中 a 是一个左值,那么调用第一个 printX函数,输出应该是左值;
2、 printX(20); 其中 20 是右值, 那么调用第二个printX函数, 输出应该是右值;
3、 forward(500), 其中 500 是右值,forward 形参x 是右值引用类型,继续调用printX,由于此时的右值具备的名字,所以将退化成一个左值,所以将调用第一个 printX 函数, 输出应该是左值;
        对于最后以重情况可能稍微难以理解,只需要记住,右值引用在传递的过程中将会退化成左值引用。这也是 std::forward 为了防止退化成左值引用,所以才出现的,被誉为完美转发 ,即不做任何变动的转发。将上述 forward 函数中的 printX(x)改成 printX(std::forward<int>(x)) 将会调用第二个函数,输出应该是右值。
输出结果:
l-value: 100r-value: 20l-value: 500
结果完全正确。
std::move
在上述的例子中,有一种情况是不能进行赋值的,即用一个左值初始化一个右值引用;
int a = 10;int&& b = a; //error 无法将右值引用绑定到左值
std::move 函数应运而生, move 通常被理解为“移动”, 但在本文中被译为“转移”更加合适,即转移所有权,将你的房产名字转给你的老婆,房子本身不变,只是所有者发生了变化。再来看 std::move 的源代码:
template<class _Ty> inline_CONST_FUN typename remove_reference<_Ty>::type&&move(_Ty&& _Arg) _NOEXCEPT{// forward _Arg as movablereturn (static_cast<typename remove_reference<_Ty>::type&&>(_Arg));}
使用 std::move 后,上述代码可以更改为:
int a = 10;int&& b = std::move(a); //ok
对于这种将左值转化为右值的方法有什么用处呢?
vector<string> vec;vec.push_back("wang");vec.push_back("zhuo");.....//插入一百万条数据.....vector<string> vec1 = vec;vector<string> vec2 = std::move(vec);
如果用 vec 这个左值直接初始化 vec1,将会发生大量的内存拷贝。
如果用 vec2 = std::move(vec), 直接将 vec 的所有权转移给 vec2即可。
用处?在对于拥有大量的堆内存或者动态数组时候,使用 std::move 可以有效的节省时间效率。
如果将 std::move 和 移动构造函数结合起来,尽可能重复利用资源, 移动构造函数接收的是一个右值引用类型。
std::forward
上文也提及到了完美转发 forward,即在右值引用传递的过程中,为了防止被编译器当作左值处理,使其以原由类型进行转发,引入了 forward。
原型如下:
std::forward<T>(x);当forward 的模板类型参数 T 为左值引用类型时,x将被转换成 T类型的左值,否则将被转换成右值。
template<typename T>void printX(T& t){std::cout << "left value"<< std::endl;}template<typename T>void printX(T&& t){std::cout << "rifht value " <<std::endl;}template<typename T>void test(T && v){printX(v);printX(move(v));printX(forward<T>(v));}int main(){test(100);int num = 10;test(num);test(forward<int>(num));test(forward<int&>(num));test(forward<int&&>(num));return 0;}
test(100), 100 是右值,test形参是未定义引用类型,即根据上文提到的 ”使用右值推导 T&& 和 auto&& 得到的是一个右值引用类型“ 形参 v 是一个具有名字的右值引用,但编译器将其视为左值。传递给第一个函数
printX变为左值,调用第一个,输出左值。使用
move(v)之后,左值 v 被 move 成右值,输出右值。printX(forward<T>(v)), T为右值引用,因此最终将会成为右值, 输出右值
2、test(forward<int&>(num));  模板参数为int&, 根据 ”当forward 的模板类型参数 T 为左值引用类型时,x将被转换成 T类型的左值“ , 将会得到一个左值,test 形参为未定义类型 T&&, 根据 ”使用右值推导 T&& 和 auto&& 得到的是一个右值引用类型,反之为左值引用“,即test 形参为左值引用类型。
printX(v);v 是左值, 输出左值printX(move(v));左值经过move后成为右值,输出右值printX(forward<T>(v));T类型为int&,v将被转换成左值, 输出左值。
你学会了吗?其他的几种留给你自己分析。
