C++ 右值,右值引用以及万能引用

右值和右值引用

左值和右值指的是表达式的属性。
一般而言,一个左值表达式表示的是一个对象的身份,而右值表达式表示的是一个对象的
在C++11 中,为了支持移动操作,引入了右值引用

int a = 1;//a是左值
int &lr = a;//正确,左值引用lr绑定左值a
int &lr1 = a + 1;//错误,左值引用lr1绑定右值a+1
const int &lr2 = a + 1;//正确,可以将一个const的引用绑定到右值上,不管这个引用是左值引用还是右值引用
int &&rr1 = a + 1;//正确,右值引用rr1绑定到右值a+1
int &&rr = a;//错误,右值引用rr绑定到左值a

不能将右值表达式绑定到左值引用上,但有一个例外,就是const引用,它可以绑定右值。
右值引用就完全相反,他只能与右值表达式绑定。

左值持久,右值短暂

左值有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。
右值引用只能绑定右值,而右值又是即将被销毁的,没有其他用户的对象。
这是移动操作的基础。

移动构造函数和移动赋值运算符的形参就是右值引用,把临时对象的资源移动到*this对象,而不另外拷贝。

移动构造和移动赋值

假设f()函数返回一个很大的vector,那么在main函数中赋值给ret的时候,我们想移动而不是拷贝它,该怎么做?
答案是使用std::move();

vector<int>f() {
	vector<int>ret({ 1,2,3,4 });
	return ret;
}
int main()
{
	//vector<int>ret = f();//调用vector的拷贝构造函数
	vector<int>ret=std::move(f());//调用vector的移动构造函数
}

万能引用

万能引用(universal reference)并不是C++的语法特性,而是我们利用现有的C++语法,自己实现的一个功能。因为这个功能既能接受左值类型的参数,也能接受右值类型的参数。所以叫做万能引用。

如果一个函数模板形参的类型为T&&,且T为推导类型,或者一个对象被声明为auto&&,那么形参或对象是万能引用。
如果类型定义不是T&&,或者T的推导并未发生,那么T&&为右值引用。 万能引用绑定在左值上时表现为左值引用,绑定在右值上时表现为右值引用。

void f(Widget&& param); // rvalue reference
Widget&& var1 = Widget(); // rvalue reference
auto&& var2 = var1; // universal reference
template<typename T>
void f(std::vector<T>&& param); // rvalue reference
template<typename T>
void f(T&& param); // universal reference

成为万能引用的一个必要条件就是要发生类型推导,下面并没有发生类型推导,所以不是万能引用。

void f(Widget&& param); // no type deduction;
 // param is an rvalue reference
Widget&& var1 = Widget(); // no type deduction;
 // var1 is an rvalue reference

另一个必要条件是类型为T&&

template<typename T>
void f(std::vector<T>&& param); // rvalue reference

当函数被调用时,类型T会被推导,但参数param的类型声明并非T&&,而是std::vector<T>&&,因此param也不是万能引用。
引用折叠
当两个引用符号叠在一起的时候,如int & &&,编译器会将引用符号折叠,折叠规则如下:
仅当前后两个都是&&时,折叠的结果是右值引用,否则为左值引用。

完美转发
好了,有了万能引用。当我们既需要接收左值类型,又需要接收右值类型的时候,再也不用分开写两个重载函数了。那么,什么情况下,我们需要一个函数,既能接收左值,又能接收右值呢?

答案就是:转发的时候。

于是,我们马上想到了万能引用。又于是兴冲冲的改写了以上的代码如下:

#include <iostream>
using namespace std;

// 万能引用,转发接收到的参数 param
template<typename T>
void PrintType(T&& param)
{
	f(param);  // 将参数param转发给函数 void f()
}

// 接收左值的函数 f()
template<typename T>
void f(T &)
{
	cout << "f(T &)" << endl;
}

// 接收右值的函数f()
template<typename T>
void f(T &&)
{
	cout << "f(T &&)" << endl;
}

int main(int argc, char *argv[])
{
	int a = 0;
	PrintType(a);//传入左值
	PrintType(int(0));//传入右值
}

我们执行上面的代码,按照预想,在main中我们给 PrintType 分别传入一个左值和一个右值。PrintType将参数转发给 f() 函数。f()有两个重载,分别接收左值和右值。

正常的情况下,PrintType(a);应该打印f(T&),PrintType(int());应该打印f(T&&)。

但是,真实的输出结果是
f(T &);
f(T &);
为什么明明传入了不同类型的值,但是void f()函数只调用了void f(int &)的版本。这说明,不管我们传入的参数类型是什么,在void PrintType(T&& param)函数的内部,param都是一个左值引用!

没错,事实就是这样。当外部传入参数给 PrintType 函数时,param既可以被初始化为左值引用,也可以被初始化为右值引用,取决于我们传递给 PrintType 函数的实参类型。但是,当我们在函数 PrintType 内部,将param传递给另一个函数的时候,此时,param是被当作左值进行传递的。 任何的函数内部,对形参的直接使用,都是按照左值进行的。

具体的做法就是,将模板函数void PrintType(T&& param)中对f(param)的调用,改为f(std::forward(param)):
std::forward通常是用于完美转发的,它会将输入的参数原封不动地传递到下一个函数中,这个“原封不动”指的是,如果输入的参数是左值,那么传递给下一个函数的参数的也是左值;如果输入的参数是右值,那么传递给下一个函数的参数的也是右值。

template<typename T>
void PrintType(T&& param)
{
	f(std::forward<T>(param));  // 将参数param转发给函数 void f()
}

输出如下:

f(T &);
f(T &&);
嗯,完美的转发!

那么,std::forward是怎么做到的呢。

std::forward的源码形式大致是这样:

/*
 *  精简了标准库的代码,在细节上可能不完全正确,但是足以让我们了解转发函数 forward 的了
 */ 

template<typename T>
T&& forward(T &param)
{
	return static_cast<T&&>(param);
}

我们来仔细分析一下这段代码:

我们可以看到,不管T是值类型,还是左值引用,还是右值引用,T&经过引用折叠,都将是左值引用类型。也就是forward 以左值引用的形式接收参数 param, 然后将param进行强制类型转换 static_cast<T&&> (),最终再以一个 T&&返回

所以,我们分析一下传递给 PrintType 的实参类型,并将推导的 T 类型代入 forward 就可以知道转发的结果了。

(1)传入 PrintType 实参是右值类型:
假设为 int 。那么,将T = int 代入forward。

int&& forward(int &param)
{
	return static_cast<int&&>(param);
}

param在forward内被强制类型转换为 int &&, 然后按照int && 返回,两个右值引用最终还是右值引用。最终保持了实参的右值属性,转发正确。
假设为int &&,引用特性被忽略,从而推断T为int(关于T为什么会被推导为int,在我的另一篇博客中有讲https://blog.csdn.net/xxxxxzz123/article/details/115910899),之后同上。
(2)传入 PrintType 实参是左值类型:
万能引用对左值有特殊处理,T将被推导为左值引用类型,假设为int&。那么,将T = int& 带入forward。

int& && forward(int& &param)
{
	return static_cast<int& &&>(param);
}

引用折叠之后就是:

int& forward(int& param)
{
	return static_cast<int&>(param);
}

传递给 PrintType 左值,forward返回一个左值引用,保留了实参的左值属性,转发正确。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值