C++左值与右值,右值引用,移动语义与完美转发详解

目录
  • C++——左值与右值、右值引用、移动语义与完美转发
    • 一、左值和右值的定义
    • 二、如何判断一个表达式是左值还是右值(大多数场景)
    • 三、C++右值引用
    • 四、std::move()与移动语义
    • 五、 完美转发
  • 总结

C++——左值与右值、右值引用、移动语义与完美转发

在C++或者C语言中,一个表达式(可以是字面量、变量、对象、函数的返回值等)根据其使用场景不同,分为左值表达式和右值表达式。

一、左值和右值的定义

1.左值的英文为locator value,简写为lvalue,可意为存储在内存中、有明确存储地址(可寻址)的数据

2.右值的英文为read value,简写为rvalue,指的是那些可以提供数据值的数据(不一定可寻址,例如存储与寄存器中的数据)

二、如何判断一个表达式是左值还是右值(大多数场景)

1.可位于赋值号(=)左侧的表达式就是左值;反之,只能位于赋值号右侧的表达式就是右值。例如:

int a = 5;

其中,变量a就是一个左值,而字面量5就是一个右值。

注:C++中的左值可以当作右值使用,反之则不行,如

int b = 10; //b是一个左值
a = b; //a、b都是左值,只不过b可以当作右值使用
10 = b; //错误,10是一个右值,不能当作左值使用

2.有名称的、可以获取到存储地址的表达式即为左值;反之则为右值

以上面定义的变量a、b为例,a和b是变量名,则通过&a和&b可以获得他们的存储地址,因此a和b都是左值;反之,字面量5、10,它们既没有名称,也无法获取其存储地址(字面量通常存储在寄存器中,或者和代码存储在一起),因此5、10都是右值

三、C++右值引用

C++ 98/03标准中就有引用,但这里的引用只能操作左值,无法对右值添加引用,故被称为左值引用,例如:

int num = 10;
int &b = num; //正确
int &c = 10; //错误

注意,虽然C++ 98/03标准不支持为右值建立非常量左值引用,但允许使用常量左值引用操作右值。即,常量左值引用既可以操作左值,也可以操作右值,例如:

int num = 10;
const int &b = num; //正确
const int &c = 10; //正确

为什么需要右值引用?

右值往往是没有名称的,因此要使用它只能借助引用的方式。这就产生一个问题,实际开发中我们可能需要对右值进行修改(如实现移动语义时),而是用常量左值引用的方式是无法做到这一点的。

为此,C++11新标准引入了另一种引用方式,成为右值引用,用&&表示

需要注意的是,和声明左值引用一样,右值引用也必须立即进行初始化操作,且只能使用右值进行初始化,比如:

int num = 10;
int &&a = num; //错误,右值引用不能初始化为左值
int &&a = 10; //正确

和常量左值引用不同的是,右值引用还可以对右值进行修改。例如:

int &&a = 10;
a = 100; //正确

另外,C++语法上是支持定义常量右值引用的,例如:

const int&& a = 10;

但这种定义出来的右值引用并无实际用处。一方面,右值引用主要用于移动语义和完美转发,其中前者需要有修改右值的权限;其次,常量右值引用的作用就是引用一个不可修改的右值,而这项工作常量左值引用就可以完成

四、std::move()与移动语义

C++11标准中,借助右值引用可以为指定类添加移动构造函数,这样当使用该类的右值对象(可以理解为临时对象)初始化同类对象时,编译器会优先选择移动构造函数。

注:移动构造函数的调用时机是:用同类的右值对象初始化新对象。那么当用此类的左值对象(有名称,能获取其存储地址的实例对象)初始化同类对象时,如何调用移动构造函数呢?C++11给出的解决方案就是调用std::move()函数。

虽然move是移动的意思,但是该函数并不移动任何数据,它的功能是将某个左值强制转化为右值,常用于实现移动语义

用法示例:

#include<iostream>
#include<utility>
using namespace std;
class movedemo
{
public:
    movedemo():num(new int(0)) {
        cout << "construct!" << endl;
    }
    //copy constructor
    movedemo(const movedemo &d):num(new int(*d.num)) {
        cout << "copy constrct!" << endl;
    }
    //move constructor
    movedemo(movedemo &&d):num(d.num) {
        d.num = NULL;
        cout << "move construct!" << endl;
    }
private:
    int *num;
};
int main()
{
    movedemo demo;
    cout << "demo2:\n";
    movedemo demo2 = demo;
    cout << "demo3:\n";
    movedemo demo3 = std::move(demo);//执行完之后demo.num会置为空
    return 0;
}

程序运行结果:

construct!demo2:copy constrct!demo3:move construct!

通过观察程序的输出结果,以及对比 demo2 和 demo3 初始化操作不难得知,demo 对象作为左值,直接用于初始化 demo2 对象,其底层调用的是拷贝构造函数;而通过调用 move() 函数可以得到 demo 对象的右值形式,用其初始化 demo3 对象,编译器会优先调用移动构造函数。

五、 完美转发

什么是完美转发?

它指的是函数模板可以将自己的参数“完美”地转发给内部调用的其他函数。所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变

举个栗子:

template<typename T>void function(T t) {	otherdef(t);}

如上所示,function()函数模板中调用了otherdef()函数。在此基础上,完美转发指的是:如果function()函数接收到的参数t为左值,那么该函数传递给otherdef()的参数t也是左值;反之如果function()函数接收到的参数t为右值,那么传递给otherdef()函数的参数t也必须为右值。

显然,上面这个例子中,function()函数模板并没有实现完美转发。一方面,参数t为非引用类型,这意味着在调用function()函数时,实参将值传递给形参的过程就需要额外进行一次拷贝操作;另一方面,无论调用function()函数模板时传递给参数t的是左值还是右值,对于函数内部的参数t来说,它有自己的名称,也可以获取它的存储地址,因此它永远都是左值,即传递给otherdef()函数的参数t永远都是左值。总之,无论从哪个角度看,function()函数的定义都不“完美”。

为什么需要完美转发?

C++11新标准中引入了右值引用和移动语义,因此很多场景中是否实现完美转发,直接决定了该参数的传递过程使用的是拷贝语义(调用拷贝构造函数)还是移动语义(调用移动构造函数)

如何实现完美转发?

C++98/03标准:由于没有右值引用,只能通过重载函数模板(可通过常量左值引用传递右值)的方式实现转发,且这种方式存在弊端,如:此实现方式只适用于模板函数仅有少量参数的情况,否则就需要编写大量的重载函数模板,造成代码的冗余。

为了方便用户更快速地实现完美转发,C++11标准中允许在函数模板中使用右值引用来实现完美转发

C++11标准中规定,通常情况下右值引用形式的参数只能接收右值,不能接收左值。但对于函数模板中使用右值引用语法定义的参数来说,它不再遵守这一规定,既可以接收右值,也可以接收左值(此时的右值引用又被称为“万能引用”)

仍以function()函数为例,在C++11标准中实现完美转发,只需编写如下一个模板函数即可:

template<typename T>void function(T &&t) {otherdef(t);}

此模板函数的参数t既可以接收左值,也可以接收右值。但仅仅使用右值引用作为函数模板的参数是远远不够的,还有一个问题需要解决,即如果调用function()函数时为其传递一个左值引用或右值引用的实参,如下所示:

int n = 10;int &num = n;function(num); //T为int &int &&num2 = 11;function(num2); //T为int &&

其中由function(num)实例化的函数底层就变成了function(int& &&t),而由function(num2)实例化的函数底层则变成了function(int&& &&t)。C++98是不支持这种语法的,而C++11为了更好地实现完美转发,专门为其指定了新的类型匹配规则,又称为引用折叠规则(假设A表示实际传递参数的类型):

  • 当实参为左值或左值引用(A&)时,函数模板中T&&将转变为A&(A& && = A&);
  • 当实参为右值或右值引用(A&&)时,函数模板中T&&将转变为A&&(A&& && = A&&);

上述规则的含义是:在实现完美转发时,只要函数模板的参数类型为T&&,则C++可以自行准确地判定实际传入的实参是左值还是右值

通过将函数模板的形参类型设置为T&&,我们可以很好地解决接收左、右值的问题。但除此之外,还需要解决一个问题,即无论传入的形参是左值还是右值,对于函数模板内部来说,形参既有名称又能寻址,因此它都是左值。那么如何才能将函数模板接收到的形参连同其左、右值属性,一起传递给被调用的函数呢?

答案就是使用C++11提供的模板函数forward(),注意其和move的区别是forward要通过显式模板实参来使用

用法示例:

#include <iostream>
#include <utility>using namespace std;//重载被调用函数,查看完美转发的效果
template<typename T>
void print(T &t)
{
cout << "lvalue" << endl;
}
template<typename T>
void print(T &&t)
{    cout << "rvalue" << endl;
}
template<typename T>
void TestForward(T &&v)
{
print(v);
print(std::forward<T>(v));
print(std::move(v));
}
int main()
{
TestForward(1);
cout << endl;
int x = 1;
TestForward(x);
cout << endl;
TestForward(std::forward<int>(x));
return 0;
}

程序执行结果为:

lvaluervaluervaluelvaluelvaluervaluelvaluervaluervalue

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注我们的更多内容!

(0)

相关推荐

  • 详解C++右值引用

    概述 在C++中,常量.变量或表达式一定是左值(lvalue)或右值(rvalue). 左值:非临时的(具名的,可在多条语句中使用,可以被取地址).可以出现在等号的左边或右边.可分为非常量左值和常量左值. 右值:临时的(不具名的,只在当前语句中有效,不能取地址).只能出现在等号的右边.可分为非常量右值和常量右值. 左值引用:对左值的引用就是左值引用.可分为非常量左值引用和常量左值引用. 注:常量左值引用是"万能"的引用类型,可以绑定到所有类型的值,包括非常量左值.常量左值.非常量右值和

  • 浅析C++11中的右值引用、转移语义和完美转发

    1. 左值与右值: C++对于左值和右值没有标准定义,但是有一个被广泛认同的说法:可以取地址的,有名字的,非临时的就是左值;不能取地址的,没有名字的,临时的就是右值. 可见立即数,函数返回的值等都是右值;而非匿名对象(包括变量),函数返回的引用,const对象等都是左值. 从本质上理解,创建和销毁由编译器幕后控制的,程序员只能确保在本行代码有效的,就是右值(包括立即数);而用户创建的,通过作用域规则可知其生存期的,就是左值(包括函数返回的局部变量的引用以及const对象),例如: int& fo

  • C++学习之移动语义与智能指针详解

    移动语义 1.几个基本概念的理解 (1)可以取地址的是左值,不能取地址的就是右值,右值可能存在寄存器,也可能存在于栈上(短暂存在栈)上 (2)右值包括:临时对象.匿名对象.字面值常量 (3)const 左值引用可以绑定到左值与右值上面,称为万能引用.正因如此,也就无法区分传进来的参数是左值还是右值. const int &ref = a;//const左值引用可以绑定到左值 const int &ref1 = 10;//const左值引用可以绑定到右值 (4)右值引用:只能绑定到右值不能绑

  • C++中左值和右值的区别详解

    目录 左值右值定义: 特性 左值引用, 右值引用 总结 左值右值定义: 左值指的是既能够出现在等号左边也能出现在等号右边的变量(或表达式),右值指的则是只能出现在等号右边的变量(或表达式). int a; int b; a = 1; b = 2; a = b; b = a; a + b = 3; // 非法表示 右值分为纯右值和将亡值: 纯右值:临时变量和不跟对象关联的字面量值 将亡值:在确保其他变量不再被使用或即将销毁时,通过盗取的方式,可以避免内存空间的释放和分配,能够延长变量值的生命期.右

  • 详解C++11中的右值引用与移动语义

    C++11的一个最主要的特性就是可以移动而非拷贝对象的能力.很多情况都会发生对象的拷贝,有时对象拷贝后就立即销毁,在这些情况下,移动而非拷贝对象会大幅度提升性能. 右值与右值引用 为了支持移动操作,新标准引入了一种新的引用类型--右值引用,就是必须绑定到右值的引用.我们通过&&而不是&来获得右值引用.右值引用一个重要的特性就是只能绑定到将要销毁的对象. 左值和右值是表达式的属性,一些表达式生成或要求左值,而另一些则生成或要求右值.一般而言,一个左值表达式表示的是一个对象的身份,而右

  • C++左值与右值,右值引用,移动语义与完美转发详解

    目录 C++——左值与右值.右值引用.移动语义与完美转发 一.左值和右值的定义 二.如何判断一个表达式是左值还是右值(大多数场景) 三.C++右值引用 四.std::move()与移动语义 五. 完美转发 总结 C++——左值与右值.右值引用.移动语义与完美转发 在C++或者C语言中,一个表达式(可以是字面量.变量.对象.函数的返回值等)根据其使用场景不同,分为左值表达式和右值表达式. 一.左值和右值的定义 1.左值的英文为locator value,简写为lvalue,可意为存储在内存中.有明

  • 详解C++中的左值,纯右值和将亡值

    目录 引入 一.表达式 二.值类别 三.左值 四.纯右值 五.将亡值 六.注意 引入 C++中本身是存在左值,右值的概念,但是在C11中又出现了左值,纯右值,将亡值得概念:这里我们主要介绍这些值的概念. 一.表达式 定义:由运算符和运算对象构成的计算式(类似数学中的算术表达式) 每个 C++ 表达式(带有操作数的操作符.字面量.变量名等)可按照两种独立的特性加以辨别:**类型和值类别 **(value category).每个表达式都具有某种非引用类型,且每个表达式只属于三种基本值类别中的一种:

  • C++11右值引用和转发型引用教程详解

    右值引用 为了解决移动语义及完美转发问题,C++11标准引入了右值引用(rvalue reference)这一重要的新概念.右值引用采用T&&这一语法形式,比传统的引用T&(如今被称作左值引用 lvalue reference)多一个&. 如果把经由T&&这一语法形式所产生的引用类型都叫做右值引用,那么这种广义的右值引用又可分为以下三种类型: 无名右值引用 具名右值引用 转发型引用 无名右值引用和具名右值引用的引入主要是为了解决移动语义问题. 转发型引用的引

  • C++精要分析右值引用与完美转发的应用

    目录 区分左值与右值 右值引用 移动语义 完美转发 结语 区分左值与右值 在C++面试的时候,有一个看起来似乎挺简单的问题,却总可以挖出坑来,就是问:“如何区分左值与右值?” 如果面试者自信地回答:“简单来说,等号左边的就是左值,等号右边的就是右值.” 那么好了,手写一道面试题继续提问. int a=1; int b=a; 问:a和b各是左值还是右值? b是左值没有疑问,但如果说a在上面是左值,在下面是右值的,那就要面壁思过了.C++从来就不是一门可以浅尝辄止的编程语言,要学好它真的需要不断地去

  • C++右值引用与move和forward函数的使用详解

    目录 1.右值 1.1 简介 1.2 右值引用 1.3 右值引用的意义 2.move 3.foward 1.右值 1.1 简介 首先区分一下左右值: 左值是指存储在内存中.有明确存储地址(可取地址)的数据: 右值是指可以提供数据值的数据(不可取地址) 如int a=123:123是右值, a是左值.总的来说 可以对表达式取地址(&)就是左值,否则为右值 而C++11 中右值又可以分为两种: 纯右值:非引用返回的临时变量.运算表达式产生的临时变量如a+b.原始字面量和 lambda 表达式等 将亡

  • C++右值引用与移动构造函数基础与应用详解

    目录 1.右值引用 1.1左值右值的纯右值将亡值右值 1.2右值引用和左值引用 2.移动构造函数 2.1完美的移动转发 1.右值引用 右值引用是 C++11 引入的与 Lambda 表达式齐名的重要特性之一.它的引入解决了 C++ 中大量的历史遗留问题, 消除了诸如 std::vector.std::string 之类的额外开销, 也才使得函数对象容器 std::function 成为了可能. 1.1左值右值的纯右值将亡值右值 要弄明白右值引用到底是怎么一回事,必须要对左值和右值做一个明确的理解

  • 详解C++ 左值引用与 const 关键字

    左值引用是已定义的变量的别名,其主要用途是用作函数的形参,将 const 关键字用于左值引用时,其在初始化时可接受的赋值形式变得更加广泛了,这里来总结一下. 左值引用是已定义的变量的别名,其主要用途是用作函数的形参,通过将左值引用变量用作参数,函数将使用原始数据,而不是副本.引用变量必须在声明时同时初始化,可将 const 关键字用于左值引用,如下所示: //声明并初始化常规左值引用变量 int x = 55; int & rx = x; //将const关键字用于左值引用变量,以下几种为等效表

随机推荐