本文参考:右值引用 | 爱编程的大丙
转移和完美转发 | 爱编程的大丙
左值、右值、左值引用、右值引用
左值 是指存储在内存中、有明确存储地址(可取地址)的数据;
右值 是指可以提供数据值的数据(不可取地址);
左值引用必须接收左值,同理右值引用必须接收右值,否则会报错
左值常量引用和右值常量
在讲左值常量引用和右值常量引用之前,先讲讲什么是常量引用?
常量引用是一种特殊的引用,声明了一个常量引用后,不可以通过此引用去修改原本变量的值了,但是直接修改原变量还是依然可以的。
常量右值引用接收的值必须是右值,注意,当常量右值引用进行传递就会变成左值,即编译器会将已命名的右值引用视为左值,将未命名的右值引用视为右值。所以下面代码中 const int&& e = b; 会产生报错,因为此时b为左值
常量左值引用是一个万能的引用类型,它可以接收各种类型的数据,包括左值、右值、左值引用、右值引用、常量左值引用、常量右值引用。
int main()
{
//左值: 可以取地址的指
int num = 9;
//左值引用
int& a = num;
int& l = 9; //error,必须接收左值
//右值:不可以取到地址的指
8;
//右值引用
int&& b = 8;
int&& c = b; //error,必须接收的是右值
//常量右值引用
const int&& d = 6;
const int&& e = b; //error,必须接收的是右值
//常量左值引用
const int& f = num; //常量左值引用可以等于左值
const int& j = 8; //常量左值引用可以等于右值
const int& k = a; //常量左值引用可以等于左值引用
const int& g = b; //常量左值引用可以等于右值引用
const int& h = f; //常量左值引用可以等于常量左值引用
const int& i = d; //常量左值引用可以等于常量右值引用
//所以称常量左值引用是一个万能的类型,可以接收各种类型的数据
}
移动构造函数
右值引用的重要作用之一 —— 移动构造函数,带来性能优化
在C++中在进行对象赋值操作的时候,很多情况下会发生对象之间的深拷贝,如果堆内存很大,这个拷贝的代价也就非常大,在某些情况下,如果想要避免对象的深拷贝,就可以使用右值引用进行性能的优化。
若不使用移动构造函数:
#include <iostream>
using namespace std;
class Test
{
public:
Test() : m_num(new int(100))
{
cout << "construct: my name is jerry" << endl;
}
Test(const Test& a) : m_num(new int(*a.m_num))
{
cout << "copy construct: my name is tom" << endl;
}
~Test()
{
delete m_num;
}
int* m_num;
};
Test getObj()
{
Test t;
return t;
}
int main()
{
Test t = getObj();
cout << "t.m_num: " << *t.m_num << endl;
return 0;
};
输出结果:vs2019 (vs2022中已被优化)
construct: my name is jerry
copy construct: my name is tom
t.m_num: 100
通过输出的结果可以看到调用 Test t = getObj(); 的时候调用拷贝构造函数对返回的临时对象进行了深拷贝得到了对象t,在getObj()函数中创建的对象虽然进行了内存的申请操作,但是没有使用就释放掉了。
如果能够使用临时对象已经申请的资源,既能节省资源,还能节省资源申请和释放的时间,如果要执行这样的操作就需要使用右值引用了,右值引用具有移动语义,移动语义可以将资源(堆、系统对象等)通过浅拷贝从一个对象转移到另一个对象这样就能减少不必要的临时对象的创建、拷贝以及销毁,可以大幅提高C++应用程序的性能。
#include <iostream>
using namespace std;
class Test
{
public:
Test() : m_num(new int(100))
{
cout << "construct: my name is jerry" << endl;
}
Test(const Test& a) : m_num(new int(*a.m_num))
{
cout << "copy construct: my name is tom" << endl;
}
// 添加移动构造函数
Test(Test&& a) : m_num(a.m_num)
{
a.m_num = nullptr;
cout << "move construct: my name is sunny" << endl;
}
~Test()
{
delete m_num;
cout << "destruct Test class ..." << endl;
}
int* m_num;
};
Test getObj()
{
Test t;
return t;
}
int main()
{
Test t = getObj();
cout << "t.m_num: " << *t.m_num << endl;
return 0;
};
输出结果:vs2019 (vs2022中已被优化)
construct: my name is jerry
move construct: my name is sunny
destruct Test class ...
t.m_num: 100
destruct Test class ...
通过修改,在上面的代码给Test类添加了移动构造函数(参数为右值引用类型),这样在进行Test t = getObj();操作的时候并没有调用拷贝构造函数进行深拷贝,而是调用了移动构造函数,在这个函数中只是进行了浅拷贝,没有对临时对象进行深拷贝,提高了性能。
在测试程序main()中getObj()的返回值就是一个将亡值,也就是说是一个右值,在进行赋值操作的时候如果=右边是一个右值,那么移动构造函数就会被调用。移动构造中使用了右值引用,会将临时对象中的堆内存地址的所有权转移给对象t,这块内存被成功续命,因此在t对象中还可以继续使用这块内存。
对于需要动态申请大量资源的类,应该设计移动构造函数,以提高程序效率。
关于&&
并不是所有情况下 && 都代表是一个右值引用,具体的场景体现在模板和自动类型推导中,如果是模板参数需要指定为T&&,如果是自动类型推导需要指定为auto &&,在这两种场景下 &&被称作未定的引用类型。
通过右值推导 T&& 或者 auto&& 得到的是一个右值引用类型
通过非右值(右值引用、左值、左值引用、常量右值引用、常量左值引用)推导 T&& 或者 auto&& 得到的是一个左值引用类型
move
使用std::move方法可以将左值转换为右值。使用这个函数并不能移动任何东西,而是和移动构造函数一样都具有移动语义,将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存拷贝。
其主要有两个作用
1、将左值转换为右值,用于初始化右值引用
class Test
{
public:
Test(){}
......
}
int main()
{
Test t;
Test && v1 = t; // error
Test && v2 = move(t); // ok
return 0;
}
2、一个对象内部有较大的堆内存或者动态数组时,使用move()可以方便的进行数据所有权的转移
list<string> ls;
ls.push_back("hello");
ls.push_back("world");
......
list<string> ls1 = ls; // 需要拷贝, 效率低
list<string> ls2 = move(ls);
forward 完美转发
在 C++11 引入,并被用于解决函数模板中的参数传递问题。
forward 用于在一个函数模板中将参数按照原始的值类别(左值或右值)转发给另一个函数。这意味着 forward 会保留参数传递时的左值或右值属性,使得接收函数能够正确地处理这些参数。
std::forward<T>(t);
当T为左值引用类型时,t将被转换为T类型的左值
当T不是左值引用类型时,t将被转换为T类型的右值