左值 右值 左值引用 右值引用 std::move std::forward

1. 左值、右值

能取地址的是左值,否则右值

int foo() {
  return 0;
}

int a = 1;       //a左值, &a有效
int &&b = 1;  //b左值, &b有效
a + 1;           //a + 1右值,&(a+1)无效
foo();           //foo()右值, &(foo())无效
1;                //右值, &1无效
int c = b + a;//a, b, c都是左值,不要以出现在等号左右来区分左值右值

2. 左值引用、常量左值引用、具名右值引用、无名右值引用

引用分为四种:左值引用、常量左值引用、具名右值引用、无名右值引用

  1. 左值引用 T &: 左值引用需要内存地址,因此,只能绑定左值。
  2. 常量左值引用 const T &: 常量左值引用可以绑定左值或者右值。
  3. 具名右值引用 T && : 带有变量名的右值引用,只能绑定右值。
  4. 无名右值引用 std::move(T): 没有变量名的右值引用,不能绑定左值或者右值。
是否可以绑定 左值 右值
左值引用 Yes No
常量左值引用 Yes Yes
具名右值引用 No Yes
无名右值引用 No No
int foo() {
  return 0;
}

int a = 0;
int &b = a;                 //可以, 左值引用绑定左值
int &c = 1;                  //不行,左值引用不能绑定右值
int &d = foo();            //不行,左值引用不能绑定右值
const int &e = a;        //可以,常量左值引用可以绑定左值
const int &f = foo();   //可以,常量左值引用可以绑定右值
int && g = b;             //不可以,具名右值引用不可以绑定左值
int && h = foo();        //可以,具名右值引用绑定右值
int && i = a + a;        //可以,具名右值引用绑定右值
std::move(a) = 1;        //不可以,无名右值引用不能绑定左值或者右值
int && j = std::move(a);//可以,具名右值引用绑定无名右值引用

左值引用,左值常量引用,和具名右值引用是3种变量类型,和int, char, long一样都有地址,因此都是左值。而无名右值引用没有地址,不属于左值。

在实现中,和左值引用不一样,具名右值引用会为自己的引用的内容开辟了一块内存空间。此时一个具名右值引用所需要的空间为 内容地址(x64: 8字节) + 内容大小,比如下面的例子,在栈上开辟了12字节,后4字节存放(int)1,前8字节存放内容的地址

int main() {
  int &&a = 1;
  return 0;
}

其汇编为

        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        movl    $1, %eax
        movl    %eax, -12(%rbp)     ;  [rbp - 12 , rbp - 8) <-- (int)1
        leaq    -12(%rbp), %rax      ;  取地址,rax = rbp - 12
        movq    %rax, -8(%rbp)      ; 存地址 [rbp - 8, rbp) <--- rbp - 12
        movl    $0, %eax
        popq    %rbp
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc

因此,我们可以修改具名右值引用的内容,而无需像左值引用一样必须先申请一个变量,下例输出2

#include <iostream>
int foo() {
  return 0;
}
int main() {
  int &&a = foo();
  a = 2;
  std::cout<<a<<std::endl;
}

所以在具名右值引用的操作下,我们可以修改右值,可以通过具名右值引用本身直接修改,也可以通过 T & = T && 的方式让其他引用修改

3. std::move

std::move负责将左值或者右值变为无名右值引用。首先看一下move的实现:

template <class typename>
struct remove_reference { typedef T type; };

template <class typename>
struct remove_reference<T&> { typedef T type; };

template <class typename>
struct remove_reference<T&&> { typedef T type; };

template<typename T>
typename std::remove_reference<T>::type&& move(T&& t) { 
  return static_cast<typename std::remove_reference<T>::type&&>(t); 
}

其中 std::remove_reference 负责去掉类型中的引用,比如 将int &变为int

可以看到,move输入是一个具名右值引用,而输出是一个具名右值引用。输入输出有什么不同呢?输入的T本身也可能是非引用比如int, std::string,也可能引用比如std::string &, int &&,而输出只能是无名右值引用。为了搞清楚这一变化,先用以下代码用以检测输入T到底是什么类型

#include <iostream>
#include <string>
template<typename T>
bool T_is_value(T && x) {
  using U = typename std::remove_reference<T>::type;
  return std::is_same<T, U>::value && (!std::is_const<U>::value);
}


template<typename T>
bool T_is_const_value(T && x) {
  using U = typename std::remove_reference<T>::type;
  return std::is_same<T, U>::value && std::is_const<U>::value;
}

template<typename T>
bool T_is_left_ref(T && x) {
  using U = typename std::remove_reference<T>::type&;
  using V = typename std::remove_reference<T>::type;
  return std::is_same<T, U>::value && (!std::is_const<V>::value);
}

template<typename T>
bool T_is_const_left_ref(T && x) {
  using U = typename std::remove_reference<T>::type&;
  using V = typename std::remove_reference<T>::type;
  return std::is_same<T, U>::value && std::is_const<V>::value;
}

template<typename T>
bool T_is_right_ref(T && x) {
  using U = typename std::remove_reference<T>::type&&;
  using V = typename std::remove_reference<T>::type;
  return std::is_same<T, U>::value && (!std::is_const<V>::value);
}
template<typename T>
bool T_is_const_right_ref(T && x) {
  using U = typename std::remove_reference<T>::type&&;
  using V = typename std::remove_reference<T>::type;
  return std::is_same<T, U>::value && std::is_const<V>::value;
}

class A {};

int main () {
  const A a;
  std::cout<<"T_is_value           "<<T_is_value(a)<<std::endl;
  std::cout<<"T_is_const_value     "<<T_is_const_value(a)<<std::endl;
  std::cout<<"T_is_left_ref        "<<T_is_left_ref(a)<<std::endl;
  std::cout<<"T_is_left_const_ref  "<<T_is_const_left_ref(a)<<std::endl;
  std::cout<<"T_is_right_ref       "<<T_is_right_ref(a)<<std::endl;
  std::cout<<"T_is_const_right_ref "<<T_is_const_right_ref(a)<<std::endl;
  return 0;
}

输出

T_is_value           0
T_is_const_value     0
T_is_left_ref        0
T_is_left_const_ref  1
T_is_right_ref       0
T_is_const_right_ref 0

因此T是一个常量左值引用类型 T = const A &。由于子函数入口参数类型都是具名右值引用 T&&,因此实际输入的实参是 const A & &&,编译器是怎么得到这一类型的?首先,C++有如下reference collapse标准:

T && & = T &  
T& && = T &
T && && = T &&

T可以带有const 或是 volatile

来看这里的例子:

  1. a类型是const A , 如果T = const A,那么参数传递为:const A && t = a,这是不允许的,因为a是左值,不能绑定到具名右值引用。
  2. 如果T = const A &,那么参数传递为 const A & && t = a,根据之前的reference collapse规则,有const A & t = a,常量左值引用绑定左值这是可以的。
  3. 如果T = const A &&,则由collapse规则得到 const A && t = a,具名右值引用绑定左值,是不可以的。

因此编译器采用第二种方案

  1. 如果将上述main函数里的a改成左值引用:A b; const A & a = b,则T = const A &
  2. 如果将上述main函数里的a改成std::move(a),则T = const A

注意具名右值引用保留const/volatile 这一特点,当一个函数有n个参数,每个参数都需要重载const / non-const 引用的时候,我们可以用T &&来统一这两种类型

4 std::forward

从上面的例子我们看出,由于函数参数总是左值,因此经过一次函数调用后,无名右值就消失了。考虑如下代码

#include <iostream>

template <typename T>
void print(T &t) {
  std::cout<<"T &t"<<std::endl;
}


template <typename T>
void print(const T &t) {
  std::cout<<"const T &t"<<std::endl;
}

template <typename T>
void print(T &&t) {
  std::cout<<"T &&t"<<std::endl;
}


template <typename T>
void forward_value(T&& val) {
  print(val);
}

class A {};
int main () {
  A a;
  forward_value(a);
  forward_value(std::move(a));
  return 0;
}

输出

T &t
T &t

这样函数void print(T &&t)就无法调用了,如何改变这一点呢?

std::forward 解决了这个问题, 将代码作如下改变:

template <typename T>
void forward_value(T&& val) {
  print(std::forward<T>(val));
}

则输出

T &t
T &&t

std::forward是如何做到这一点的?看一下实现

template<typename T>
T&& forward(typename std::remove_reference<T>::type& t) { 
  return static_cast<T&&>(t); 
}
 
template<typename T> 
T&& forward(typename std::remove_reference<T>::type&& t) {
  static_assert(!std::is_lvalue_reference<T>::value, "template argument"
            " substituting _Tp is an lvalue reference type");
  return static_cast<T&&>(t);

很显然了,就是再次转T &&。考虑上面的例子,

  1. forward_value(a); => std::forward<A &>(val)); => return static_cast<T& &&>(t); => print(A &)
  2. forward_value(std::move(a)); => std::forward(val)); => return static_cast<T&&>(t); => print(A &&)

猜你喜欢

转载自www.cnblogs.com/qxred/p/std_move_forward.html
今日推荐