首先我们来看这么一段小程序:
class Widget { }; void f(Widget&) { cout << "f(Widget&)" << endl; } void f(Widget&&) { cout << "f(Widget&&)" << endl; } int main() { Widget&& w1 = Widget(); Widget& w2 = w1; f(w1); f(w2); } /** * output: * * f(Widget&) * f(Widget&) * /
结果出乎意料,两个都调用了void f(Widget&)
。此外,我们居然可以用类型为Widget&&
的w1
来初始化w2
!
显然的,我们知道,一个 non-const lvalue reference 不能绑定至右值, 但既然这里的 w2
是合法的,那么,我们几乎可以肯定, w1
不是一个右值,即便他的类型确确实实是 Widget&&
。
Scott Meyers 《Effective Modern C++》的第2页便强调了一个事实:
“一个表达式的类型与它是左值还是右值无关”(The type of an expression is independent of whether the expression is an lvalue or rvalue)。
此外,C++标准 [ISO/IEC 14882 2011] 第 87 页有这么一句话:
“……named rvalue 被当做lvalue看待,而unnamed rvalue则当成xvalue……”(…named rvalue references are treated as lvalues and unnamed rvalue references to objects are treated as xvalues…)
如此,我们似乎可以猜测,之所以 w2
的初始化是合法的,仅仅是因为w1
被当成了lvalue。也因为如此,Widget&& w3 = w1
这样的表达式是不合法的,我们不能把一个 rvalue reference 绑定至一个 lvalue。
那么,问题又来了,w1
是怎么回事?他到底是什么?lvalue?还是 rvalue?这个问题其实没有太大意义,上面已经说明,无论如何,w1
始终会被解释为 lvalue。也就是说,他是事实上的 lvalue:
Widget w3;
w1 = w3; // w1 is evaluated to a lvalue
至于这样的初始化语法为什么是合法的,可以看看这样一个例子:
void foo(Widget&&) { }
foo(Widget());
对于这个,我相信会比较容易使人信服的多。但实际上,函数实参的初始化,和变量的初始化规则是一样的。利用《Effective Modern C++》中 Item 4 介绍的技巧,我们可以通过让编译器告诉我们,在他世界里,都有些什么:
template <typename T>
class Type<T>;
template <typename T>
void f(T&& t) {
Type<T> type;
}
int main() {
f(Widget());
}
以下是 gcc-c++ 5.3.1 的输出:
由于T
的类型为Widget
,所以实参t
的类型确确实实是 Widget&&
并且 initializer 的类型为Widget
。从而我们可以推断,这样的初始化式,只不过是将 rvalue reference 绑定至一个临时变量而已,这个也与我们熟知的语法规则一致。w1
在上面的例子中所表现的行为,并不是因为 reference collapse 而导致他变成了一个 lvalue reference,而是语法规则让他被当成了 lvalue。
下面这两段小程序可以证明上面的推断:
void foo(Widget&&) {
cout << "foo(Widget&&)" << endl;
}
void foo(const Widget&) {
cout << "foo(const Widget&)" << endl;
}
foo(Widget{}); // foo(Widget&&)
而换成下面这两个函数时:
void foo(Widget&) { // param type is now Widget&
cout << "foo(Widget&)" << endl;
}
void foo(const Widget&) { // as before
cout << "foo(const Widget&)" << endl;
}
foo(Widget{}); // foo(const Widget&)
error: invalid initialization of non-const reference of type 'Widget&' from an rvalue of type 'Widget'
”说到这里,突然有种被骗的感觉,Widget&& w = Widget()
,里,Widget()
生成一个临时变量,而后 w
绑定至该临时变量。顺理成章,不是吗?那……下面这几个呢?特别是,第一个和第三个。事实上,他们和上面所讨论的是一个东西。
Widget&& w{};
Widget&& w2(Widget{});
foo({});
foo(Widget{});
另外,这里还有点值得注意的地方。虽然 w1
的类型为 Widget&&
,但实际调用的却依然是 f(Widget&)
。从这里我们可以判断,涉及 rvalue reference时,在 overload resolution 过程中,并非凭变量的静态类型就确定重载的函数,而是会根据实际“计算”(根据语法规则计算)所得的结果选择适当的函数:
Widget makeWidget() {
return Widget();
}
void f(Widget&) {
cout << "f(Widget&)" << endl;
}
void f(const Widget&) {
cout << "f(cosnt Widget&)" << endl;
}
void f(Widget&&) {
cout << "f(Widget&&)" << endl;
}
f(makeWidget()); // f(Widget&&)
这里,由于makeWidget()
所返回的是一个不具名的右值(unnamed rvalue),所以,也就不会被解释为 lvalue,从而最终调用的是 f(Widget&&)
。
现在,我们可以来谈谈 std::forward
了。
假设有这么一个函数模板,他昨晚任意函数的wrapper,执行权限检查任务。
void checkPermission() {}
template <typename Function, typename... Args>
decltype(auto) // C++ 14
secureCall(Function fn, Args&&... args) {
checkPermission();
return fn(forward<Args>(args)...);
}
这的问题是,既然 T&&
是所谓的 universal reference,为什么我们需要使用 std::forward
?
答案很简单,在讨论上一个问题的时候,就已经给出了。尽管这里的args
能够保佑调用者调用时的类型,但当其为 rvalue reference 且我们以其作为参数调用 fn
时,他们将会被当成 lvalue!于是,我们使用 std::forward
将其 cast 回原来的 rvalue。这个其实也是 Scott 在 《Effective Modern C++》(第158页)中强调的一个事实:
函数实参永远是lvalue,即使它的类型是一个rvalue reference (a parameter is always an lvalue, even if its type is an rvalue reference)