理解std::move和std::forward

  1. 移动语义使得编译器得以使用不那么昂贵的移动操作,来替换昂贵的复制操作。移动语义也使得创建只移类型对象成为可能,这些类型包括std::unique_ptr和std::future和std::thread等。
  2. 完美转发使得人们可以撰写接受任何实参的函数模板,并将其转发到其他函数,目标函数会接受到与转发函数所接受的完全相同的实参。
  3. 形参总是左值,即使其类型是右值引用。比如void f(Widget&& w);,形参w是个左值,即使它的类型是个指向Widget类型对象的右值引用。
  4. std::move并不进行任何移动,std::forward也不进行任何转发。它俩都是仅仅执行强制类型转换的函数(其实就是函数模板),std::move无条件地将实参强制转换成右值,而std::forward则仅在某个特定条件满足时才执行同一个强制转换。std::move将实参强制转换成了右值,这就是该函数的全部所作所为。
  5. 由于右值是可以移动的,所以在一个对象上实施了std::move就是告诉编译器该对象具备可移动的条件,它简化了对象是否可移动的表述。
  6. 为了维持常量正确性,C++不允许常量对象传递到有可能改动它们的函数(例如移动构造函数)而指向常量的左值引用允许绑定一个指向右值类型的实参,因此通常会调用复制构造函数而非移动构造函数。
  7. 如果想取得对某个对象执行移动操作的能力,则不要将其声明为常量,因为针对常量对象执行的移动操作将一声不响地变换成复制操作。std::move不仅不实际移动任何东西,甚至不保证经过其强制类型转换的对象具备可移动能力(对象为常量即不可移动)。
  8. std::forward是一个有条件的强制类型转换,其最常见使用场景就是某个函数模板使用了万能引用类型为形参,随后将其传递给另一个函数。由于所有形参都是左值,std::move提供了一种机制,当且仅当用来初始化形参的实参是个右值的条件下把形参强制转换成右值类型,这就是std::move干的一切。
  9. 当传入实参为左值时,std::forward并不会进行强制类型转换,所以我们通常在构造移动构造函数时需要用到std::move,而不是std::forward。
  10. 在运行期,std::move和std::forward都不会做任何操作。

区分万能引用和右值引用

  1. “T&&”有两种不同的含义,其中一种含义是右值引用,它们仅仅会绑定到右值,其主要的存在理由在于识别出可移对象,另一种含义则表示其既可以是右值引用(T&&),亦可以是左值引用(T&),形如右值引用T&&,但可以像左值引用T&一样运作。这种双重特性既可以绑定到右值(右值引用),也可以绑定到左值(左值引用),可称为万能引用。
  2. 万能引用会在两种场景下现身,最常见一种场景是函数模板的形参,比如
     template<typename T>
     void f(T&& param);    //param是个万能引用
    

    第二种场景是auto声明auto&& var2=var1;//var2是个万能引用,这个场景的共同之处在于它们都涉及类型推导。

  3. 万能引用的初始化物会决定它代表的是个左值还是右值引用,如果初始化物为左值,万能引用就会对应到一个左值引用,如果初始化物为右值,万能引用就会对应到一个右值引用。
  4. 若要使一个引用成为万能引用,其涉及类型推导是必要条件,但还不是充分条件,引用声明的形式也必须正确无误,并且该形式被限定得很死:必须得正好形如“T&&”才行。
     template<typename T>
     void f(std::vector<T>&& param);//param是个右值引用,不是万能引用,不能给右值引用绑定一个左值
     template<typename T>
     void f(const T&& param);//param是个右值引用
    

    即使是一个const修饰词,也会剥夺一个引用成为万能引用的资格。只有涉及类型推导的函数模板中的T&&才是万能引用。声明为auto&&类型的变量都是万能引用,因为它们肯定涉及类型推导和正确的形式。

针对右值引用实施std::move,针对万能引用实施std::forward

  1. 当转发右值引用给其他函数时,应当对其实施右值的无条件强制类型转换(通过std::move),因为它们一定绑定到右值;而当转发万能引用时,应该对其实施向右值的有条件强制类型转换(通过std::forward),因为它们不一定绑定到右值。
  2. 针对右值引用使用std::forward也能弄出正确行为,但是代码啰嗦易错且不符合习惯用法,所以应当避免针对右值引用使用std::forward,另一方面针对万能引用使用std::move则更为糟糕,因为那样做的后果是某些左值会遭到意外改动(例如某些局部变量)。
  3. 为了解决针对万能引用使用std::move所带来的问题,可以使用依左值和右值的重载,这带来的问题不仅是代码膨胀或者对习惯用法的背离,还有运行期的效率,最严重的问题是这种设计的可扩展性太差,有n个形参就需要2n。因此针对左值右值进行重载并不可行,万能引用才是唯一的解决之道。
  4. 如果想要在单一函数内将某个对象不止一次地绑定到右值引用或者万能引用,而且想保证完成对该对象的其他所有操作之前其值不被移走,在这种情况下我们就得在最后一次使用该引用时,对其实施std::move(右值引用)或者std::forward(万能引用)。在少数我们确定不会有异常抛出时,我们可以使用std::move_if_noexcept来代替std::move。
  5. 在按值返回的函数中,如果返回的是绑定到一个右值引用或万能引用的对象,则当你返回该引用时,应该对其实施std::move或者std::forward。如果该类型支持移动构造则比复制构造效率更高,如果不支持移动构造将其转换成右值也并无大碍,右值也会通过对象的复制构造函数来完成复制。对于万能引用和std::forward来说情况也类似。
  6. 但是上述优化不能扩展到欲返回的局部变量上,不能通过将“复制”转换为移动来“优化”一个按值返回局部变量的函数。因为本身C++标准中的返回值优化(return value optimization,RVO)就可以通过直接再为函数返回值分配的内存上创建局部变量w来避免复制之。
  7. 编译器若要在一个按值返回的函数里省略对局部对象的复制(或者移动),则需要满足两个前提条件:
    ①局部对象类型和函数返回值类型相同
    ②返回的就是局部对象本身
  8. 返回右值引用或万能引用的移动版本的按值返回的函数不满足上述②里的条件,调用std::move或std::forward返回的是局部对象的引用,因此无法进行RVO优化,编译器必须把局部变量移入函数返回值存储位置。
  9. 当RVO的前提条件允许时,要么发生复制省略,要么std::move隐式地被实施于返回的局部对象上。因此,针对函数中按值返回的局部对象实施std::move的操作不能给编译器帮上忙,反而有可能帮倒忙。

避免依万能引用类型进行重载

  1. 形参为万能引用的函数,是C++中最贪婪的,它们会在具现过程中,和几乎任何实参都会产生精确匹配,因此一旦万能引用成为重载候选,他就会吸引走大批的实参类别。
  2. 类中如果撰写一个带万能引用的构造函数,并不会阻止编译器同时生成复制和移动构造函数,因此可能在试图调用复制构造函数和移动构造函数时会产生调用形参为万能引用的模板函数从而引发异常。
  3. C++重载决议规则中规定:若在函数调用时,一个模板实例化函数和一个非函数模板具备相等的匹配程度,则优先选用常规函数。
  4. 完美转发构造函数的问题尤为严重,因为对于非常量的左值类型而言,它们一般都会形成相对于复制构造函数的更佳匹配,并且它们还会劫持派生类中对基类的复制和移动构造函数的调用。