以下文章来源于雨乐聊编程,作者雨乐
今天我们聊聊Modern cpp的两个非常重要的概念移动语义和转发引用。
概念
值类别
在C++11之前,值类别分为左值和右值两种,但是自C++11起,引入了纯右值,消亡值两种。其中,左值和将亡值合称为泛左值,纯右值和将亡值合称为右值(C++11之前的右值等同于C++11中的纯右值)。因为本文目的不在于分析值类别,所以本文意义中的左值和右值就是字面意义上的左值右值。
右值(RVALUE),即立即创建和使用的临时值。在C++中,右值是与内存地址无关的表达式,这意味着其没有地址,也不能被修改。通常3、1.0以及std::string("abc")这种都属于右值。
PS:需要注意的是常量字符串"abc"等这种属于左值。
与右值相反,左值(LVALUE),其具有内存地址和可修改,其可以用于分配新值或者获取对象的地址。
可能有人有疑问,就是如何区分左值和右值,目前一个比较通用的判断方式就是:判断其是否可以取地址。
左值引用 & 右值引用
既然有左值和右值,那么相应的,也就存在左值引用和右值引用,常常如下这种表示:
inta=0; int&la=a; int&&r=3;
在上述示例中,a、la以及r都属于左值,其中la是左值引用,r是右值引用。
看下面一个例子:
#includevoidPrint(int&lref){ std::cout<< "Lvalue reference" << std::endl; } void Print(const int& lref) { std::cout << "const Lvalue reference" << std::endl; } void Print(int&& rref) { std::cout << "Rvalue reference" << std::endl; } int main() { int x = 5; const int y = 10; Print(x); // lvalue reference Print(y); // lvalue reference Print(20); // rvalue reference return 0; }
上述示例输出如下:
Lvaluereference constLvaluereference Rvaluereference
std::move
std::move是C++中的一个常用函数,它执行到右值引用的转换,允许您将左值转换为右值。这在您想要转移所有权或启用对象的移动语义的情况下非常有用。移动语义允许开发人员有效地将资源(如内存或文件句柄)从一个对象传输到另一个对象,而无需进行不必要的复制。
正如字面意义所理解的,移动语义允许将对象有效地从一个位置“移动”到另一个位置,而不是复制,这对于管理资源的对象特别有用。它实际上并没有移动任何东西;它只是将表达式的类型更改为右值引用。这允许调用移动构造函数或移动赋值运算符,而不是调用复制构造函数或复制赋值运算符。
gcc对move的实现如下:
templateinlinetypenamestd::remove_reference<_Tp>::type&& move(_Tp&&__t) {returnstatic_cast ::type&&>(__t);}
也就是说,其仅仅通过static_cast<>做了类型转换~
std::move仅仅将对象转换为右值引用,仅此而已
#include#include classObj{ public: Obj(){ std::cout<< "Default constructor "; } Obj(const Obj&) { std::cout << "Copy constructor "; } Obj(Obj&&) noexcept { std::cout << "Move constructor "; } Obj& operator=(Obj&& other) noexcept { std::cout << "Move assignment operator "; return *this; } }; int main() { Obj obj1; /* Default constructor */ Obj obj2 = std::move(obj1); /* Move constructor */ Obj obj3; obj3 = std::move(obj2); /* Move assignment operator */ return 0; }
输出如下:
Defaultconstructor Moveconstructor Defaultconstructor Moveassignmentoperator
在上述示例中:
•Obj1创建对象并调用构造函数•obj2是通过使用std::move移动obj1创建的,它调用移动构造函数•创建obj3并调用默认构造函数•当使用std::move将obj2移动到 obj3 时,将调用移动赋值运算符
在此示例中,使用std::move操作, obj1到obj2 以及 obj2到obj3调用的是移动的行为,这样可以提高性能,尤其是在移动大型数据结构或资源时。但是,重要的是要注意移动对象的状态及其拥有的资源。
#include#include classObj{ public: Obj(){ std::cout<< "Obj constructed" << std::endl; } ~Obj() { std::cout << "Obj destructed" << std::endl; } void fun() { std::cout << "in fun" << std::endl; } }; int main() { std::unique_ptr p1=std::make_unique (); std::unique_ptr p2=std::move(p1); if(p1){ std::cout<< "p1 is not empty" << std::endl; } p2->fun(); return0; }
在这个例子中,首先创建了一个类型为std::unique_ptr的指针p1,然后通过调用std::move()将p1的所有权转移至p2,接着判断p1是否为有效的指针,如果是则输出,接着p2调用fun()函数。
上述示例输出结果如下:
Objconstructed infun Objdestructed
从这个输出结果可以看出,通过std::move()将所有权从p1转移至p2后,p1不再持有任何资源。
std::forward
std::forward是 C++ 标准库中的一个函数模板,用于在模板函数中进行完美转发。它允许在模板函数中将参数转发到另一个函数,同时保持参数的值类别(value category)和 cv 限定符(const 或 volatile 限定符)不变。
std::forward通常与右值引用(&&)结合使用,用于转发传递给模板函数的参数。在模板函数内部,你可以使用std::forward来将参数转发给其他函数,并保持原始参数的性质。
示例如下:
#includevoidPrint(constint&lref){ std::cout<< "Lvalue reference" << std::endl; } void Print(int&& rref) { std::cout << "Rvalue reference" << std::endl; } template voidFun(T&¶m){ Print(std::forward (param)); } intmain(){ intx=5; constinty=10; Fun(x);//lvaluereference Fun(y);//lvaluereference Fun(20);//rvaluereference return0; }
在这个例子中,我们创建了一个模板函数Fun(),其参数类型为T&&,当使用左值调用Fun()时候,它将param作为左值进行转发,当使用右值调用Fun()时候,它将param作为右值进行转发,然后调用对应的函数,这样可保证在不损失真实类型的情况下调用正确的函数。
move vs forward
对于第一次接触这块知识点的开发人员来说,可能有点疑惑,是否可以用move来替代forward,我们且看一个例子,相信你看了之后就不会对这块一目了然:
#includevoidPrint(int&a){ std::cout<< "int&: " << a << std::endl; } void Print(int&& a) { std::cout << "int&&: " << a << std::endl; } template voidfunc1(T&&a){ Print(std::move(a)); } template voidfunc2(T&&a){ Print(std::forward (a)); } intmain(){ intarg=10; std::cout<< "Calling func1 with std::move()..." << std::endl; func1(arg); /* arg is an lvalue */ func1(25); /* 25 is an rvalue */ std::cout << "Calling func2 with std::forward()..." << std::endl; func2(arg); /* arg is an lvalue */ func2(25); /* 25 is an rvalue */ return 0; }
上述代码输出如下:
Callingfunc1withstd::move()... int&&:10 int&&:25 Callingfunc2withstd::forward()... int&:10 int&&:25
在上述代码中:
•创建了两个重载函数Print,其参数类型分别为**int &和int &&**,函数的功能是输出其参数的类型
•模板函数func1(),函数参数a为转发引用(T&&,也有地方称之为万能引用),函数体内调用参数为std::move(a)的Print()函数,将a转换为右值引用,这意味着,如果a是左值,则传递给Print()函数的参数类型为右值引用
•模板函数func2(),与模板函数func1()一样,该函数也采用转发引用(T&&)。但是,它使用 std::forward来保留a的原始值类别。这意味着如果a是左值,它将作为左值传递给Print()函数,如果它是右值,它将作为右值传递
•在 main() 中,使用左值和右值调用函数func1和func2,以观察对应的行为
通过上面输出,基本可以区分这俩,在此,做下简单的总结:
•目的
•std::forward:用于完全按照传递的参数转发,保留其值类别(左值或右值)
•std::move:用于将对象转换为右值引用,通常用于启用移动语义并转移所有权
•用法
•std::forward:通常用于转发引用(通用引用),以保留传递给另一个函数的参数的值类别
•std::move:用于将对象显式转换为右值引用
•影响
•std::forward:不更改参数的值类别。如果原始参数是右值引用,则它返回右值引用,否则返回左值引用
•std::move:将其参数转换为右值引用,将其值类别更改为右值
•安全
•std::forward:可以安全地与转发引用 (T&&) 一起使用,以确保正确转发参数,而不会产生不必要的副本。
•std::move:应谨慎使用,因为它可能会导致从其他地方仍需要的对象移动,从而导致未定义的行为
•场景
•std::forward:用于需要完美转发参数的场景,例如模板函数和类中。
•std::move:在显式转移所有权或调用移动语义时使用,例如从函数返回仅移动类型时
•返回类型
•std::forward:返回类型取决于传递给它的参数的值类别,它可以返回左值引用或右值引用。
•std::move:始终返回右值引用
-
内存
+关注
关注
8文章
3008浏览量
73918 -
字符串
+关注
关注
1文章
578浏览量
20493 -
函数
+关注
关注
3文章
4317浏览量
62487 -
C++
+关注
关注
22文章
2105浏览量
73531
原文标题:性能大杀器:std::move 和 std::forward
文章出处:【微信号:CPP开发者,微信公众号:CPP开发者】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
相关推荐
评论