右值引用、引用折叠、std::move和std::forward

一、右值引用

理解右值引用前,我们需要先理解什么是右值。右值(RValue)是指存在于内存上的,但是我们无法通过符号(或者叫别名)去访问修改的临时变量。e.g.

int k = 5; // k = LValue, 5 = RValue
int m = k + j; // m = LValue, k+j = RValue

右值引用,类似于左值引用,用来指向右值。下面例子就是通过赋值操作将右值42连接到引用j:

int &&j = 42;

有了右值引用,我们可以对42存储的内存位置进行读写。同时,通过右值引用,我们还延长了临时变量右值42的生命周期,只要j还在有效的scope里,右值42就是有效的。

引入右值引用的主要考虑之一就是节省拷贝操作,使得赋值操作更有效率。以下面的例子为例:

int m = k + l; // m = LValue, k+l = RValue

执行上面这条指令,会有以下几步:

  1. 在内存上创建变量左值m;
  2. 计算k+l,结果以右值的形式保存在内存上;
  3. 临时的计算结果拷贝到m的地址处,此时同样的一个值存在两处,因此消耗了两倍的内存;
  4. 释放临时变量占用的内存。

因此,这行简单的计算赋值的花销是:内存上创建了两个变量,一次拷贝操作和一次释放操作。下面看一下使用右值引用的情况:

int &&n = k + l; // n = RValue-Reference

执行步骤:

  1. 在内存上创建右值引用n;
  2. 计算k+l,然后将临时结果的地址赋给n。

可以看出,使用右值引用,只在内存上创建了一次对象,并且没有拷贝操作和释放操作。因此比没有使用右值引用要更高效。

跟左值引用一样,右值引用可以作为函数的参数,从而避免拷贝操作。看起来右值引用和左值引用似乎一样,都是通过提供一个别名指向内存上变量的位置来节省拷贝操作,唯一的区别就是右值引用指向的是内存上没有名字的临时变量,而左值引用指向的是内存上有名字的变量。但是在实际使用时,不能将右值赋给左值引用,也不能将左值赋给右值引用。看下面的例子:

int main()
{
    int i{5};
    int &lv_ref = i;
    // int &lv_ref = 5; // Error: lvalue reference to type ‘int’ cannot bind to a temporary of type ‘int’
 
    int &&rv_ref = 10;
    // int &&rv_ref = i; // Error: rvalue reference to type ‘int’ cannot bind to lvalue of type ‘int’
    rv_ref = 20;
    cout << "rv_ref = " << rv_ref << endl; // rv_ref = 20

    return 0;
}

从例子中可以看出,我们也可以使用右值引用来改变临时变量的值。

下面我们来看下编译器对左值和右值的判断:

void passValue(int &¶m)
{
    cout << "Rvalue\n";
}
 
void passValue(int ¶m)
{
    cout << "Lvalue\n";
}
 
int main()
{
    int i{5};
    int &&rv_ref = 10;

    passValue(i); // output: Lvalue
    passValue(5); // output: Rvalue
 
    passValue(rv_ref); // output: Lvalue
 
    return 0;
}

前面两个调用很好理解,i是左值,所以调用参数是左值引用的函数,5是右值,所以调用参数是右值引用的函数。比较有意思的是,入参是一个右值引用的话,调用的却是参数是左值引用的函数。编译器把右值引用看作是左值,这也不难理解,右值引用是指向内存上临时变量的别名,我们可以读取它也可以通过它来修改临时变量的值,因此右值引用在使用上和左值没有区别。所以,右值引用基本上是左值。

同样地,在void passValue(int &¶m)函数体内param也是一个左值:

// parameter is rvalue reference
void passValue(int &¶m)
{
  // but here expression param has an lvalue value category
  // can use std::move to convert it to an xvalue
}

如果给参数再加上const,那么情况会更复杂一点点:

// parameter is const lvalue reference
void fn(const X &) { std::cout<< "const X &\n"; }

int main()
{
  X a;
  fn(a); // works, argument is an lvalue

  fn(X()); // also works, argument is an rvalue
}

也就是说const X&既可以接收左值,也可以接收右值。可以参考这两个解释 解释1 解释2

当然,如果X&, X&&, const X&三类参数都有各自的重载函数,那调用时会优先选择最匹配的那个,如果没有X&X&&版本,那么会退而求其次调用const X&版本:

struct X {};

// overloads
void fn(X &) { std::cout<< "X &\n"; }
void fn(const X &) { std::cout<< "const X &\n"; }
void fn(X &&) { std::cout<< "X &&\n"; }

int main()
{
  X a;
  fn(a);
  // lvalue selects fn(X &)
  // fallbacks on fn(const X &)

  const X b;
  fn(b);
  // const lvalue requires fn(const X &)

  fn(X());
  // rvalue selects fn(X &&)
  // fallbacks on fn(const X &)
}

这条规则最典型的应用就是copy constructor/assignment, move constructor/assignment:

  1. const X & for copy constructor/assignment
  2. X && for move constructor/assignment

二、引用折叠

引用折叠,也就是Reference Collapsing。在理解引用折叠前,我们需要理解一个场景,就是当模板函数的参数中有模板参数的右值引用时,对函数参数类型的推导。

当模板函数的参数中有模板参数的右值引用时,这时函数参数使用起来并不是一个右值引用,它有一个新的名字,转发引用(forwarding reference)(具体可以参考C++标准委员会的文档:forwarding reference). 下面是一个判别是否是转发引用的例子:

template
void foo(T &&); // forwarding reference here
// T is a template parameter for foo

template
void bar(std::vector &&); // but not here
// std::vector is not a template parameter,
// only T is a template parameter for bar

当出现转发引用时,模板函数既可以接收左值,也可以接收右值。

  • 当接收X类型的左值时,T就会解析为X&;
  • 当接收X类型的右值时,T就会解析为X

当第一种情况出现时,函数参数就变成X& &&,这时就要引入引用折叠的规则:

  • X& & 折叠成 X&
  • X& && 折叠成 X&
  • X&& & 折叠成 X&
  • X&& && 折叠成 X&&

借助下面的例子,我们再加深理解一下:

template
void fn(T &&) { std::cout<< "template\n"; }

int main()
{
  X a;
  fn(a);
  // argument expression is lvalue of type X
  // resolves to T being X &
  // X & && collapses to X &

  fn(X());
  // argument expression is rvalue of type X
  // resolves to T being X
  // X && stays X &&
}

三、std::move

std::move主要是用来将变量转变成右值,从而可以使用move语义将变量"move"到别的地方。下面是一个使用std::move的例子:

struct X
{
  std::string s_;

  X(){}

  X(const X & other) : s_{ other.s_ } {}

  X(X && other) noexcept : s_{ std::move(other.s_) } {}
  // other is an lvalue, and other.s_ is an lvalue too
  // use std::move to force using the move constructor for s_
  // don't use other.s_ after std::move (other than to destruct)
};

int main()
{
  X a;

  X b = std::move(a);
  // a is an lvalue
  // use std::move to convert to a rvalue,
  // xvalue to be precise,
  // so that the move constructor for X is used
  // don't use a after std::move (other than to destruct)
}

所以,std::move并不做实际的move操作,它只是返回一个右值,从而使得真正干move活的重载函数能被编译器选中执行。

四、std::forward

std::forward主要用在模板函数中,并且这个模板函数的参数是转发引用(forwarding reference)。在这个模板函数中,函数参数现在是一个左值,std::forward可以将它原本的值类型提取出来,然后传给下一个调用链,也就是所谓的完美转发(perfect forwarding)。

下面是一个使用std::forward的例子:

// without std::forward
template 
void foo(T &&arg)
{
    // arg is an lvalue object of the rvalue reference type. 
    // It calls the copy constructor. 
    // vector vect is copied element-by-element to the var variable.
  std::vector var = arg; 
  ....
}

std::vector vect(1'000'000, 1);
foo(std::move(vect));


// with std::forward
template 
void foo(T &&arg)
{
    // The std::forward function is called. 
    // It returns an xvalue object with the rvalue reference type. 
    // It calls the move constructor. vector vect is moved to the var variable.
  std::vector var = std::forward(arg); 
  ....
}

std::vector vect(1'000'000, 1);
foo(std::move(vect));

从例子中可以看出,使用std::forward可以提取出模板函数参数原本的类型,从而触发move操作,提高执行效率。

整理翻译自:

1.Learn About Rvalue References | Udacity

2.C++ std::move and std::forward 

3.The std::forward function

你可能感兴趣的:(C++,c++)