VC10中的C++0x特性 Part 2 (5) : 右值引用
来源:vcblog 翻译:飘飘白云 kesalin@gmail.com
这个系列的第一部分( 1, 2, 3 )介绍了 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 会被推导成函数实参转换之后的类型。up 和 down 都是 lvalue,它们遵循那条特殊规则,这就是为什么 quark(up) 打印出"T:string&" ,而 quark(down) 打印出 "T: cosnt string&"的原因。strange() 和 charm() 都是右值,它们遵循一般规则,这就是为什么 quark(strange()) 打印出 "T: string" 而 quark(charm()) 打印出"T: const string" 的原因。
替换操作会在类型推导之后进行。模板形参 T 出现的每一个地方都会被替换成推导出来的模板实参类型。在 quark(string()) 中 T 是 string ,因此 T&& 会是 string&& 。同样,在 quark(charm()) 中,T 是 const string , 因此 T&& 是 const string&& 。但 quark(up) 和 quark(down) 不同,它们遵循另外的特殊规则。
在 quark(up) 中, T 是 string& 。进行替换的话 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 来引用 t1 和 t2 ,这就破坏了完美转发。
幸运的是,不具名 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 (很快我们讲到 Identidy 对 T1 做了什么)。因此 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() 中,具名的 t1 和 t2 却是 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&>::type 和 RemoveReference<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,