C++左值右值/左右值引用/std::move()

C++左值右值/左右值引用/std::move()

左值/右值

  • 左值(lvalue)是放在赋值语句左边可以被赋值的值(不是变量!)左值必须在内存中有一个确定的地址
  • 右值(rvalue)用来放在赋值语句右边,将自己的值取出赋给别的变量,右值没有一个确定的地址

也可以这么理解:

  • 左值是指表达式结束后依然存在的持久化对象

  • 右值是指表达式结束时就不再存在的临时对象。所有的具名变量或者对象都是左值,而右值不具名(所以匿名变量是一个右值)。

《C++ Primer(第5版)》中描述左值和右值:

  • 当对象被用作左值的时候,用的是对象的身份(在内存中的位置)
  • 当一个对象被用作右值的时候,用的是对象的值(内容)

以上三个说法都是非常容易理解的,下面我们举个例子:

1
2
int x = 0;
int* px = &x;
  • int x中的x是一个左值,因为他是赋值语句=左边的值,表达式结束后依然存在的持久化对象,用的是对象的身份(在内存中的位置,向内存写入0)
  • &x是返回一个右值,它指向了对象x的地址,通过赋值运算符=,将对象x的地址(右值)赋值给了一个新定义的左值对象px

常见的右值:“abc”,123这种字面值常量和表达式求值过程中创建的临时对象,还有typename()这种匿名变量/匿名对象。

访问关系上来看

一般来说:右值可以读,不可以写;左值既可以读,也可以写。

当然有一些例外情况

  • 即使它是左值,也不可以被修改,比如const限定符,const int a = 3,a是不可以被修改的。

  • 右值在某些情况下也可以写,就是右值引用

    • ```c++
      int a = 0;
      int &&temp = a + 3;
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13

      ### 从替代关系上来看

      **一般来说:**需要右值的地方,都可以使用左值来替代。

      当然有一些**例外情况**:

      - 右值引用接收右值,拒绝左值

      - ```c++
      int a = 0;
      int &&temp = a + 3;//yes 右值引用接受 a + 3这个表达式(右值)
      int &&temp1 = a; // no 右值引用不接受a这个左值

左值引用/右值引用

基本介绍

引用是变量的别名,必初始化,C++引入的,C语言只有指针,没有引用。引用操作从反汇编层面看可以说完全指针一样,从使用层面来说的确降低大家都指针的理解成本,传参时引用本意减少拷贝,提高性能,但由于是编译器的内部转为为指针,有时比指针的灵活性弱一点点,像原来拷贝构造函数const T&左引用存在一定的缺陷,右值引用带来的移动语义就是来弥补。

指向左值的引用就是左引用,我们单个&来表示,C++11前一直使用的;对右值的引用是右引用,我们用&&来表示,如下面代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int a1 = 100;


// c1对左值a1引用,左引用
int& c1 = a1;

// b1对右值200的引用,右引用
int&& b1 = 200;

// rkTA1对右值TestClassA(1000)进行右引用(匿名变量是左值!)
TestClassA&& rkTA1 = TestClassA(1000);

// 无法对右值( a1++)进行左引用,编译失败
int& c2 = a1++; // error

// 无法对左值a1进行直接右引用,编译失败
int&& b2 = a1; // error

// 无法对右值TestClassA(1000)进行左引用,编译失败
TestClassA& rkTA2 = TestClassA(1000); // error

注意:虽然右值引用只能引用右值,但是右值引用本身却是左值。

1
2
int &&rr_1 = 42;
int &&rr_2 = rr_1; // 编译错误 error: cannot bind rvalue reference of type 'int&&' to lvalue of type 'int'|

因此rr_1单独使用,是一个表达式——一个没有运算符的表达式,它返回的是左值;因此不可以将右值引用绑定到左值rr_1上。

一种特殊的常量左值引用

有没有直接对右值进行左引用的?

还真有,那就是const T&常量左值引用,能接受右值,对右值进行这种形式左引用写法也不少,其生命周期被延续。

这个const T&最大的好处就是:

工业界实践的时候,如果C++中,函数传参,不改变参数时,尤其是大数据,尽量使用const T&。我们常用的拷贝构造函数T(const T&)参数是这个形式,vector容器的函数push_back(const value_type& val)参数也是,有没有注意到,这类函数同时接受右值和左值

左右值转换:std::move

既然是左/右值引用数据类型,就存在转换关系。

我们可以使用标准库函数std::move得到左值的右值引用类型。

1
2
int &&rr_1 = 42;
int &&rr_2 = std::move(rr_1);

move调用告诉编译器,我们有一个左值rr_1,但是我希望像一个右值一样处理它。我们必须保证,接下来除了对rr_1赋值或销毁它,我们不再使用它。我们不能对rr_1的值作任何假设。

《C++ Primer(第5版)》推荐使用std::move而不是move,可以避免潜在的命名冲突。

std::move的定义:

img

​ 这里,T&&是通用引用,需要注意和右值引用(比如int&&)区分。通过move定义可以看出,move并没有”移动“什么内容只是将传入的值转换为右值,此外没有其他动作。std::move+移动构造函数或者移动赋值运算符 ,也就是两者合力才起到这样的作用,才能充分起到减少不必要拷贝的意义。
std::move使用前提:

  • 定义的类使用了资源并定义了移动构造函数和移动赋值运算符 (右值和移动构造函数/移动赋值函数配合使用才能起到资源直接转移的作用,减少不必要的拷贝)

  • 该变量即将不再使用 (很容易理解:move后就会变成右值,右值不再具有资源,而单纯作为一个值,不再作为一个对象)

关于move到底是怎么做到左右值转换的请看这篇文章:

https://blog.csdn.net/daaikuaichuan/article/details/88371948

简单总结下来就是:我们通过static_cast<>进行强制类型转换返回T&&右值引用,而static_cast之所以能使用类型转换,是通过remove_refrence::type的偏特化模板移除T&&,T&的引用,获取具体类型T(模板偏特化)。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!