译文

VC10中的C++0x特性 Part 2 (5) : 右值引用

翻译:飘飘白云 | 2009-06-02 15:02:57 | 阅读443 | 来源

VC10中的C++0x特性 Part 2 (5) : 右值引用

来源:vcblog 翻译:飘飘白云 kesalin@gmail.com

这个系列的第一部分( 123 )介绍了 lambda, auto static_assert
第二部分共五页:
  第一页  第二页  第三页  第四页 本页

现在,让我们来揭开“魔术“的神秘面纱,其实它靠的就是模板参数推导和引用折叠(reference collapsing)技术。

rvalue 引用:模板参数推导和引用折叠
(reference collapsing)

rvalue 引用与模板以一种特别的方式相互作用。下面是一个示例:

C:Temp>type collapse.cpp

#include <iostream>

#include <ostream>

#include <string>

using namespace std;

 

template <typename T> struct Name;

 

template <> struct Name<string> {

    static const char * get() {

        return "string";

    }

};

 

template <> struct Name<const string> {

    static const char * get() {

        return "const string";

    }

};

 

template <> struct Name<string&> {

    static const char * get() {

        return "string&";

    }

};

 

template <> struct Name<const string&> {

    static const char * get() {

        return "const string&";

    }

};

 

template <> struct Name<string&&> {

    static const char * get() {

        return "string&&";

    }

};

 

template <> struct Name<const string&&> {

    static const char * get() {

        return "const string&&";

    }

};

 

template <typename T> void quark(T&& t) {

    cout << "t: " << t << endl;

    cout << "T: " << Name<T>::get() << endl;

    cout << "T&&: " << Name<T&&>::get() << endl;

    cout << endl;

}

 

string strange() {

    return "strange()";

}

 

const string charm() {

    return "charm()";

}

 

int main() {

    string up("up");

    const string down("down");

 

    quark(up);

    quark(down);

    quark(strange());

    quark(charm());

}

 

C:Temp>cl /EHsc /nologo /W4 collapse.cpp

collapse.cpp

 

C:Temp>collapse

t: up

T: string&

T&&: string&

 

t: down

T: const string&

T&&: const string&

 

t: strange()

T: string

T&&: string&&

 

t: charm()

T: const string

T&&: const string&&

 

这里藉由 Name 的显式规格说明来打印出类型。


当我们调用 quark(up) 时,会进行模板参数推导。 quark() 是一个带有模板参数 T 的模板函数,但是我们还没有为它提供显式的类型参数(比如像 quark<X>(up)这样的)。通过比较函数形参类型 Type&& 和函数实参类型(一个 string 类型的 lvalue)我们就能推导出模板实参类型。(译注:原文用 argument 表示实参,parameter 表示形参


C++0x 会转换函数实参类型和形参类型,然后再进行匹配。


首先,转换函数实参的类型。这遵循一条特殊规则(提案N2798 14.8.2.1[temp.deduct.call]/3):如果模板形参类型为 T,函数形参类型为 T&& ,且函数实参类型为 A 的 lvalue,那么模板实参类型会被推导为 A& 。(但这条特殊规则不适用于函数形参类型为 T&const T& 的情况,这种情况下会和 C++98/03 保持一致,另外它也不适用于函数形参类型为 const T&& 的情况)。在 quark(up) 这个例子中,依照这个规则我们会把 string 转换成 string&


然后,转换函数形参的类型。不管是 C++98/03 还是 C++0x 都会解除引用( lvalue 引用和 rvalue 引用在 C++0x 中都会被解除掉)。在前面例子的四种情形中,这样我们会把 T&& 转换成 T


于是, T 会被推导成函数实参转换之后的类型updown 都是 lvalue,它们遵循那条特殊规则,这就是为什么 quark(up)  打印出"T:string&" ,而 quark(down) 打印出 "T: cosnt string&"的原因。strange()charm() 都是右值,它们遵循一般规则,这就是为什么 quark(strange()) 打印出 "T: string" 而 quark(charm()) 打印出"T: const string" 的原因。


替换操作会在类型推导之后进行。模板形参 T 出现的每一个地方都会被替换成推导出来的模板实参类型。在 quark(string())Tstring ,因此 T&& 会是 string&& 。同样,在 quark(charm()) 中,Tconst string , 因此 T&&const string&& 。但 quark(up) 和 quark(down) 不同,它们遵循另外的特殊规则。


quark(up) 中, Tstring& 。进行替换的话 T&& 就成了 string& && ,在 C++0x 中会折叠(collapse)引用的引用,引用折叠的规则就是“lvalue 引用是传染性的”X& &, X& &&X&& & 都会被折叠成 X& ,只有 X&& && 会被折叠成 X&& 。因此 string& && 被折叠成 string& 。在模板世界里,那些看起来像 rvalue 引用的东西并不一定真的就是。 因而 quark(up) 被实例化为 quark<string&>() ,进而 T&& 经替换与折叠之后变成 string& 。我们可以调用 Name<T&&>::get() 来验证这个。 同样, quark(down) 被实例化为 quark<const string&>() ,进而 T&& 经替换与折叠之后变成 const string& 。在 C++98/03中,你可能习惯了常量性(constness)隐藏于模板形参中(也就是说可以传 const Foo 对象作实参来调用形参为 T& 的模板函数,就像 T& 会是 const Foo& 一样),在 C++0x 中,左值属性(lvalueness) 也能隐藏于模板形参中。


那好,这两条特殊规则对我们有什么影响?在 quark() 内部,类型 T&& 有着和传给 quark() 的实参一样的左/右值属性(lvalueness/rvalueness)和常量性。这样 rvalue 引用就能保持住左右值属性和常量性,做到完美转发。


完美转发: std::forward() 和 std::identidy 是怎样工作的


让我们再来看看 outer() :


template <typename T1, typename T2> void outer(T1&& t1, T2&& t2) {

    inner(Forward<T1>(t1), Forward<T2>(t2));

}


现在我们明白了为什么 outer() 的形参是 T1&&T2&& 类型的了,因为它们能够保持住传给 outer() 的实参的信息。那为什么这里要调用 Forward<T1>()Forward<T2>() 呢?还记得么,具名 lvalue 引用和具名 rvalue 引用都是 lvalue 。如果 outer() 调用 inner(t1, t2) ,那么 inner() 总是会当 lvalue 来引用 t1t2 ,这就破坏了完美转发。


幸运的是,不具名 lvalue 引用是 lvalue,不具名 rvalue 引用还是 rvalue 。因此,为了将 t1 和 t2 转发给 inner(),我们需要将它们传到一个帮助函数中去,这个帮助函数移除它们的名字,保持住它们的属性信息。这就是 std::forward() 做的事情:


template <typename T> struct Identity {

    typedef T type;

};


template <typename T> T&& Forward(typename Identity<T>::type&& t) {

    return t;

}


当我们调用 Forward<T1>(t1)Identidy 并没有修改 T1 (很快我们讲到 IdentidyT1 做了什么)。因此 Forward<T1>() 接收 T1&& ,返回 T1&& 。这样就
移除了 t1 的名字,保持住 t1 的类型信息(而不论 t1 是什么类型, string& 也好, const string& 也好, string&& 也好或 const string&& 也好)。这样 inner() 看到的 Forward<T1>(t1) ,与 outer() 接收的第一个实参有着相同的信息,包括类型,lvalueness/rvalueness,常量性等等。完美转发就是这样工作的。


你可能会好奇如果不小心写成 Forward<T1&&>(t1) 又会怎样呢?(这个错误还是蛮诱人的,因为 outer() 接收的就是 T1&& t1 )。很幸运,没什么坏事情会发生。 Forward<T1&&>() 接收与返回的都是 T1&& && ,这会被折叠成 T1&& 。于是,Forward<T1>(t1)Forward<T1&&>(t1) 是等价的,我们更偏好前者,是因为它要短些。


Identidy 是做什么用的呢?为什么下面的代码不能工作?


template <typename T> T&& Forward(T&& t) { // BROKEN

    return t;

}


如果 Forward() 像是上面那样,它就能被隐式调用(不带明确的模板参数)。当我们传给 Forward() 一个 lvalue 实参时,模板参数推导就介入了,如我们前面看到的那样会将 T&& 变成 T&,也就是变成一个 lvalue 引用。问题来了,即使形参 T1&&T2&& 指明是 rvalue 引用,但在 outer() 中,具名的 t1t2 却是 lvaue ,这个问题是我们一直想要解决的!使用上面那个错误的实现, Forward<T1>(t1) 是可以工作的,而 Foarward(t1) 虽然能通过编译(很诱人哦)但会出错,就如它就是 t1 一样。真是痛苦的源泉啊,因此,Identity 被用来阻止模板参数推导typename Identity<T>::type 中的那对冒号就像绝缘体,模板参数推导无法穿越它,有模板编程经验的程序员应该对此很熟悉了,因为这在 C++98/03 和 C++0x 中是一样的。(要解释这个是另外的事情了)


move 语意: std::move() 是怎样工作的


现在我们已经学习了模板参数推导和引用折叠的特殊规则,让我们再来看看 std::move() :


template <typename T> struct RemoveReference {

     typedef T type;

};

 

template <typename T> struct RemoveReference<T&> {

     typedef T type;

};

 

template <typename T> struct RemoveReference<T&&> {

     typedef T type;

};

 

template <typename T> typename RemoveReference<T>::type&& Move(T&& t) {

    return t;

}


RemoveReference 机制基本上是复制 C++0x <type_traits> 中的 std::remove_reference 。举例来说,RemoveReference<string>::type , RemoveReference<string&>::typeRemoveReference<string&&>::type 都是 string


同样, move() 机制也基本上是复制 C++0x <utility> 中的 std::move()

· 当调用 Move(string), string 是一个 lvalue 时, T 会被推导为 string& ,于是 Move() 接收的就是 string& (经折叠之后)并返回 string&& (经 RemoveReference 之后)。


· 当调用 Move(const string), const string 是一个 lvalue 时, T 会被推导为 const string& ,于是 Move() 接收的就是 const string&& (经折叠之后)并返回 const string&& (经 RemoveReference 之后)。


· 当调用 Move(string), string 是一个 rvalue 时, T 会被推导为 string ,于是 Move() 接收的就是 string&& 并返回 string&&


· 当调用 Move(const string), const string 是一个 rvalue 时, T 会被推导为 const string ,于是 Move() 接收的就是 const string&& 并返回 const string&&


这就是 Move() 如何保持其参数的类型和常量性,还能把 lvalue 转换成 rvalue 的过程。


回顾

如果你想对 rvalue 引用有
更多了解,你可以去读有关它们的提案。要注意,提案与现在的决定可能已经不同了, rvalue 引用已经被整合到 C++0x 草案中来了,在那里它得到持续的改进。有些提案或已不再正确,或已过时,或已有了替代方案,就没有被采纳。无论怎样,它们还是能提供一些有用信息的。


N1377, N1385, 和 N1690 是主要的提案,N2118 包含被整合进标准草案之前的最后版本。 N1784, N1821, N2377, 和 N2439 记录了“将 Move 语意扩展到 *this ”的演化过程,这个也被整合到 C++0x 中来了,但还没有在VC10 中得到实现。


展望


N2812 “Rvalue 引用的安全问题(以及如何解决)” 提出了对初始化规则的修改,它禁止 rvalue 引用绑定到 lvalue 。 这不会影响 move 语意和完美转发,所以它不会让你刚学到的新技术失效(它只是修改了 std::move() 和 std::forward() 的实现)。


Stephan T. Lavavej

Visual C++ Libraries Developer

Published Tuesday, February 03, 2009 9:27 AM by vcblog


< 第一页  第二页  第三页  第四页 本页 >


分享:

标签:C++0x,

添加评论