现在的位置: 首页 > 综合 > 正文

c++11 rvalue reference & perfect forwarding

2013年10月01日 ⁄ 综合 ⁄ 共 5390字 ⁄ 字号 评论关闭
文章目录

简介

右值引用,是c++11中为了解决大对象拷贝性能问题,以及参数传递而新加的特性。形如T&&,其中T是referenced type。

Lvalue & Rvalue

左值和右值在c语言中差不多可以表述为分别出现在表达式左右两侧,但是在c++中,因为引入了class,情况变得更加复杂。基本上可以总结为:
  1. 左值可以运用&操作符取得地址,注意临时对象是无法取得地址的,因为很容易导致问题。
  2. 左值必然有一个名字
  3. 不是左值的是右值
左值和右值只是表达式的属性。还有一个概念需要理解就是类型type,类型和左右值不是一个概念,马上就会讲解。
我们来看几个例子,理解一下左右值的概念。
int f();
int i;
int& g();
int&& h();

&f; // f is lvalue, &f returns to pointer to the function
f(); // f() is rvalue, as f returns a int by value
i;  // i is lvalue
g(); // g() is lvalue, as f returns a lvalue reference to int
h(); // h() is rvalue, as f returns a rvalue reference to int

void f(int&& i)
{
    // i is a rvalue reference, but i is a lvalue as named rvalue reference is lvalue
}

这里需要注意的是函数的返回值,一个函数调用通常在返回值是左值引用的时候才是左值(见n3242 5.2.2/10,下面截取标准中的文字以供参考)。

5.2.2/10
A function call is an lvalue if the result type is an lvalue reference type or an rvalue reference to function
type, an xvalue if the result type is an rvalue reference to object type, and a prvalue otherwise.
还有一个需要注意的是,一个命名的右值引用是左值,即最后一个给出的情况(见n3242 5/6,下面给出标准中的文字供参考)。
5/6
In general, the effect of this rule is that named rvalue references are treated as lvalues and unnamed rvalue
references to objects are treated as xvalues; rvalue references to functions are treated as lvalues whether
named or not

Reference

左值引用形如T&,而右值引用形如T&&,并且我们知道右值引用可以绑定到右值,那么我们时候可以绑定到一个右值常量?因为常量是不可修改的,但是由于T&&不是reference to const,所以是否成立?

答案是可以的,请看如下例子:

#include <iostream>
using namespace std;

int main()
{
    int&& rri = 5;
    rri = 4;
    cout << rri << endl;

    // error
    // char const*&& rrcc = "hello";
    // *rrcc = '1';
}

这里在g++4.7.3中运行后可以发现rri的值是4。

这里涉及到reference的初始化,标准中规定,对于使用右值来初始化一个右值引用,可以创建一个拷贝,即,这里5会被保存在一个对象中,所以这里我们对这个对象进行修改。标准中8.5.3/5中如下描述(部分):

8.5.3/5

— Otherwise, the reference shall be an lvalue reference to a non-volatile const type (i.e., cv1 shall be
const), or the reference shall be an rvalue reference.

    If the initializer exrepssion is xvalue or ...

    — Otherwise, a temporary of type “cv1 T1” is created and initialized from the initializer expression
    using the rules for a non-reference copy-initialization

再看下面注释中的代码,由于rrcc的类型是reference to pointer to const char,所以我们在后面对char const*赋值时出错。

Move Constructor

我们来看一个move ctor的例子:
class foo
{
public:
    foo(foo&& f)
        : m_s(f.m_s) // m_s(move(f.m_s))
    {
    }
private:
    string m_s;
};

这里,foo(foo&&)是move ctor,由于f是右值引用,我们认为,我们可以通过直接调用string的move ctor而不做任何处理。这是错误的。结果这里只有string的copy ctor被调用。

原因很简单,上面我们已经提到,对于一个named rvalue reference,它是一个lvalue,由于rvalue不能绑定lvalue,所以lvalue只能绑定到string的copy ctor。而我们的愿意是要move,而不是copy,所以这里就需要使用标准库的std::move函数,将左值转化成右值引用。而对于一个unamed rvalue reference,它是一个右值,这样就可以调用string的move ctor。

std::move

std::move所做的事情很简单,只是通过static_cast来将一个左值转换为右值引用,即
static_cast<T&&>(v)

当然具体实现会复杂一下,不过请继续接着看。

Perfect Forwarding

perfect forwarding需要解决的问题是爆炸式函数重载。因为我们有了move ctor之后,我们显然会在函数参数声明时将参数声明为T&&,否则如果还是使用T const&来声明,那么我们将不能move ctor,这样就没有什么意义了。但是问题在于,用户可能传入一个右值,或者一个左值,那么我们可以重载:
void f(foo const& a);
void f(foo&& a);

但是当参数数量变多时怎么办,假设有N个参数,那么显然我们需要重载2^N个函数才能解决问题,所以引入了perfect forwarding。

function template

在使用perfect forwarding时,我们需要结合函数模板和右值引用,即
void g(int const&);
void g(int&&);

template<typename T>
void f(T&& v)
{
    g(forward<T>(v));
}

这里标准库函数forward完成了类型转发。抱枕传递给的g的类型的左右值属性是用户传入的属性。

注意,当且仅当参数为T&&,才会触发perfect forwarding,引述标准的文字
14.8.2.1/3
If P is a cv-qualified type, the top level cv-qualifiers of P’s type are ignored for type deduction. If P is a reference type, the type referred to by P is used for type deduction. If P is an rvalue reference to a cv-unqualified template parameter and
the argument is an lvalue, the type “lvalue reference to A” is used in place of A for type deduction.
具体这段话的意思请继续看。

Reference Collapsing Rule

我们知道在c++中引用的引用是非法,所以标准中引入如下规则,
  1. T& && = T&
  2. T&& & = T&
  3. T& & = T&
  4. T&& && = T&&

这里的意思是,当一个类型,比如T& &&时,最终得到的类型是T&。可以看出,仅当T&& &&的情况,类型才是右值引用,其他情况都是左值引用。

Deduction

在讲解forwarding的原理前,先来了解下几个模板推导中用到的术语,表中中定义P,就是函数模板的参数类型,A是函数模板调用时的用户给出的类型。
推导的目的就是为了匹配P和A,来解析出T。Deduced A就是P经过转换后的类型(见标准的14.8.2.1),transformed A是当P为特定条件时,变换后得到的类型。
回到前面perfect forwarding的讲解,先来看函数模板f如何推导template parameter T。下面给出标准中的例子:
Template <class T> int f(T&&);
template <class T> int g(const T&&);
int i;
int n1 = f(i); // calls f<int&>(int&)
int n2 = f(0); // calls f<int>(int&&)
int n3 = g(i); // error: would call g<int>(const int&&), which
               // would bind an rvalue reference to an lvalue

先来看f(i)的调用,根据14.8.2.1/3,我们有

  1. P = T&&,由于P是右值引用,所以Deduced A = T
  2. 由于P是右值引用,并且i是左值,并且i的类型是int,所以Transformed A = int&
  3. Deduced A = Transformed A => T = int&

所以我们将T = int&代入f,得到f<int&>(int& &&),根据reference collapsing rule,我们可以得出f的参数类型是int&,并且是左值,是我们要的。

再来看f(0)的调用,我们有
  1. P = T&&,由于P是右值引用,所以Deduced A = T
  2. 0是右值,所以Transformed A = int
  3. Deduced A = Transformed A => T = int

所以最后得到f的签名为f<int>(int&&),参数类型是右值引用。

至此保留了用户参数的左右值属性。
对于最后一个例子,大家可以自己尝试推导。

std::forward

这里给出实现,forward实际有2个重载,但是这里我们只关心其中一个对左值引用的重载,因为我们知道,命名参数是左值,所以传递给forward的必然是左值。
template<typename T>
T&& forward(std::remove_reference<T>::type& v)
{
    return static_cast<T&&>(v);
}

又是一个函数模板。假设我们指定T = int&,那么将会有:

  1. remove_reference<T>::type& = int&
  2. static_cast<int& &&>(v) = static_cast<int&>(v)
  3. 返回 int& && = int&

这里再一次用到了reference collapsing rule,我们将T指定为int&,返回值也是int&,perfect!!! forwarding。

假设我们指定T = int(这里我们也可以指定T = int&&,但是通常不这么写),有
  1. remove_reference<T>::type& = int&,这里还是左值引用,没错,还记得我们传递给forward的也是左值吗?虽然它是右值引用。
  2. static_cast<int&& &&>(v) = static_cast<int&&>(v)
  3. 返回int&& && = int&&

perfect!!!。

至此,我们完成了完美转发,在没有改变参数类型的情况下,将参数传递给了另外一个函数。

Explictly specifiec template parameter

有的读者可能注意到了,我们在调用forward时,显式指定了模板参数T,为什么?先来看另一种forward的定义:
template<typename T>
T&& forward(T& v)
{
    return static_cast<T&&>(v);
}

我们还是可以推导出,这里也能够实现完美转发(有的读者可能认为无法对参数是const的对象进行转发,事实不是如此,我们尽在f(T&&)内部使用,所以不存在这个问题,可以试着自己推导)。

但是如果一旦不指定T,而是让模板自动推导,那么根据前面我们学到的,假设f为:
template<typename T>
void f(T&& v)
{
    g(forward(v));
}

假设我们传入一个左值int,那么v的类型就是int&,而传入forward的v的类型在进行推导前会被变成int(根据标准中5.5),所以我们推导出forward的T = int,

最后forward将会返回int&&。出现问题了,左值变成了右值。
标准之所以显式让我们指定T就是为了防止模板自动推导导致的问题。

引用文档

读者若有兴趣,可以参考以下文档,我从中受益匪浅,这里写的也差不多只是摘录:
  1. N3242,c++标准草案,有更新的版本,自行google。
  2. C++ Rvalue Reference Explained
  3. ACCU Overload 111 中的 Universal Reference

抱歉!评论已关闭.