C++ -- 右值引用、move函数、完美转发

214 阅读7分钟

右值引用

移动操作:对于不可拷贝的对象,可以通过移动进行操作。同时在某些情况下,对象拷贝后就立即被销毁了,在这些情况下,移动而非拷贝对象会大幅度提升性能。 为了支持移动操作,C++11引入了一种新的引用类型——右值引用。所谓右值引用就是必须绑定到右值的引用,通过 && 而不是 & 来获得右值引用。右值以用有一个重要的性质——只能绑定到一个将要销毁的对象。右值引用可以绑定到要求转换的表达式、字面常量或者是返回右值的表达式,但是不能将一个右值引用直接绑定到一个左值上:

int i = 42;
int &r = i; // 正确:r引用i
int &&rr = i; // 错误:不能将一个右值引用绑定到一个左值上
int &r2 = i * 42; // 错误:i*42是一个右值
const int &r3 = i * 42; // 正确:我们可以将一个const的引用绑定到一个右值上
int &&rr2 = i * 42; // 正确:将rr2绑定到乘法结果上

赋值、下标、解引用和前置递增/递减运算符都是返回左值的表达式的例子,可以将一个左值引用绑定到这类表达式的结果上。 算术、关系、位以及后置递增/递减运算符,都生成右值。可以将一个const的左值引用或一个右值引用绑定到这类表达式上。
左值有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。由于右值引用只能绑定到临时对象,我们可以知道:(1)所引用的对象将要被销毁 (2)该对象没有其他用户。 这两个特性意味着:使用右值引用的代码可以自由地接管所引用对象的资源
变量是左值。 变量可以看作只有一个运算对象而没有运算符的表达式。变量表达式都是左值,这导致一个结果:我们不能将一个右值引用绑定到一个右值引用类型的变量上

int &&rr1 = 42;
int &&rr2 = rr1; // 错误:表达式rr1是左值!

move函数

虽然不能将一个右值引用直接绑定到一个左值上,但是我们可以显示地将一个左值转换为对应的右值引用类型。即通过move函数来获得绑定到左值上的右值引用。

int &&rr3 = std::move(rr1); // 正确

调用move就表示告诉编译器:有一个左值,但是希望像一个右值一样处理它。调用move就意味着承诺:除了对rr1赋值或销毁它外,我们将不再使用它。我们可以销毁一个移后源对象,也可以赋给它新值,但是不能使用一个移后源对象的值。 在使用move时,应该使用std::move而不是move这样可以避免潜在的名字冲突。

完美转发

完美转发:函数模板可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。首先举个没有实现完美转发的例子:

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标准中规定,通常情况下右值引用形式的参数只能接收右值,不能接收左值。但对于函数模板中使用右值引用语法定义的参数来说,它不再遵守这一规定,既可以接收右值,也可以接收左值(此时的右值引用又被称为“万能引用”)。 实现完美转发的例子:

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/03 标准是不支持这种用法的,而 C++ 11标准为了更好地实现完美转发,特意为其指定了新的类型匹配规则,又称为引用折叠规则(假设用 A 表示实际传递参数的类型):

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

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

forward函数

C++11 标准的开发者已经帮我们想好的解决方案,该新标准还引入了一个模板函数 forword<T>(),我们只需要调用该函数,就可以很方便地解决此问题。仍以 function 模板函数为例,如下演示了该函数模板的用法:

#include <iostream>
using namespace std;

//重载被调用函数,查看完美转发的效果
void otherdef(int & t) {
    cout << "lvalue\n";
}
void otherdef(const int & t) {
    cout << "rvalue\n";
}

//实现完美转发的函数模板
template <typename T>
void function(T&& t) {
    otherdef(forward<T>(t));
}

int main()
{
    function(5);
    int  x = 1;
    function(x);
    return 0;
}

程序执行结果:

rvalue
lvalue

注意程序中第 12~16 行,此 function() 模板函数才是实现完美转发的最终版本。可以看到,forword() 函数模板用于修饰被调用函数中需要维持参数左、右值属性的参数

总的来说,在定义模板函数时,我们采用右值引用的语法格式定义参数类型,由此该函数既可以接收外界传入的左值,也可以接收右值;其次,还需要使用 C++11 标准库提供的 forword() 模板函数修饰被调用函数中需要维持左、右值属性的参数。由此即可轻松实现函数模板中参数的完美转发。