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

yizhihongxing

左值与右值

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等属性不变;

原文链接:https://www.cnblogs.com/zutterhao/p/17299837.html

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:MordernC++之左值(引用)与右值(引用) - Python技术站

(0)
上一篇 2023年4月17日
下一篇 2023年4月17日

相关文章

  • QML和QT

    推荐一些学习qml教程 Qt官方的QML教程: https://doc.qt.io/qt-5/qtqml-index.html这是一个由Qt官方提供的完整的QML教程,包含了所有基本知识和高级语法。 QML中文网:http://www.qmlcn.com/这是一个非常不错的中文QML学习网站,提供了丰富的例子和教程,而且有很多QML爱好者在这里交流。 《Qt…

    C++ 2023年4月18日
    00
  • 内存淘汰策略|页面置换算法对比总结

    在学习【操作系统】 【MySQL】【Redis】后,发现其都有一些缓存淘汰的策略,因此一篇小文章总结一下。 目前还没着笔,初略一想MySQL和操作系统应该都是使用的年轻代和老生代的改进策略,而Redis使用的是随机抽的策略。 MySQL MySQL中存在一个内存缓存池,Buffer Pool。里面存在着控制块和缓存的数据页(当然还有些其他缓存,比如:锁信息、…

    C++ 2023年4月18日
    00
  • 【Visual Leak Detector】在 VS 2015 中使用 VLD

    说明 使用 VLD 内存泄漏检测工具辅助开发时整理的学习笔记。本篇介绍在 VS 2015 中使用 VLD。同系列文章目录可见 《内存泄漏检测工具》目录 目录 说明 1. 使用前的准备 3. 在 VS 2015 中使用 VLD 3.1 无内存泄漏时的输出报告 3.2 有内存泄漏时的输出报告 4. 无法正常使用的可能原因 1. 使用前的准备 参考本人另一篇博客 …

    C++ 2023年4月17日
    00
  • C++/Qt网络通讯模块设计与实现(六)

    前面章节主要讲述网络通讯客户端的实现,各位小伙伴需认真阅读以及理解,理会其中的思想,有疑问的地方可及时给我私信,我都会非常认真地解答大家的疑惑。 C++/Qt网络通讯模块设计与实现(一) C++/Qt网络通讯模块设计与实现(二) C++/Qt网络通讯模块设计与实现(三) C++/Qt网络通讯模块设计与实现(四) C++/Qt网络通讯模块设计与实现(五) 这节…

    C++ 2023年4月18日
    00
  • Qt-FFmpeg开发-视频播放(5)

    音视频/FFmpeg #Qt Qt-FFmpeg开发-视频播放【软/硬解码 + OpenGL显示YUV/NV12】 目录 音视频/FFmpeg #Qt Qt-FFmpeg开发-视频播放【软/硬解码 + OpenGL显示YUV/NV12】 1、概述 2、实现效果 3、FFmpeg硬解码流程 4、优化av_hwframe_transfer_data()性能低问题…

    C++ 2023年4月17日
    00
  • 32位进程设置大地址(3G)空间

    对应32位应用,我们不做任何设置,我们能用的地址空间其实不足2G,有两种方式设置大地址空间: 一、设置链接选项 二、工具editbin 1.开启方法 (1)利用管理员身份运行”Visual Studio 工具命令提示”程序; (2)输入命令:editbin /LARGEADDRESSAWARE D:\xxx.exe 2.检查是否开启成功 (1)利用管理员身份…

    C++ 2023年5月6日
    00
  • <五>move移动语义和forward类型转发

    move : 移动语义,得到右值类型forward:类型转发,能够识别左值和右值类型 只有两种形式的引用,左值引用和右值引用,万能引用不是一种引用类型,它存在于模板的引用折叠情况,但是能够接受左值和右值区分左值和右值得一个简单方式就是能不能取地址一个右值一旦有名字那么就变成了左值 #include <iostream> using namespa…

    C++ 2023年5月4日
    00
  • L2-001-紧急救援*C++(使用Dijkstra算法附带全详细注释)

      L2-001 紧急救援 分数 25 全屏浏览题目 切换布局 作者 陈越单位 浙江大学作为一个城市的应急救援队伍的负责人,你有一张特殊的全国地图。在地图上显示有多个分散的城市和一些连接城市的快速道路。每个城市的救援队数量和每一条连接两个城市的快速道路长度都标在地图上。当其他城市有紧急求助电话给你的时候,你的任务是带领你的救援队尽快赶往事发地,同时,一路上召…

    C++ 2023年4月18日
    00
合作推广
合作推广
分享本页
返回顶部