今天我们聊聊Modern cpp的两个非常重要的概念移动语义和转发引用。
概念
值类别
在C++11之前,值类别分为左值和右值两种,但是自C++11起,引入了纯右值,消亡值两种。其中,左值和将亡值合称为泛左值,纯右值和将亡值合称为右值(C++11之前的右值等同于C++11中的纯右值
)。因为本文目的不在于分析值类别,所以本文意义中的左值和右值就是字面意义上的左值右值。
右值(RVALUE),即立即创建和使用的临时值。在C++中,右值是与内存地址无关的表达式,这意味着其没有地址,也不能被修改。通常3、1.0以及std::string("abc")这种都属于右值。
PS:需要注意的是常量字符串"abc"等这种属于左值。
与右值相反,左值(LVALUE),其具有内存地址和可修改,其可以用于分配新值或者获取对象的地址。
可能有人有疑问,就是如何区分左值和右值,目前一个比较通用的判断方式就是:判断其是否可以取地址。
左值引用 & 右值引用
既然有左值和右值,那么相应的,也就存在左值引用和右值引用,常常如下这种表示:
int a = 0;
int &la = a;
int &&r = 3;
在上述示例中,a、la以及r都属于左值,其中la是左值引用,r是右值引用。
看下面一个例子:
#include <iostream>
void Print(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;
}
上述示例输出如下:
Lvalue reference
const Lvalue reference
Rvalue reference
std::move
std::move是C++中的一个常用函数,它执行到右值引用的转换,允许您将左值转换为右值。这在您想要转移所有权或启用对象的移动语义的情况下非常有用。移动语义允许开发人员有效地将资源(如内存或文件句柄)从一个对象传输到另一个对象,而无需进行不必要的复制。
正如字面意义所理解的,移动语义允许将对象有效地从一个位置“移动”到另一个位置,而不是复制,这对于管理资源的对象特别有用。它实际上并没有移动任何东西;它只是将表达式的类型更改为右值引用。这允许调用移动构造函数或移动赋值运算符,而不是调用复制构造函数或复制赋值运算符。
gcc对move的实现如下:
template<typename _Tp>
inline typename std::remove_reference<_Tp>::type&&
move(_Tp&& __t)
{ return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }
也就是说,其仅仅通过static_cast<>做了类型转换~
std::move仅仅将对象转换为右值引用,仅此而已
#include <iostream>
#include <utility>
class Obj {
public:
Obj() {
std::cout << "Default constructor\n";
}
Obj(const Obj&) {
std::cout << "Copy constructor\n";
}
Obj(Obj&&) noexcept {
std::cout << "Move constructor\n";
}
Obj& operator=(Obj&& other) noexcept {
std::cout << "Move assignment operator\n";
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;
}
输出如下:
Default constructor
Move constructor
Default constructor
Move assignment operator
在上述示例中:
•Obj1创建对象并调用构造函数•obj2是通过使用std::move移动obj1创建的,它调用移动构造函数•创建obj3并调用默认构造函数•当使用std::move将obj2移动到 obj3 时,将调用移动赋值运算符
在此示例中,使用std::move操作, obj1到obj2 以及 obj2到obj3调用的是移动的行为,这样可以提高性能,尤其是在移动大型数据结构或资源时。但是,重要的是要注意移动对象的状态及其拥有的资源。
#include <iostream>
#include <memory>
class Obj {
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<Obj> p1 = std::make_unique<Obj>();
std::unique_ptr<Obj> p2 = std::move(p1);
if (p1) {
std::cout << "p1 is not empty" << std::endl;
}
p2->fun();
return 0;
}
在这个例子中,首先创建了一个类型为std::unique_ptr
上述示例输出结果如下:
Obj constructed
in fun
Obj destructed
从这个输出结果可以看出,通过std::move()将所有权从p1转移至p2后,p1不再持有任何资源。
std::forward
std::forward
是 C++ 标准库中的一个函数模板,用于在模板函数中进行完美转发。它允许在模板函数中将参数转发到另一个函数,同时保持参数的值类别(value category)和 cv 限定符(const 或 volatile 限定符)不变。
std::forward
通常与右值引用(&&
)结合使用,用于转发传递给模板函数的参数。在模板函数内部,你可以使用 std::forward
来将参数转发给其他函数,并保持原始参数的性质。
示例如下:
#include <iostream>
void Print(const int& lref) {
std::cout << "Lvalue reference" << std::endl;
}
void Print(int&& rref) {
std::cout << "Rvalue reference" << std::endl;
}
template<typename T>
void Fun(T&& param) {
Print(std::forward<T>(param));
}
int main() {
int x = 5;
const int y = 10;
Fun(x); // lvalue reference
Fun(y); // lvalue reference
Fun(20); // rvalue reference
return 0;
}
在这个例子中,我们创建了一个模板函数Fun()
,其参数类型为T&&
,当使用左值调用Fun()时候,它将param作为左值进行转发,当使用右值调用Fun()时候,它将param作为右值进行转发,然后调用对应的函数,这样可保证在不损失真实类型的情况下调用正确的函数。
move vs forward
对于第一次接触这块知识点的开发人员来说,可能有点疑惑,是否可以用move来替代forward,我们且看一个例子,相信你看了之后就不会对这块一目了然:
#include <iostream>
void Print(int& a) {
std::cout << "int&: " << a << std::endl;
}
void Print(int&& a) {
std::cout << "int&&: " << a << std::endl;
}
template <typename T>
void func1(T&& a) {
Print(std::move(a));
}
template <typename T>
void func2(T&& a) {
Print(std::forward<T>(a));
}
int main() {
int arg = 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;
}
上述代码输出如下:
Calling func1 with std::move()...
int&&: 10
int&&: 25
Calling func2 with std::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:始终返回右值引用
以上~~