左值和右值
左值:左值是具有标识符(变量名)的表达式,在内存中有一个确定的地址,可以被赋值,它可以是变量,对象,返回引用的函数等
int x=10; //x是左值
int &ref=x; //ref也是左值
右值:右值通常位于等号的右侧,通常是一个临时的,无标识符的值例如字面常量,临时对象,函数返回的对象等,无法对其取地址,赋值的操作
int x=10;
int y=20; //右值10和20是字面常量
int z=x+y; //x+y是一个表达式他产生的结果是一个临时值
int w=result() //如果函数返回的是一个临时对象的话同样也为右值
左值引用和右值引用
实际应用:
- 左值引用通常是避免对象的拷贝,保持对原始变量的操作,函数返回值等
- 右值引用是实现移动语义和完美转发
左值引用
- 左值引用用&声明
- 相当于起别名与其引用对象拥有同一块内存
- 函数传参为左值引用时可以保持对原始数据的操作,并不会发生拷贝
右值引用
- 右值引用使用&&符号声明,用于引用右值
- 右值引用在移动语义中发挥重要作用,允许将资源的所有权转而不进行拷贝
- 实现完美转发
右值引用的应用
移动语义
移动语义是一种语言特性,它允许对象的资源在转移所有权时不进行深拷贝,而是移动资源的指针
来提高效率
- 一般是通过右值引用实现
- 当处理临时对象时可以通过移动语义将资源转换给另一个对象
class my_array {
public:
my_array(): value_(nullptr) {} //默认构造
my_array(int *k): value_(k) {} //有参构造
my_array(my_array &&other) : value_(other.value_) { //移动构造
other.value_ = nullptr; // 将源对象的资源置为空
}
my_array &operator=(my_array &&other) { // 移动赋值运算符重载
if (this != &other) {
//避免自我赋值
delete[] value_; // 移动赋值运算符重载
value_ = other.value_; // 转移资源的所有权
other.value_ = nullptr; // 将源对象的资源置为空
}
return *this;
}
private:
int *value_; // 指向动态分配的数组的指针
};
在上面我们可以看到移动语义不再进行深拷贝,而是通过int指针来转移资源
特别的,当我们需要将一个旧对象赋值给新对象时且旧对象不再使用时,我们可以使用std::move()函数实现
int ch[3]={1,2,3};
my_array pre(ch);
/*出于某种原因pre不再使用但是资源需要保存*/
my_array now=std::move(pre)
std::move函数接收一个左值然后返回该左值的右值引用(返回的是一个右值,虽然是右值引用但是资源已经转移),从而发生了右值引用的移动语义,可以看出这大大提高了效率
引用折叠规则
引用折叠无非就是下面四种
- & & 被推导为&
- & && 被推到为&
- && & 被推导为&
- && && 被推导为&&
完美转发
我们在了解完美转发前先了解一下万能引用
什么是万能引用?
万能引用不是一种引用类型而是处理左右值时返回相应的左值引用和右值引用,万能引用有以下两种场合
- 用在模板中
template<typename T>
void tempFun(T&& k) {} //T && 是万能引用最常见的使用场合
2.auto推断类型
auto&& ref2= ref1; //auto这种需要推断类型的地方
接下来我们用代码解释下万能引用
template<typename T>
void tempFun(T&& t)
{
t = 40;
cout << t << endl;
}
int main()
{
int x = 19;
tempFun(x); //T为int&, t为int & ,即得到左值引用
tempFun(30);//T为int, t为int&&,即得到右值引用
int &&r = 100;
tempFun(r); //虽然r绑定到一个右值,但r变量本身是个左值 即得到左值引用
return 0;
}
由于编译器只能判断左右值无法判断是左值还是右值引用,如果是左值T推导为 int&然后和 &&发生折叠导致t为int&类型,右值的话T推导为int所以t为int&&,所以通过传左右值都得到了相应的引用
回到主题我们再来看段代码猜测一下运行结果
#include <iostream>
using namespace std;
template<typename T>
void tempFun(T &&t)
{
cout << "右值引用" << endl;
}
template<typename T>
void tempFun(T &t)
{
cout << "左值引用" << endl;
}
template<typename T>
void select1(T &&t)
{
tempFun(t);
}
template<typename T> //实现完美转发
void select2(T &&t)
{
tempFun(forward<T>(t));
}
void test()
{
int x = 1;
select1(x);
select1(1);
select1(move(x));
}
void test1()
{
int x = 1;
select2(x);
select2(1);
select2(move(x));
}
int main()
{
test();
cout << "***************" << endl;
test1();
return 0;
}
下面是运行结果:
可以看出在select1中传递,左值,右值,右值,都得出了左值引用,因为后两种情况T被推导为int那么t就是int&&(右值引用),t是有名字得一个局部变量所以它是左值,所以执行左值引用的函数
那么如何解决这种情况呢,可以看到我们的select2中有forward函数就可以解决了
forward函数
template<typename T>
T&& forward(T&& arg) noexcept {
return static_cast<T&&>(arg);
}
当你传入得原始实参得到得是一个左值时,那么 T 就会推导为相应得T&(int->int&),右值T则是T(int->int)forward通过T来保持相应的属性
T为T&的时候保持其t为原始参数为左值的特性,T为T时保持其t为原始值为右值的属性从而实现左右值引用的转发从而实现完美转发