【译】VC10中的C++0x特性 Part 2 (3):右值引用
来源:vcblog 作者:Stephan T. Lavavej 翻译:飘飘白云
(转载时请注明作者和出处。未经许可,请勿用于商业用途)
简介
这一系列文章介绍Microsoft Visual Studio 2010 中支持的C++ 0x特性,目前有三部分。
Part 1 :介绍了Lambdas, 赋予新意义的auto,以及 static_assert;
Part 2( 一 , 二 , 三 ):介绍了右值引用(Rvalue References);
Part 3:介绍了表达式类型(decltype)
VC10中的C++0x特性 Part 1,2,3 译文打包下载(doc 和 pdf 格式): 点此下载
本文为 Part 2 的第三页
转发问题
在程序员不用写高度泛化的代码的时候,C++98/03 的 lvalue, rvalue, 引用,还有模板看起来是很完美的。假设你要写一个完全泛化的函数 outer(),这个函数的目的是将任意数目个任意类型的参数传递(也就是“转发”)给函数 inner()。已有很多不错的解决方案,比如 factory 函数 make_shared<T>(args) 是把 args 传给 T 的构造函数,然后返回 shared_ptr<T>。(这样就把 T 对象和用于对它进行引用计数的代码存储到同一块动态内存中,性能上与侵入式引用计数一样好); 而像 function<Ret(args)> 这样的包装类是把参数传给其内部存储的函数对象(functor),等等。在这篇文章里,我们只对 outer() 是如何把参数传递给 inner() 这部分感兴趣。至于 outer() 的返回类型是怎么决定的是另外的问题(有时候很简单,如 make_shared<T>(args) 总是返回 shared_prt<T>,),但要在完全搞定这个问题的一般化情况,你就要用到 C++0x的 decltype 特性了)。
如果不带参数,就不存在这样的问题,那么带一个参数情况呢?让我们尝试写个 outer() :
template <typename T> void outer(T& t) {
inner(t);
}
问 题来了,如果传给它的参数是非常量 rvalue,那我们就无法调用 outer()。如果 inner() 接收 const int& 型的参数,那 inner(5) 是可以通过编译的,但是 outer(5) 就编译不过了。因为 T 会被推导为 int, 而 int& 是不能绑定到常量 5 的。
好吧,让我们试试这个:
template <typename T> void outer(const T& t) {
inner(t);
}
如果 inner()接收 int& 型参数,那就会违法 const 正确性,编译都过不了。
现在,你可以重载两个分别带 T& 和 const T& 参数的 outer(),这确实管用。当你调用 outer()时,就像直接调用 inner() 一样。
可惜的是,这中方法在多参数的情况下就麻烦了(译注:要写的重载函数太多了)。你就得为每一个参数像 T1& 和 const T1&, T2& 和 const T2& 等这样进行重载,要重载的函数数目呈指数级增长。(VC9 SP1 的 tr1::bind() 就够让人感到绝望了,它为 5 个参数这么重载出了 63 个函数。如果不这么蛮干的话,没有像这里的长篇累述,我们就很难跟使用者解释为什么不能调用用 1729 这样的 ravlue 做参数的函数。为了产生出这些重载函数使用了令人作呕的预处理机制,恶心到你都不想知道它)。
在 C++98/03 中,转发问题是很严重的,而且本质上无解(必须求助于恶心的预处理机制,这会严重拖慢编译速度,还让代码变得难以阅读)。总算, rvalue 优雅地解决了这个问题。
完美转发: 模式
完美转发让你能简单而清晰地只写一个模板函数就可以转发所有的参数给任意函数,不管它带几个参数,也不管参数类型是什么。而且参数的非常量/常量, lvalue/rvalue 属性都能得以保留,让你可以像使用 inner() 一样使用 outer(),还可以和 move 语意一起用从而获得额外的好处。( C++0x 的变长模板技术解决了“任意数目”这部分,我们在这里把 N 看做任意数目)。乍看之下很神奇,实际上很简单:
C:\Temp>type perfect.cpp
#include <iostream>
#include <ostream>
using namespace std;
template <typename T> struct Identity {
typedef T type;
};
template <typename T> T&& Forward(typename Identity<T>::type&& t) {
return t;
}
void inner(int&, int&) {
cout << "inner(int&, int&)" << endl;
}
void inner(int&, const int&) {
cout << "inner(int&, const int&)" << endl;
}
void inner(const int&, int&) {
cout << "inner(const int&, int&)" << endl;
}
void inner(const int&, const int&) {
cout << "inner(const int&, const int&)" << endl;
}
template <typename T1, typename T2> void outer(T1&& t1, T2&& t2) {
inner(Forward<T1>(t1), Forward<T2>(t2));
}
int main() {
int a = 1;
const int b = 2;
cout << "Directly calling inner()." << endl;
inner(a, a);
inner(b, b);
inner(3, 3);
inner(a, b);
inner(b, a);
inner(a, 3);
inner(3, a);
inner(b, 3);
inner(3, b);
cout << endl << "Calling outer()." << endl;
outer(a, a);
outer(b, b);
outer(3, 3);
outer(a, b);
outer(b, a);
outer(a, 3);
outer(3, a);
outer(b, 3);
outer(3, b);
}
C:\Temp>cl /EHsc /nologo /W4 perfect.cpp
perfect.cpp
C:\Temp>perfect
Directly calling inner().
inner(int&, int&)
inner(const int&, const int&)
inner(const int&, const int&)
inner(int&, const int&)
inner(const int&, int&)
inner(int&, const int&)
inner(const int&, int&)
inner(const int&, const int&)
inner(const int&, const int&)
Calling outer().
inner(int&, int&)
inner(const int&, const int&)
inner(const int&, const int&)
inner(int&, const int&)
inner(const int&, int&)
inner(int&, const int&)
inner(const int&, int&)
inner(const int&, const int&)
inner(const int&, const int&)
两行!完美转发只用了两行!够简洁吧!
这个例子示范了怎么把 t1 和 t2 从 outer() 透明地转发给 inner(); inner() 可以知道它们的非常量/常量, lvalue/ravlue 属性,就像inner是被直接调用的那样。
跟 std::move() 一样, std::identify 和 std::forward() 都是在 C++<utility> 中定义的( VC10 会有, VC10 CTP中没有)。我将演示怎么来实现它们。(再次,我将交替使用 std::identity 和我的 Identity, std::forward() 和我的 Forward(),因为他们的实现是等价的。)
现在,让我们来揭开“魔术“的神秘面纱,其实它靠的就是模板参数推导和引用折叠(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
然后,转换函数形参的类型。不管是 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
翻译:飘飘白云
(转载时请注明作者和出处。未经许可,请勿用于商业用途)
< 第一页, 第二页, 本页>