【问题标题】:Forwarding reference vs const lvalue reference in template code模板代码中的转发引用与 const 左值引用
【发布时间】:2017-08-31 20:22:50
【问题描述】:

我最近一直在研究 C++ 中的转发引用,下面是我目前对该概念的理解的快速总结。

假设我有一个模板函数foo对T 类型的单个参数进行转发引用。

template<typename T>
void foo(T&& arg);

如果我使用左值调用此函数,则 T 将被推断为 T&amp;,由于引用折叠规则 T&amp; &amp;&amp; -&gt; T&amp;,arg 参数的类型为 T&amp;。

如果使用未命名的临时函数调用此函数,例如函数调用的结果,则 T 将被推断为 T,从而使 arg 参数的类型为 T&amp;&amp;。

然而,在 foo 内部,arg 是一个命名参数,因此如果我想将参数传递给其他函数并仍保持其值类别,则需要使用 std::forward。

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

据我了解,cv 限定符不受此转发的影响。这意味着,如果我使用命名的 const 变量调用 foo,那么 T 将被推断为 const T&amp;,因此由于引用折叠规则,arg 的类型也将是 const T&amp;。对于 const 右值,T 将被推导出为 const T,因此 arg 将是 const T&amp;&amp; 类型。

这也意味着,如果我在 foo 中修改 arg 的值,如果我确实将一个 const 变量传递给它,我将得到一个编译时错误。

现在谈谈我的问题。 假设我正在编写一个容器类,并希望提供一种将对象插入容器的方法。

template<typename T>
class Container
{
public:
    void insert(T&& obj) { storage[size++] = std::forward<T>(obj); }
private:
    T *storage;
    std::size_t size;
    /* ... */
};

通过使insert 成员函数对obj 进行转发引用,如果insert 实际上传递了一个临时对象,我可以使用std::forward 来利用存储类型T 的移动赋值运算符.

以前,当我对转发引用一无所知时,我会编写此成员函数并采用 const 左值引用: void insert(const T&amp; obj).

这样做的缺点是,如果 insert 被传递了一个临时对象,则此代码不会利用(可能更有效的)移动赋值运算符。

假设我没有错过任何东西。

是否有任何理由为插入函数提供两个重载?一个采用 const 左值引用,一个采用转发引用。

void insert(const T& obj);
void insert(T&& obj);

我问的原因是 the reference documentation for std::vectorstates push_back 方法有两个重载。

void push_back (const value_type& val);
void push_back (value_type&& val);

为什么需要第一个版本(采用const value_type&amp;)?

【问题讨论】:

  • 您的insert(T&amp;&amp; obj) 不是转发参考。它是由班级修复的。

标签: c++ c++11 templates


【解决方案1】:

你必须小心函数模板,而不是类模板的非模板方法。您的成员insert 本身并不是模板。它是一个模板类的方法。

Container<int> c;
c.insert(...);

我们很容易看到T在第二行没有推导出来,因为它已经在第一行固定为int,因为T是类的模板参数,而不是方法。

类模板的非模板方法,只有在一个方面与常规方法不同,一旦类被实例化:除非它们被实际调用,否则它们不会被实例化。这很有用,因为它允许模板类使用类型,而只有一些方法才有意义(STL 容器中充满了这样的示例)。

底线是,在我上面的示例中,由于T 固定为int,您的方法变为:

void insert(int&& obj) { storage[size++] = std::forward<int>(obj); }

这根本不是一个前向引用,而只是通过右值引用获取,即它只绑定到右值。这就是为什么您通常会看到像 push_back 这样的两个重载,一个用于左值,一个用于右值。

【讨论】:

  • 非常清晰简洁的答案。我知道我错过了什么!谢谢!
【解决方案2】:

@Nir Friedman 已经回答了这个问题,所以我将提供一些额外的建议。

如果您的 Container 类不用于存储多态类型(这在容器中很常见,包括 std::vector 和其他类似的 STL 容器),您可以按照您尝试的方式简化代码在你原来的例子中做。

代替:

void insert(T const& t) {
    storage[size++] = t;
}
void insert(T && t) {
    storage[size++] = std::move(t);
}

您可以通过编写以下代码来获得完全正确的代码:

void insert(T t) {
    storage[size++] = std::move(t);
}

这样做的原因是,如果正在复制对象,t 将与提供的对象一起复制构造,然后移动分配到 storage[size++],而如果正在移动对象,@ 987654327@ 将使用提供的对象移动构造,然后移动分配到storage[size++]。因此,您以一个额外的移动分配为代价简化了您的代码,许多编译器会很乐意对其进行优化。

这种方法有一个主要缺点:如果对象定义了一个复制构造函数并且没有定义一个移动构造函数(在遗留代码中的旧类型中很常见),这在所有情况下都会导致双重复制.您的编译器可能能够优化它(因为编译器可以优化到完全不同的代码,只要用户可见的效果不变),但也许不是。如果您必须使用不实现移动语义的重物,这可能会对性能造成重大影响。这可能是 STL 容器不使用这种技术的原因(它们重视性能而不是简洁)。但是,如果您正在寻找一种方法来减少您编写的样板代码的数量,并且不担心必须使用“仅复制”对象,那么这可能对您来说很好。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-02-20
    • 2021-12-25
    • 1970-01-01
    • 1970-01-01
    • 2012-01-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多