首页 > 编程语言 >MordernC++之左值(引用)与右值(引用)

MordernC++之左值(引用)与右值(引用)

时间:2023-04-09 16:57:20浏览次数:50  
标签:std 右值 int 左值 Mordern 引用 &&

左值与右值

C++中左值与右值的概念是从C中继承而来,一种简单的定义是左值能够出现再表达式的左边或者右边,而右值只能出现在表达式的右边

int a = 5; 	// a是左值,5是右值 
int b = a;	// b是左值,a也是左值
int c = a + b;	// c是左值,a + b是右值

另一种区分左值和右值的方法是:有名字、能取地址的值是左值,没有名字、不能取地址的值是右值。比如上述语句中a,b, c是变量可以取地址,所以是左值,而5和a + b无法进行取地址操作,因此是右值。C++中左值与右值的一个主要的区别是:左值可以被修改,而右值不可修改

左值引用与右值引用

了解了左值与右值的概念后,接下来介绍下C++中的左值引用与右值引用。左值引用很简单,就是一个变量的别名,绑定到一个左值上:

int a = 1;
int& b = a; 	//a = 1,b = 1
b = 2;		// a = 2,b = 2

这里b就等于a,在汇编层面其实和普通的指针一样,对引用的修改(b)也会修改到被引用的对象(a),需要注意的是,因为引用实际是一个别名,因此必须初始化,即告诉编译器是那个具体对象的别名。因此下列左值引用都是错误的:

int& a;		// 错误!左值引用必须初始化
int& b = 10;	// 错误!左值引用不能以临时变量初始化(临时变量没有地址)

右值引用是C++11中新增的特性,顾名思义,右值引用就是用来绑定到右值的引用,一个右值被绑定到右值引用之后,原本需要被销毁的此右值生命周期会延长至绑定它的右值引用的生命周期。在汇编层面,右值引用和const引用所做的事情是一样的,即产生临时量来存储常量。但是右值引用可以进行读写操作,而const引用只能进行读操作。绑定右值引用使用&&,具体使用如下:

int a = 5;
int& b = a;		// 正确!b是一个左值引用
int&& c = 6;		// 正确!c是一个右值引用,绑定到右值6
int&& d = a * 2;	// 正确!d是一个右值引用,绑定到右值a * 2
int&& e = i;		// 错误!不能将左值绑定到右值引用
int& f = 7;		// 错误!不能将右值绑定到左值引用
const int& g = a * 3;	// 正确!可以将右值绑定到const 左值引用

可以看到我们虽然不能将右值绑定到左值引用,但是可以将右值绑定到const左值引用
注意: 变量表达式都是左值!。变量可以看作是只有一个运算对象而没有运算符的表达式,跟其他表达式一样,变量表达式也有左值/右值属性。变量表达式都是左值,因此我们不能将一个右值引用绑定到一个右值引用类型的变量上。

int&& a = 5;	// 正确!a是一个右值引用
int&& b = a;	// 错误!a是一个左值,不能绑定到右值引用

这里虽然a是右值引用类型,但是确实一个左值,因此无法绑定到右值引用b上。因为在C++中,右值一般是临时对象,但是绑定到右值引用之后,其生命周期变长了,因此a是一个左值。我们不能将一个右值引用直接绑定到一个变量上,即使是这个变量是右值引用类型也不行。具体的这个问题在后续的介绍forward的时候会详细说明。

左值/右值引用的模板实参推断

在另一篇文章中介绍了C++的模板类型推断的几种类型,可以总结为以下三种:

  1. ParamType 是一个指针或者引用(&),但是不是通用引用(&&)
  2. ParamType是一个通用引用(&&)
  3. ParamType既不是指针,也不是引用(&)或者通用引用(&&)

从左值引用函数参数推断类型

当一个函数参数是模板的左值引用(T&)时,根据绑定规则,只能传递一个左值实参,这个左值实参可以时const类型,也可以不是。如果实参时const的,那么T就会被推导为const类型

template<typename T>
void func(T& param);

int a = 0;
const int b = a;
func(a);	// T被推导为int,param类型为int&
func(b);	// T被推导为const int,param类型为const int&
func(5);	// 错误!实参必须是一个左值!

如果一个函数的类型时const T&,那么根据绑定规则,可以传递任何类型的实参:const或者非const,左值或者右值,由于函数类型本身已经是const,因此T的推导结果不会是一个const,因为const已经是函数参数类型的一部分了。

template<typename T>
void func(const T& param);

int a = 0;
const int b = a;
func(a);	// T被推导为int,param类型为const int&
func(b);	// T被推导为int,param类型为const int&
func(5);	// 正确!const T&可以绑定一个右值,T为int

可以看到,当函数参数类型为const T&时,可以接受一个右值实参,而函数参数类型为 T& 时是不可以的。

从右值引用函数参数推断类型

当一个函数的参数是一个右值引用(T&&)时,根据绑定规则可以传递一个右值实参。类似左值引用推导,右值引用推导得到的T的类型为右值的类型:

template<typename T>
void func(T&& param);

func(5);	// 实参5为右值,T被推导为int类型

与不能给右值引用赋值左值不同,右值引用函数的模板实参却可以接受一个左值的输入。当我们将一个左值传递给函数的右值引用参数时,且此右值引用指向模板参数类型(T&&)时,编译器推导模板类型参数为实参的左值引用类型:

template<typename T>
void func(T&& param);

int a = 1;
func(a);	// T被推导为int&,而不是int

如上述推导所示,当传入一个左值a时,T被推导为int&,而不是int,对应的param的类型为int& &&,根据引用折叠的规则,int& &&被折叠为int&。

引用折叠规则
T& & ,T& && 和T&& &都会被折叠为T&
T&& &&被折叠为T&&

引用折叠的规则告诉我们:如果一个函数的参数时指向模板参数类型的右值引用(如T&&),则可以传递给它任意类型的实参,如果传递的左值实参,那么T将会推导成为一个左值引用,函数参数被实例化为一个普通的左值引用(T&)。这种引用叫做“通用引用”

右值引用与通用引用

C++中T&&有两种不同的意思,第一种是右值引用,用于绑定到右值上,它们主要存在的原因是为了声明某个对象可以被移动。T&&的第二层意思是,它既可以是一个右值引用,也可以是一个左值引用。这种引用在代码里看起来像是右值引用(T&&),又可以表现的像是左值引用(T&)。它既可以绑定到右值,也可以绑定到左值,还可以绑定到const和no_const对象上,几乎可以绑定到任何东西,这种引用叫做“通用引用”。在两种情况下会出现通用引用,最常见的就是函数模板参数:

template<typename T>
void func(T&& param); // param是一个通用引用

第二种情况是auto声明符:

auto&& a = b;	//a是一个通用引用

以上两种情况的共同之处在于都是类型推导。在func内部,param类型需要被推导,在auto声明中,a的类型也需要被推导,而如果带有&&而不需要推导,则就是普通的右值引用:

void func(A&& param);	// 没有类型推导,param是一个右值引用
A&& a = b;		// 没有类型推导,a是一个右值引用

由于引用必须初始化,通用引用也一样。一个通用引用的初始值决定了其具体代表的是一个左值引用还是右值引用。如果初始值是一个左值,那么通用引用对应的就是左值引用,如果初始值是一个右值,那么通用引用对应的就是一个右值引用。

template<typename T>
void func(T&& param); // param是一个通用引用

int a = 1;
func(a);		// a是左值,T被推导为int&,参数param的类型是int&,是一个左值引用
func(5);		// 5是右值,T被推导为int,参数param的类型是int&&,是一个右值引用

需要注意的是,判断一个引用是不是通用引用,类型推导是必要的,但是并不是类型推导就是通用引用,还需要看是不是准确的T&&,如:

template<typename T>
void func(std::vector<T>&& param); // param是一个右值引用

template<typename T>
void func(const T&& param); // param是一个右值引用

上述模板函数func被调用的时候,类型T也会被推导,但是参数param的类型并不是T&&,而是一个std::vector&&,因此param是一个右值引用而不是通用引用。即使多了一个const,那么param也不能成为一个通用引用。

理解std::move()

有了上述的知识基础之后,C++中的move函数功能就很好理解了,std::move的主要作用是将一个左值/右值无条件的转换为右值,但是函数本身并不移动任何东西,只是进行类型的转换,那么这种转换是如何做到的呢?我们来看下std::move具体实现的代码:

template<class T>
typename remove_reference<T>::type&& move(T&& param)
{
    using returnType = typename remove_reference<T>::type&&;
    return static_cast<returnType>(param);
}

通过源码可以看到,std::move接受一个通用引用的参数,函数返回一个&&表明std::move函数返回的是一个右值引用,这里remove_reference表示移除类型T的引用部分,具体的实现可以参考文档,即返回结果是右值。在C++14中std::move的实现更加简单:

template<typename T>
decltype(auto) move(T&& param)
{
    using returnType = remove_reference_t<T>&&;
    return static_cast<returnType>(param);
}

让我们通过以下的代码示例具体分析下std::move是如何工作的:

string s1("hello"),s2;
s2 = std::move(string("world")); // 从右值移动数据
s2 = std::move(s1);		 // 将左值转换为右值 

在第一个赋值中,传递给move的实参是一个右值,当向一个右值引用传递一个右值时,推导的类型即被引用的类型,因此在std::move(string("world"))中:

  • T被推导为string
  • returnType为string
  • move的返回类型为string&&
  • move的函数参数param的类型为string&&
    则函数std::move被推导为:
string&& move(string&& param)
{
	return static_cast<string&&>(param);
}

由于param已经时右值引用类型,因此实际上move函数什么也没做。
在第二个赋值中,传给std::move的参数是一个左值,则在std::move(s1)中:

  • T被推导为string&
  • returnType为string
  • move的返回类型为string&&
  • move的函数参数param的类型为string&
    则函数std::move被推导为:
string&& move(string& param)
{
	return static_cast<string&&>(param);
}

可以看到参数param被static_cast转换为sting&&,在C++中,从一个左值static_cast到一个右值引用时允许的
从以上的示例可以看到,不管传入的是左值还是右值,最终move都会返回一个右值。

理解std::forward()

std::forward与std::move实现的功能是类似的,只不过std::move总是无条件的将它的参数转换为右值,而std::forward只有在满足一定的条件下才会执行转换。std::forward最常见的使用场景是一个模板函数,接受一个通用引用参数,并将其传递给另外的函数:

void Process(const A& lvalue);	// 处理左值
void Process(A&& rvalue);	// 处理右值

template<typename T>
void PrintAndProcess(T&& param)
{
    Print("Some Log");
    process(std::forward<T>(param))
}

现在考虑两次对PrintAndProcess的调用,一次参数为左值,一次参数为右值

A a;
PrintAndProcess(a);		// 左值参数
PrintAndProcess(std::move(a));	// 右值参数

在PrintAndProcess函数内部,参数param被传递给process函数,process函数分别对左值和右值进行了重载,传入PrintAndProcess左值参数时希望process左值版本被调用,传入PrintAndProcess右值参数时,process右值版本被调用。但是前面我们提过,一个右值引用的变量,其本身时一个左值,因此无论传给PrintAndProcess函数的实参时左值还是右值,最终调用process函数都是左值版本。为了解决这个问题,我们就需要一种机制:当传入PrintAndProcess函数的实参是右值时,调用的时process的右值版本。这就是std::forward的使用场景:只把由右值初始化的参数,转换为右值

那么std::forward如何知道param参数是被一个左值还是一个右值给初始化的呢?我们来看下std::forward实现的源码:

template<class T>
constexpr T&& forward(std::remove_reference_t<T>& arg) noexcept{
    // forward an lvalue as either an lvalue or an rvalue
    return (static_cast<T&&>(arg));
}

template<class T>
constexpr T&& forward(std::remove_reference_t<T>&& arg) noexcept{
    static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
        " substituting _Tp is an lvalue reference type");
    // forward an rvalue as an rvalue
    return (static_cast<T&&>(arg));
}

对于左值的转发,首先通过获取类型type,定义args为左值引用的左值变量,然后通过static_cast<T&&>进行强制转换,这里T&&会发生引用折叠,当T被推导为左值引用时,则为T&& &,折叠为T&,当推导为右值引用时,则本身为T&&,forward返回值与static_cast都为T&&。
对于右值的转发不同于左值,只有当类型时右值时才进行static_cast转换,arg为右值引用的左值变量,通过cast转换为T&&。
对应到上述PrintAndProcess函数中我们进行分析:

  • 当PrintAndProcess(a),传入的为左值A时,T被推导为A&,std::forward返回值和static_cast被推导为A& &&,折叠为A&,返回一个左值。
  • 当PrintAndProcess(std::move(a)),传入为右值时,T被推导为A,在std::forward返回值和static_cast被推导为T&&,返回一个右值。

std::move 和 std::forward对比

  • std::move执行到右值的无条件转换。就其本身而言,它没有move任何东西。
  • std::forward只有在它的参数绑定到一个右值上的时候,它才转换它的参数到一个右值。
  • std::move和std::forward只不过就是执行类型转换的两个函数;std::move没有move任何东西,std::forward没有转发任何东西。在运行期,它们没有做任何事情。它们没有产生需要执行的代码,一个byte都没有。
  • std::forward()不仅可以保持左值或者右值不变,同时还可以保持const、Lreference、Rreference、validate等属性不变;

标签:std,右值,int,左值,Mordern,引用,&&
From: https://www.cnblogs.com/zutterhao/p/17299837.html

相关文章

  • C++逆向分析——引用
    引用类型引用类型就是变量的别名,其在初始化时必须要赋值。//基本类型intx=1;int&ref=x;ref=2;printf("%d\n",ref); //类Personp;Person&ref=p;ref.x=10;printf("%d\n",p.x); //指针类型int******x=(int******)1;int******&ref=x;r......
  • wpf CommunityToolkit.Mvvm8.1 MVVM工具包安装引用指南
    CommunityToolkit.Mvvm包(又名MVVM工具包,以前名为Microsoft.Toolkit.Mvvm)是一个现代、快速且模块化的MVVM库。它支持:.NETStandard2.0、.NETStandard2.1和.NET6(UIFramework不支持) 文档地址:https://learn.microsoft.com/zh-cn/dotnet/communitytoolkit/mvvm/......
  • 小程序自定义组件 - 创建与引用
    简单理解组件即"页面的一部分".组件化开发也更多是为了代码复用,方便管理和提高开发效率.前端的组件化开发我想大抵也是借鉴后端开发思想吧.从前端的实现来看,以vue为例即通过扩展自定义HTML标签的的形式,让其局部拥有"单文件"的功能(包括了模板,样式,逻辑).然后组......
  • java中的引用
    引用分类强引用强引用是我们常见的普通对象引用,只有还有强引用指向一个对象,就表明这个对象还“存活”,垃圾收集器不会碰这个对象当一个对象被强引用对象引用时,它就处于可达状态,它是不能被垃圾回收的,即使出现了OOM也不会对该对象回收软引用软引用是相对于强引用......
  • 《渗透测试》安全开发-PHP应用&留言板功能&超全局变量&数据库操作&第三方插件引用 202
    PHPStorm:专业PHP开发IDEPhpStudy:ApacheMYSQL环境NavicatPremium:全能数据库管理工具 #数据导入-mysql架构&库表列1、数据库名,数据库表名,数据库列名2、数据库数据,格式类型,长度,键等 #数据库操作-mysqli函数&增删改查PHP函数:连接,选择,执行,结果,关闭等参考:https://......
  • vue里cdn引入改为本地js文件引用
     问题vue项目cdn引用会出现网络加载不了问题 vue项目cdn引用  改为本地public/index.html引入首先把cdn的链接复制在浏览器里打开或直接下载下来把js文件放到public文件夹下面  在index.html里引入,注意生产环境才加载出来 ......
  • C++中的左值和右值
    左值与右值左值和右值有如下3个规则:左值和右值都是表达式。左值是对象定位器,理论上左值指示指示一个对象。右值是一个临时值,其他位置无法访问这个值,通常情况下在语句执行完成后右值就被丢弃。http://c.biancheng.net/view/281.html左值:可以进行取地址的称为左值。右值:不......
  • Swift中让值类型以引用方式传递
    Swift中让值类型以引用方式传递在Swift众多数据类型中,只有class是引用类型,其余的如Int、Float、Bool、Character、Array、Set、enum、struct全都是值类型.有时候我们需要通过一个函数改变函数外面变量的值(将一个值类型参数以引用方式传递),这时,Swift提供的inout关键......
  • Qt音视频开发33-不同库版本不同位数的库和头文件的引用
    一、前言做开发过程中难免遇到需要引入第三方库的时候,而且需要在不同库版本、不同系统、不同位数下都需要。第三方的库版本众多,一般在大版本中的小版本都是兼容的,但是大版本不兼容,比如ffmpeg目前就有1-6六个大版本,除去1几乎没人用那还剩5个大版本,目前主要还是4居多。vlc主要是vlc......
  • 指针和引用的区别
    指针和引用在C++中都用于间接访问变量,但它们有一些区别:指针是一个变量,它保存了另一个变量的内存地址,引用是另一个变量的别名,与原变量共享内存地址。指针可以被重新赋值,指向不同的变量,引用在初始化后不能更改,始终指向同一个变量。指针可以为nullptr,表示不指向任何变量;引用......