为什么我们需要参考折叠规则

时间:2013-10-05 03:04:28

标签: c++ templates c++11 rvalue-reference perfect-forwarding

每个人都可能知道我们使用rvalue-references结合参考折叠规则来构建完美的转发函数,如此

template<typename T>
void f(T&& arg) {
    otherfunc(std::forward<T>(arg));
}

f(4);

参考折叠规则类似于

+------+-----+--------+
| T    | Use | Result |
|------|--------------|
| X&   | T&  | X&     |
| X&   | T&& | X&     |
| X&&  | T&  | X&     |
| X&&  | T&& | X&&    |
+------+-----+--------+

因此,在我的示例f中,Tint&&T&&int&& &&,它会折叠为int&&

我的问题是,如果已将T推断为int&&,我们为什么需要这些规则?为什么

template<typename T>
void f(T arg);

f(4);
如果void f(int)void f(int&&)

会变成T而不是int&&?如果T确实是intT&&int&&因此void f(int&&),那么为什么我们需要参考折叠规则,因为它似乎是它们从未被应用过?这是我从有限的知识中可以看出的唯一两个选项,所以很明显有一条我不知道的规则。

从标准中引用这个引用也是有帮助的。

2 个答案:

答案 0 :(得分:1)

在您的示例中,Tint,而非int&&void f(T arg)变体将始终按值接受参数,从而制作副本。当然,这无法完美转发:otherfunc很可能通过引用获取其参数,避免复制;你甚至可以用一个不可复制的类来调用它。

另一方面,假设您调用f()传递左值,比如类C。然后TC&T&&变为C& &&折叠为C&。这样,完美转发可以保留“左值”,可以这么说。这就是崩溃规则的用途。

考虑:

#include <utility>
#include <iostream>
using namespace std;

class C {
public:
    C() : x(0) {}
    int x;
private:
    C(const C&);
};

void otherfunc(C& c) { c.x = 1; }

template<typename T>
void f(T&& arg) {
    otherfunc(std::forward<T>(arg));
}

template<typename T>
void g(T arg) {
    otherfunc(std::forward<T>(arg));
}

int main() {
    C c;
    f(c);  // OK
//  g(c);  // Error: copy constructor inaccessible

    cout << c.x;  // prints 1
    return 0;
}

答案 1 :(得分:1)

  

我的问题是,如果已经将T推断为int&amp;&amp;&amp;和

,为什么我们需要这些规则?

这不是真的。类型推导的规则不会推断该参数是参考。也就是说,在:

template <typename T>
void f(T);

表达式:

X g();
X& h();
X a;
f(g());        // argument is an rvalue, cannot be bound by lvalue-ref
f(h());        // argument is an lvalue
f(a);          // argument is an lvalue

在最后两种情况下推导出的类型将是X,并且在第一种情况下将无法编译。推导出的类型将是类型,而不是引用类型。

下一步是弄清楚如果模板通过左值或右值参考获取参数,推导出的类型是什么。对于左值引用,选项很明确,修改后的f

template <typename T>
void f(T &);

f(g());       // only const& can bind an rvalue: f(const X&), T == const int
f(h());       // f(X&)
f(a);         // f(X&)

到目前为止,它已在先前版本的标准中定义。现在的问题是,如果模板采用rvalue-references,推导出的类型应该是什么。这是C ++ 11中添加的内容。现在考虑一下:

template <typename T>
void f(T &&);

并且rvalue只会绑定到右值,而不会绑定到左值。这意味着使用与lvalue-references相同的简单规则(T将使调用编译的类型)第二次和第三次调用编译:

f(g());     // Fine, and rvalue-reference binds the rvalue
f(h());     // an rvalue-reference cannot bind an lvalue!
f(a);       // an rvalue-reference cannot bind an lvalue!

如果没有引用折叠规则,用户必须为模板提供两个重载,一个采用rvalue-reference,另一个采用左值引用。问题在于,随着参数数量的增加,替代方案的数量呈指数级增长,并且在C ++ 03中实现完美转发几乎同样难以实现(唯一的优势是能够使用rvalue-reference检测rvalue)。 / p>

因此需要做一些不同的事情,那就是引用折叠,这实际上是描述所需语义的一种方式。描述它们的另一种方式是,当您通过模板参数键入&&时,您并不真正要求 rvalue-reference ,因为这不允许使用 lvalue ,但您要求编译器为您提供最佳类型的参考匹配。