Widget&& w{} 究竟表示什么?这跟 std::forward 又有什么关系

首先我们来看这么一段小程序:

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 的输出:

Widget&& w{} 究竟表示什么?这跟 std::forward 又有什么关系_第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&)
  1. 同时存在 rvalue reference 和 const lvalue reference,优先选择 rvalue reference
  2. 只有 const lvalue reference 能够绑定至该临时变量,也就是说,去掉该函数后(也就是说,只留下non-const lvalue reference 版本),编译器将会抱怨说:“ 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)





你可能感兴趣的:(C++)