个人认为最核心的变化是右值引用的引入,这是C++现代化最重要的一步。建议每一位C++开发者都应该深刻理解并充分利用它。右值引用是C++11中最重要的新特性之一。它解决了C++中大量的历史问题,使C++标准库的实现能够消除各种场景下不必要的额外开销(如std::vector、std::string),也使得一些其他的标准库(如std::unique_ptr,std::function)可能。即使您不直接使用右值引用,您也可以通过标准库间接地从这个新特性中获益。为了更好的理解标准库结合右值引用带来的优化,我们有必要了解一下右值引用的意义。右值引用的含义通常被解释为两个主要角色:移动语义和完美转发。本文重点介绍移动语义。1.Move语义Move语义,简单来说就是解决对象在各种情况下的资源所有权转移问题。在C++11之前,移动语义的缺乏是C++中最受诟病的问题之一。例如。问题一:如何将大象放入冰箱?答案是众所周知的。首先你需要有一个专门用来装大象的冰箱。你打开冰箱门,把大象放进冰箱,然后关上冰箱门。问题二:如何将大象从一台冰箱转移到另一台?常见答案:打开冰箱门,取出大象,关上冰箱门,再打开另一扇冰箱门,把大象放进去,关上冰箱门。2B方案:启动第二台冰箱的量子复制系统,克隆一头一模一样的大象,然后启动高能激光,将第一台冰箱的大象气化消失。等等,这个2B解决方案听起来很熟悉,当你想移动一个对象时,这不就是C++所做的吗?“动”,这是一个三岁小孩都懂的概念。将大象(资源)从一个冰箱(对象)移动到另一个冰箱(对象)的行为是如此自然,以至于没有人会诉诸先复制然后销毁大象的不可思议的方法。C++通过拷贝构造函数和拷贝赋值运算符为类设计了拷贝/复制的概念,但要实现资源的移动操作,调用者必须采用先拷贝后析构的方式。否则,您需要自己实现移动资源的接口。为了实现移动语义,首先需要解决的问题是如何识别可以移动的对象的资源?这种机制必须以最小开销的方式实现,并且适用于所有类。C++的设计者注意到,在大多数情况下,右值包含的对象可以安全地移动。右值(和对应的左值)是自从C语言设计以来就存在的概念,但因为它们太基础了,所以也是最常被忽视的概念。笼统地说,左值对应变量的存储位置,右值对应变量本身的值。C++中的右值可以分配给左值或绑定到引用。类的右值是一个临时对象,如果它没有绑定到引用,它会在表达式末尾被丢弃。所以我们可以在右值被丢弃之前移除它的资源以进行浪费利用,从而避免无意义的复制。被移除资源的右值在被丢弃时变成了一个空壳,销毁的开销也会减少。右值中的数据可以安全移动这一事实允许使用右值来表达移动语义。从相同类型的右值构造对象时,需要通过引用传入参数。顾名思义,右值引用专门用于引用右值。可以分别重载左值引用和右值引用,从而保证调用左值和右值进行语义上的复制和移动。对于左值,如果我们明确放弃其资源的所有权,则可以通过std::move()将其转换为右值引用。std::move()实际上是static_cast()的简单包装器。右值引用至少可以解决以下场景下运动语义缺失的问题:1.按值传递参数按值传递参数是人类最一致的思维方式。其基本思想是,如果传递参数的目的是将资源交给函数接收者,那么参数应该按值传递。同时,按值传递参数兼容任何cv限定的左值和右值,这是最兼容的方式。classPeople{public://按值传入字符串,可以接收左值和右值。//接收到左值时复制,接收到右值时移动People(stringname):name_(move(name))//显式移动构造,将传入的字符串移动到一个成员变量中{}stringname_;};Peoplea(“爱丽丝”);//移动构造名称字符串bn="Bob";Peopleb(bn);//构造bycopy构造名时,调用一个stringconstructor和一个stringmove构造函数。如果用conststring&name来接收参数,就会有构造函数、拷贝构造和非平凡的析构函数。尽管看起来很痛苦,而且尽管编译器进行了优化,但按值传递参数在语义上是最优的。如果你想在构造函数中接收std::shared_ptr并将其存储为类的成员(这很常见),那么按值传入是唯一的选择。复制std::shared_ptr需要线程同步,与移动std::shared_ptr相比非常容易和愉快。2、按值返回和接收输入参数一样,按值返回返回值也是最符合人的思维方式。曾经有无数的函数必须这样写才能返回一个容器:://由值语义定义的字符串拆分函数voidstr_split(conststring&s,vector*vec);这里不考虑分隔符,假设分隔符是固定的。这就需要在外部预先构造好vec,此时没办法知道vec的大小。即使函数内部有一种方法可以预测vec的大小,但由于函数不负责构建vec,因此可能仍然需要调整大小。调用这样的嵌套函数就更痛苦了,谁用谁知道。使用移动语义,它可以这样写:vectorstr_split(conststring&s){vectorv;//...返回v;//v是一个左值,但是先移动,不支持移动,仍然可以被复制。}如果函数按值返回,return语句直接返回栈上的左值对象(入参除外),标准要求先调用move构造函数,不匹配则调用copy构造函数。尽管``v是一个左值,移动语义仍然是首选,并且返回向量`从此变得轻而易举。此外,无论移动还是复制,编译器优化仍会尽可能应用,但语义不受影响。对于std::unique_ptr,这简直就是福音。unique_ptrcreate_obj(/*...*/){unique_ptrptr(newSomeObj(/*...*/));ptr->foo();//一些可能的初始化returnptr;}当然还有更简单的形式:unique_ptrcreate_obj(/*...*/){returnunique_ptr(newSomeObj(/*...*/));}在工厂类中,这个的语义非常普遍。返回unique_ptr可以明确转移构造对象的所有权。特别是,可以忽略这种工厂类的返回值而不会导致内存泄漏。以上两种形式分别返回栈上的左值和右值,但都应用了移动语义(unique_ptr不支持复制)。3.当接收到一个没有移动语义的右值表达式时,用表达式的值初始化对象(例如函数调用)或者像这样给对象赋值:vectorstr_split(conststring&s);//返回的vector用于复制构造对象v,为v申请堆内存,复制数据,然后销毁临时对象(释放堆内存)。矢量<字符串>v=str_split("1,2,3");vectorv2;//将返回的向量复制到对象v(复制赋值运算符)。需要先清理v2中的原始数据,将临时对象中的数据复制到v2中,然后销毁临时对象。v2=str_split("1,2,3");注意:v的复制构造调用可能被优化掉了,但语义上仍然存在复制操作。相同的代码,只是在支持移动语义的世界中更好。vectorstr_split(conststring&s);//返回的vector用于移动构造的对象v,v直接带走临时对象堆上的内存,不需要重新申请。之后临时对象就变成了一个空壳,不再有任何资源,销毁时也不需要释放堆内存。矢量<字符串>v=str_split("1,2,3");vectorv2;//返回的向量移动到对象v(移动赋值运算符)。先释放v2的原始数据,然后直接从返回值中取数据,然后销毁返回值。v2=str_split("1,2,3");注意:v的move构造调用可能被优化掉了,但是语义上仍然有move操作。不言而喻,上述形式是多么普遍和自然。而且,根本没有显式使用右值引用,性能提升是默默实现的。4.容器中存储对象的问题和前面的构造函数传参问题类似。不同的是这里的参数是通过两种引用来传递的。参见std::vector的push_back函数。voidpush_back(constT&value);//(1)voidpush_back(T&&value);//(2)不用说了,左值调用1,右值调用2。如果你想把非常大的对象放在容器里,那么版本2自然不是选择。矢量<矢量<字符串>>vv;矢量<字符串>v={"123","456"};v.push_back("789");//将临时构造的字符串类型右值移入容器vvv.push_back(move(v));//困扰多年的显式将v移入vv的不为人知的秘密是否已经过去了?5.std::vector的增长是另一个隐藏的优化。当vector的存储容量需要增加时,通常会重新申请一块内存,将原来的内容逐个复制并删除。是的,复制删除,用move代替就够了。对于像vector这样的容器,当频繁插入导致存储容量不可避免地增加时,移动语义可以带来安静而漂亮的优化。6.std::unique_ptr被放入容器中。由于向量增长时对象将被复制,因此不能将不可复制的对象(如std::unique_ptr)放入容器中。但实际上vector并不复制对象,它只是“移动”它们。所以随着move语义的引入,把std::unique_ptr放到std::vector中是理所当然的事。将std::unique_ptr存储在容器中有很多好处。想必大家都写过这样的代码:MyObj::MyObj(){for(...){vec.push_back(newT());}//...}MyObj::~MyObj(){for(vector::iteratoriter=vec.begin();iter!=vec.end();++iter){if(*iter)删除*iter;}//...}乏味话虽这么说,异常安全也是一个大问题。使用vector>,根本不需要显式销毁,unqiue_ptr会搞定一切。完全不用写析构函数的感觉,你做到了吗?unique_ptr是一个非常轻量级的包,它的存储空间相当于一个裸指针,但是它的安全性却强了一个世纪。需要共享所有权的对象(指针)在实践中比较少见,但需要转移所有权是很常见的。auto_ptr的垮台是其转移所有权的繁琐操作。unique_ptr可以轻松解决带有移动语义的所有权转移问题。注意:如果确实需要共享所有权,那么引用计数的shared_ptr是一个不错的选择。shared_ptr也可以移动。移动shared_ptr比复制更轻,因为不需要线程同步。7.std::thread线程的转移也是典型的无法复制的资源,但可以通过移动转移所有权。同样,std::futurestd::promisestd::packaged_task等多线程类也不能复制,也可以通过移动传递。2.完美转发除了移动语义,右值引用还解决了C++03中引用语法不能转发右值的问题,实现了完美转发,使得std::function有了优雅的实现。这部分不再展开。3.小结Move语义绝不是语法糖,而是对C++的一次深刻革新。Move语义不仅仅针对库作者,任何程序员都需要了解它。尽管您可能不会主动为您的类实现移动语义,但您无时无刻不在享受移动语义带来的好处。所以它绝不意味着这是一个可选的东西。所以,如果你想写出优雅的ModernC++代码,你应该多使用右值引用,喜欢它,拥抱它。