【C++】 C++11:左值、右值,移动语义与完美转发

【C++】 C++11:左值、右值,移动语义与完美转发 目录一、左值与右值二、左值引用的短板三、右值引用与移动语义移动语义变“复制”为“窃取”移动构造与移动赋值示例四、强制“窃取”std::move() 的妙用五、完美转发与 std::forward1. 披着右值皮的“万能引用”2. 属性退化的痛点3. std::forward 完美转发总结核心考点在 C 的学习路线上C11 是一次极其重要的进化。相比于 C98/03C11 包含了约 140 个新特性能更好地用于系统开发和库开发提升程序员的开发效率。而在这些新特性中右值引用和移动语义无疑是最核心、也是最能带来性能飞跃的语法特性。一、左值与右值在了解引用之前我们必须先分清什么是“左值”什么是“右值”左值是一个表示数据的表达式如变量名或解引用的指针。我们可以获取它的地址可以对它赋值。左值可以出现在赋值符号的左边。右值也是一个表示数据的表达式如字面常量、表达式返回值、函数返回值等。右值不能出现在赋值符号左边不能取地址。inta10;// a 是左值10 是右值int*pnewint(0);// p 和 *p 是左值intba10;// a 10 的结果是一个右值二、左值引用的短板传统的 C 语法中就有引用的语法C11 中新增了右值引用语法特性因此我们之前学习的引用就叫做左值引用使用表示。无论左值引用还是右值引用都是给对象取别名。左值引用在做函数参数和做返回值时都可以提高效率。但是它有一个致命的短板当函数返回对象是一个局部变量出了函数作用域就不存在了就不能使用左值引用返回只能传值返回。stringto_string(intvalue){string str;// ... 将 value 转换为字符串放入 strreturnstr;// 出作用域 str 被销毁只能传值返回}传值返回会导致至少 1 次拷贝构造如果是一些旧一点的编译器可能是两次拷贝构造。对于包含大量堆内存数据的容器或字符串这种深拷贝极其拖累程序效率。三、右值引用与移动语义为了补齐左值引用的短板C11 提出了右值引用使用表示也就是对右值的引用给右值取别名。intrr110;doublerr2xy;移动语义变“复制”为“窃取”有了右值引用我们可以增加移动构造和移动赋值函数。移动构造本质是将参数右值的资源窃取过来占位已有那么就不用做深拷贝了。所以它叫做移动构造就是窃取别人的资源来构造自己从而提高效率。移动构造与移动赋值示例在自定义string类中移动构造和移动赋值是这样工作的// 拷贝构造深拷贝性能开销大string(conststrings):_str(nullptr){stringtmp(s._str);swap(tmp);}// 移动构造窃取资源快如闪电string(strings):_str(nullptr),_size(0),_capacity(0){// 直接调用 swap 交换指针接管资源swap(s);}// 移动赋值窃取资源stringoperator(strings){swap(s);return*this;}intmain(){bit::string retbit::to_string(1234);//调用移动构造bit::string ret1;ret1bit::to_string(1234);//调用移动构造和移动赋值return0;}当执行bit::string ret bit::to_string(1234);时编译器会识别出to_string的返回值是一个右值如果既有拷贝构造又有移动构造调用就会匹配调用移动构造没有新开空间拷贝数据所以效率提高了。当执行bit::string ret1; ret1 bit::to_string(1234);时我们看到调用了一次移动构造和一次移动赋值。因为如果是用一个已经存在的对象接收编译器就没办法优化了。bit::to_string函数中会先用str生成构造生成一个临时对象但是我们可以看到编译器很聪明的在这里把str识别成了右值调用了移动构造。然后在把这个临时对象做为bit::to_string函数调用的返回值赋值给ret1这里调用的移动赋值。STL中的容器都是增加了移动构造和移动赋值四、强制“窃取”std::move() 的妙用按照语法右值引用只能引用右值。但当我们需要用右值引用去引用一个左值实现移动语义时可以通过std::move()函数将左值转化为右值。C11 中的std::move()函数名字具有迷惑性它并不搬移任何东西唯一的功能就是将一个左值强制转化为右值引用然后实现移动语义。strings1(hello world);// 我们把 s1 move 处理以后会被当成右值调用移动构造strings3(std::move(s1));// 警告这里要注意一般不要这样用因为 s1 的资源被转移给了 s3s1 被置空了五、完美转发与 std::forward当我们把右值引用和模板结合在一起时会发生一些奇妙的化学反应。在这里我们必须要了解一个概念万能引用与完美转发。1. 披着右值皮的“万能引用”在模板中并不总是代表右值引用。模板中的是万能引用其既能接收左值又能接收右值。templatetypenameTvoidPerfectForward(Tt){// 这里的 t 既能接收左值也能接收右值}2. 属性退化的痛点模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力。但是这里存在一个巨大的坑引用类型的唯一作用就是限制了接收的类型后续使用中都退化成了左值。也就是说即使你给PerfectForward传了一个右值在函数内部t有了名字它可以被取地址它本身就变成了一个左值。如果我们在这时继续把t传给底层的其他函数底层函数永远只会把它当成左值处理原本的右值属性丢失了移动语义也就无法触发。3. std::forward 完美转发我们希望能够在传递过程中保持它的左值或者右值的属性就需要用完美转发。C11 提供了std::forward。std::forwardT(t)在传参的过程中保留对象原生类型属性如果你原来是左值传下去还是左值如果你原来是右值传下去依旧是右值。voidFun(intx){cout左值引用endl;}voidFun(intx){cout右值引用endl;}templatetypenameTvoidPerfectForward(Tt){// std::forwardT(t) 在传参的过程中保持了 t 的原生类型属性Fun(std::forwardT(t));}intmain(){inta10;PerfectForward(a);// a 是左值Fun 会打印 左值引用PerfectForward(std::move(a));// move(a) 是右值Fun 会打印 右值引用return0;}完美转发在 STL 的很多底层实现比如可变参数模板、emplace_back等中被大量使用它是 C11 高效泛型编程的基石。总结核心考点左值引用只能引用左值但const左值引用既可引用左值也可引用右值其短板是当函数返回局部对象时只能传值返回。右值引用只能引用右值但是可以引用 move 以后的左值。移动构造/移动赋值本质是将参数右值的资源窃取过来不用做深拷贝完美解决了传值返回导致的性能瓶颈。std::move()的唯一功能就是将一个左值强制转化为右值引用用于实现移动语义。