-
std::move()
只是做了一次强制类型转换,将实参转成右值类型,并不做其他任何动作; -
如果想用
std::move()
实现真正的移动操作,那么传入的实参类型一定不能是常量类型,不然通过std::move()
强转出来的类型也仍然带有常量属性,导致调用构造函数时调用的是复制构造函数,而不是移动构造函数; -
当定义的函数的形参是右值类型的时候,只能传入一个右值给这个函数,不能传一个左值给这个函数;
-
std::move()
是无条件的将实参转成右值类型(当然会保留常量属性),而std::forward<T>()
一般跟模板函数的万能引用一起使用,并且仅当传给模板函数的实参是右值的时候,才会在模板函数内部将此实参对应的形参变量强转成右值的; -
在自定义的移动构函或者移动赋值操作中,一般需要主动使用
std::move()
来移动类中的数据成员,而不是简单的赋值; -
如果函数模板的形参具备
T&&
的形式,且T的类型是推导而来的,或者如果对象使用auto&&
声明的,则该形参或者该对象就是个万能引用。万能引用几乎可以绑定到任何类型的对象上,可以绑定到右值、左值、const对象或非const对象、volatile对象或非volatile对象以及既有const又有volatile修饰的对象上。如果绑定到万能引用上的是右值,则此万能引用就变成一个右值引用类型,如果万能引用绑定到一个左值上,则此万能引用就变成一个左值引用; -
注意,对于函数模板形参类型的万能引用(
T&&
),只有当T是编译器自动推导出来的情况时,其才是万能引用。如果是需要用户主动填写的,则仍旧是右值引用:template<typename T> void f(std::vector<T>&& param); // 不是万能引用,因为其形式不是 T&& template<typename T, typename Allocator = allocator<T>> class vector { public: void push_back(T&& x); // 不是万能引用,因为在vector使用的时候都会主动加上类型,这样最后此函数实例化出来时已经有类型了,不需要编译器推导出来 ...... template<typename... Args> void emplace_back(Args&&... args); // 是万能引用,因为这里的类型最终是由编译器推导出来的 };
-
一个使用
auto&&
的特别好的例子,记录任意函数调用所花费的时长(需要C++14支持):auto timeFuncInvocation = [](auto&& func, auto&&... params) -> decltype(auto) { // 此处计时器启动 auto res = std::forward<decltype(func)>(func)(std::forward<decltype(params)>(params)...); // 此处计时器停止,并记录流逝的时间 return res; };
-
针对右值引用使用
std::move
,针对万能引用使用std::forward
(即函数的形参是右值引用的时候,函数内部一般使用std::move
来处理这个右值引用的形参,如果模板函数的形参是万能引用,则一般在函数体内使用std::forward
来处理这个形参)。需要注意,右值引用变量本身是左值,所以一定要注意需要加上std::move
:class Widget { public: Widget(Widget&& rhs) : name(std::move(rhs.name)), p(std::move(rhs.p)) {} private: std::string name; std::shared_ptr<Data> p; };
注意到移动构函中新参是右值引用,但是在初始化成员时需要主动使用
std::move
,因为形参本身是一个左值。因此,一般情况下,右值引用的形参都需要传递给std::move
,然后再传给其他函数。万能引用与右值引用不同,万能引用只有在使用右值初始化时才会强转成右值类型:class Widget { public: template<typename T> void SetName(T&& newName) { name = std::forward<T>(newName); } ...... };
此处
newName
是一个万能引用,使用std::forward
进行转发。综上所述:当转发右值引用给其他函数时,使用std::move
进行转发,而当转发万能引用给其他函数时,使用std::forward
进行转发; -
如果类中有个函数需要基于左值或右值进行重载,则可以考虑使用万能引用+完美转发来实现:
class Widget { public: void SetName(const std::string& newName) { name = newName; } void SetName(std::string&& newName) { name = std::move(newName); } private: std::string name; };
上述
SetName()
进行了优化,这样当传入临时变量时就能够加速,提高效率。不过,如果此处使用万能引用,效率可能会更高,且代码量更少:...... class Widget { public: template<typename T> void SetName(T&& newName) { name = std::forward<T>(newName); } private: std::string name; };
这样,一个函数的效果等价于上面两个重载函数的效果。并且,如果有这样的代码:
Widget w; w.SetName("abc");
如果使用上面的重载的方案,则编译器可能要先创建一个临时的
std::string
变量,然后再将临时变量通过移动构造函数传给成员变量name
,然后临时变量再析构。如果使用的是万能引用,则编译器直接将字符串字面值传给name
的赋值运算符,不需要创建中间的临时std::string
,也就起到了加速效果。虽然在有的编译器上,使用第一种方案时会优化成类似与第二种方案,但是并不保证,且第二种方案可扩展性差。所以编写设置数据成员的成员函数的时候,可以多考虑使用万能引用+完美转发来实现,只不过可能维护性会降低,因为相对来说可读性会变差; -
在某些情况下,可能要需要使用
std::move_if_noexcept()
来代替std::move()
; -
如果函数返回的是值而不是引用,并且此函数的形参传入一个右值引用或者一个万能引用,而函数想返回这个右值引用或万能引用对象,则最好使用
std::move()
或者std::forward()
来返回; -
另外,如果函数是按值返回,且返回的是函数里面的一个局部对象,则千万不要使用
std::move()
或std::forward()
,直接返回值即可。因为编译器会自动进行返回值优化(RVO
),这样不仅可以避免复制操作,也能避免移动操作。如果非要返回std::move(局部变量)
,则由于将局部变量变成了引用,编译器无法执行RVO技术,所以就要额外创建临时对象并执行移动操作。即使编译器无法对局部变量执行RVO,那么编译器也会自动执行移动操作。如果你手动强制执行移动操作,那么就阻止了编译器可能进行的返回值优化动作。因此,函数返回局部变量时千万不要返回std::move()
或std::forward()
; -
RVO优化是直接不创建函数中的局部变量,而是直接在返回值的变量上执行操作,这样就不用返回对象了(连移动操作都省了)。执行RVO优化需要两个前提条件:局部对象类型和返回值类型相同、返回的就是局部对象本身。即使编译器无法执行RVO优化,比如一个函数中不同的控制路径返回不同的局部变量时(此时编译器无法得知要返回哪一个局部变量,因此无法执行RVO优化),编译器会自动执行移动操作(相当于自动加上
std::move
); -
重载的函数中最好不要使用万能引用来进行重载,因为万能引用几乎可以精确匹配任何类型,这样导致重载失效;
-
如果一个类的构造函数使用了模板(不是类使用模板,而是里面的构造函数使用了模板,特别是使用了万能引用的时候),即使实例化后此构造函数与复制操作或者移动操作具有相同的函数签名,此时编译器仍然能够为此类生成复制操作或者移动操作,并不会阻止编译器生成。这时候,如果使用一个非常量的此类的一个左值对象去初始化另一个对象,则会调用万能引用版本的构函,而不会调用编译器自动生成的复制构造函数或者移动函数:
class Person { public: template<typename T> explicit Person(T&& n) {...} // 万能引用构造函数 Person(const Person& rhs) {...} // 编译器自动生成的复制构造函数 Person(Person&& rhs) {...} // 编译器自动生成的移动构造函数 }; Person p1; const Person p2; Person p3(p1); // 这里会调用万能引用版本的构函 Person p4(p2); // 这里会调用编译器自动生成的复制构函
上述代码中,由于p1的类型是
Person
,因此更符合万能引用实例化出来的版本,而p2的类型是const Person
,因此更符合编译器生成的复制构函(虽然模板构函也能够实例化出跟复制构函一样的签名,但是标准规定:若在函数调用时,一个模板实例化函数和一个普通的非模板函数具有相等的匹配程度,则优先选用普通的非模板函数)。这样的话,就很可能出错,因为没有调用到你所设想的函数。因此创建模板构造函数的时候,一定要小心,它与编译器生成的版本具有错综复杂的关系,特别是在继承体系中,它们会劫持子类中对基类的复制和移动构函的调用; -
当传递给
std::is_intergral<T>
类型是任意的左值引用时,其结果为假,也就是说int&
并不是整形; -
一种能够解决万能引用模板函数重载导致函数匹配错误的技术是
标签分派
。即接口模板仍然使用万能引用形参,但是此接口函数内部会调用一个重载函数,重载函数会多一个参数,用来根据传入的类型区分到底要调用内部的哪一个重载函数:template<typename T> void InterfaceFunc(T&& param) { ImplFunc(std::forward<T>(param), std::is_integral<typename std::remove_reference<T>::type>()); } template<typename T> void ImplFunc(T&& param, std::false_type) {......} // 注意,下面的重载函数没有模板 void InplFunc(int i, std::true_type) {......}
比如上面的函数,接口中通过调用重载函数来实现精确匹配。如果传入的是整形,则会自动调用第二个实现函数,如果传入的是非整形,则调用的是第一个实现函数;
-
使用
std::decay<T>::type
可以将类型T
的所有引用、const以及volatile修饰属性给去掉,只留下原原本本的基本类型,它也可以将数组或函数类型转成指针类型; -
std::is_base_of<T1, T2>::type
中,如果T1是T2的基类,则值为true,如果T1和T2类型一样,则同样为true。所以std::is_base_of
的功能完全包含了std::is_same
,并且比后者功能更强大; -
形如
template<typename T, typename = typename std::enable_if<condition>::type>
的模板被称为禁用的。默认情况下,所有模板都是启用的,但是实施了std::enable_if
的模板只在满足了std::enable_if
指定的条件的前提下才会启用(即只有当condition
为true时才会启用,才能够实例化此模板)。使用此技术,搭配万能引用,就能够解决由于函数重载造成的万能引用模板函数匹配错误的情况。其实typename = ...
就是typename U = ...
,也就是加了一个默认模板形参; -
当类的构造函数需要使用万能引用的时候,使用上述的模板禁用技术可以解决函数匹配错误的问题,如果是普通的使用了万能引用技术的模板函数,也可以使用上面的
标签分派
计数来达到类似效果; -
不过,如果使用两种万能引用技术,则出错时不容易理解,所以一般情况下不使用万能引用技术。如果真的需要,可能还需要在模板函数里面通过
static_assert
来处理传入非法参数类型时报错的信息,有助于理解; -
对于
std::string
类来说,编译器有可能执行“小型字符串优化”(SSO技术)技术,即当字符串内容较少时(比如只有15个字符),不在堆上分配内存,而是直接在栈缓冲区上分配内存。此时移动操作并不会起到加速作用; -
如果想让自定义类的移动操作能够被充分使用,则一定要加上
noexcept
说明符,不然即使可移动,编译器可能也会使用复制操作。比如类型A的移动操作未加noexcept
,则std::vector<A>
对象在执行对应的移动操作的时候,为了保证强异常安全,会调用A类的复制操作,而不是移动操作。如果移动操作加上了noexcept
,编译器就可以放心的使用A的移动操作了; -
在类中声明这样的变量:
static const int m = 20;
,由于在类内声明的,所以这个语句不算是定义。理论上,需要在类外在定义一下(由于类内已经提供了初始值,类外定义的时候不能再提供初始值),这样其他地方使用此变量的指针或者引用时(包含万能引用),才可用(否则可以编译,但是会出现链接错误,因为没有为这个变量创建内存)。如果只是在类内像上面那样声明,而未在类外定义,则类外在用到这个变量的值(不是左值,不涉及内存和地址)时,编译器直接将字面值20传过去,此时编译器可能根本不会为变量m创建一个内存,编译器会直接将对应的字面值替换过去; -
万能引用(及配套的完美转发)无法适用的几种情况:
- 大括号初始化;
- 0或NULL用作空指针;
- 仅有声明(没有定义)的const static成员变量;
- 重载的函数名字或模板名字;
- 位域;
对于第一种情况,不能直接将大括号初始化物传递给万能引用的形参,必须要单独创建一个变量(是
std::inltializer_list<T>
类型的,可用auto创建),然后将此变量作为实参传递给万能引用的形参:template<typename T> void func(T&& param) {......} void func1(const std::vector<int>& param) {......} auto il = {1, 2, 3}; func(il); // 正确 func({1, 2, 3}); // 错误,编译器无法推导,除非有一个func重载函数的形参是std::inltializer_list<T>的 func1({1, 2, 3}); // 正确,编译器能自动推导 func1(il); // 正确
对于第二种情况,由于使用0或NULL时,编译器会自动推导成整型,而不是指针,所以就会推导出错误的类型,此时应该使用nullptr。
对于第三种情况,如果未定义整形的static const成员变量,则编译器不会产生存储空间,因此无法对其获取指针或者绑定引用,而万能引用是需要引用的(硬件层面引用跟指针是几相同的),所以此时要把这种成员变量作为实参传给万能引用的形参,则必须要在类外定义一下这个变量。
对于第四种情况,由于重载函数或者模板函数同一个名称对应多个函数,所以函数名称传给万能引用时,编译器无法推导出到底是要传入哪个函数版本:void f( int (*pf)(int) ) {......}; // 函数f的形参是函数指针 template<typename T> void f1(T&& param) {......} int process(int v); int process(int v, int p); f(process); // 正确,编译器能够根据f的形参类型来推导出到底传入哪一个process f1(process); // 错误,编译器无法推导出调用哪一个process using funcType = int(*)(int); funcType p1 = process; f1(p1); // 正确,主动指明了传入的实参类型 f1(static_cast<funcType>(process)); // 正确,同上
第五种情况下,由于位域无法通过引用绑定或者无法获取地址,所以无法直接将位域作为实参传递给万能引用。这种情况直接将位域赋值给正常的一个变量,然后传递正常变量即可。
C++学习笔记-右值引用、移动语义和完美转发
最新推荐文章于 2024-05-11 20:08:32 发布