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

这篇具有很好参考价值的文章主要介绍了MordernC++之左值(引用)与右值(引用)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

左值与右值

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函数中我们进行分析:文章来源地址https://www.toymoban.com/news/detail-408752.html

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

到了这里,关于MordernC++之左值(引用)与右值(引用)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【送书】【C++11】左值引用和右值引用

    需要云服务器等云产品来学习Linux的同学可以移步/--腾讯云--/--阿里云--/--华为云--/官网,轻量型云服务器低至112元/年,新用户首次下单享超低折扣。     目录 一、新的类功能 1、新的默认成员函数 2、类成员变量初始化 3、强制生成默认函数的default 4、禁止生成默认函

    2023年04月09日
    浏览(39)
  • 左值引用、右值引用,std::move() 的汇编解释

    1:左值引用 引用其实还是指针,但回避了指针这个名字。由编译器完成从地址中取值。以vs2019反汇编: 如图,指针和引用的汇编代码完全一样。但引用在高级语言层面更友好,对人脑。比如可以少写一个 * 号和 - 。 ,以下是指针和引用的使用: 以上就是左值引用,引用的

    2024年02月04日
    浏览(40)
  • C++ 学习系列 1 -- 左值、右值与万能引用

    简单的说,左值可以放在等号的左边,右值可以放在等号的右边。 左值可以取地址,右值不能取地址。 1.1 左值举例: 变量、函数或数据成员 返回左值引用的表达式 如 ++x、x = 1、cout \\\' \\\'  int x = 0 1.2 右值举例: 返回非引用类型的表达式 如 x++、x + 1 除字符串字面量之外的字面

    2024年02月14日
    浏览(44)
  • C++右值引用(左值表达式、右值表达式)(移动语义、完美转发(右值引用+std::forward))(有问题悬而未决)

    在 C++ 中,表达式可以分为左值表达式和右值表达式。左值表达式指的是可以出现在赋值语句左边的表达式,例如变量、数组元素、结构体成员等;右值表达式指的是不能出现在赋值语句左边的表达式,例如常量、临时对象、函数返回值等。 右值是指将要被销毁的临时对象或

    2024年02月04日
    浏览(45)
  • C++ 左值和右值

    在C++11中所有的值必属于左值、右值两者之一,右值又可以细分为纯右值、将亡值。在C++11中可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值(将亡值或纯右值)。举个例子,int a = b+c, a 就是左值,其有变量名为a,通过a可以获取该变量的地址;

    2024年02月14日
    浏览(39)
  • C++左值右值完美转发转移

    英文含义: 左值(Lvalue) : Locator value ,意味着它指向一个具体的内存位置。 右值(Rvalue) : Read value ,指的是可以读取的数据,但不一定指向一个固定的内存位置。 定义 左值 :指的是一个持久的内存地址。左值可以出现在赋值操作的左侧或右侧。例如,变量、数组的元

    2024年03月10日
    浏览(54)
  • 【C语言深入】细聊C语言中的“左值”和“右值”

    左值就是那些可以出现在赋值符号左边的东西,它标识了一个可以存储结果值的地点。 程序在编译时,编译器会为每个变量分配一个地址(左值),这个地址在编译是即可知。 也就是说,左值在编译时即可知,左值标志存储结果的一个地方,也可以理解为左值就是一块空间。

    2024年02月14日
    浏览(46)
  • C++面试八股文:什么是左值,什么是右值?

    某日二师兄参加XXX科技公司的C++工程师开发岗位第16面: 面试官:什么是左值,什么是右值? 二师兄:简单来说,左值就是可以使用 符号取地址的值,而右值一般不可以使用 符号取地址。 二师兄:一般左值存在内存中,而右值存在寄存器中。 二师兄:严格意义上分,右值

    2024年02月09日
    浏览(42)
  • C++:深入理解C++11新特性:Chapter3:左值和右值

    在C语言中,我们常常会提起左值(lvalue),右值(rvalue)这样的称呼,而在编译程序时,编译器有时也会报出错误信息中包含 左值,右值说法。不过左值、右值通常不是通过一个严谨的定义而为人所知。下面我通过这样一个例子,来引导大家认识: 左值,右值,左值引用,右

    2024年02月04日
    浏览(53)
  • C++右值引用,右值引用与const引用的区别

    左值:可以取地址的、有名字的变量,有持久性; 右值:一般是不可寻址的常量,或在表达式求值过程中创建的无名临时对象,短暂性的。 C++11新增了另一种引用——右值引用。这种引用可指向右值,使用声明。 右值引用只能引用临时变量和常量值。 const引用:可以引用普

    2024年01月18日
    浏览(64)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包