我最近一直在研究C ++中的转发引用,下面简要概述了我目前对该概念的理解。
假设我有一个模板函数foo
对T
类型的单个参数进行转发引用。
template<typename T>
void foo(T&& arg);
如果我使用左值调用此函数,则T
将推断为T&
,因为参考折叠规则arg
使T&
参数属于T& && -> T&
类型1}}。
如果使用未命名的临时函数(例如函数调用的结果)调用此函数,则T
将推导为T
,使arg
参数的类型为{{ 1}}。
在T&&
内,foo
是一个命名参数,因此如果我想将参数传递给其他函数并仍然保持其值类别,我将需要使用arg
。 / p>
std::forward
据我所知,cv-qualifiers不受此转发的影响。这意味着如果我使用命名的const变量调用foo,那么template<typename T>
void foo(T&& arg)
{
bar(std::forward<T>(arg));
}
将被推断为T
,因此const T&
的类型也将是arg
,因为参考折叠规则。对于const rvalues,const T&
将推断为T
,因此const T
将为arg
类型。
这也意味着如果我在const T&&
内修改arg
的值,如果我将一个const变量传递给它,我将得到编译时错误。
现在回答我的问题。 假设我正在编写一个容器类,并希望提供一种将对象插入容器的方法。
foo
通过使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
来利用存储类型std::forward
的移动赋值运算符T
事实上,{1}}传递了一个临时对象。
以前,当我对转发引用一无所知时,我会写一个const lvalue引用的成员函数:
insert
。
这样做的缺点是,如果void insert(const T& obj)
传递了临时对象,则此代码不会利用(可能更高效)移动赋值运算符。
假设我没有错过任何内容。
有没有理由为insert函数提供两个重载?一个采用const左值参考,一个采用转发参考。
insert
我问的原因是the reference documentation for std::vector
表示void insert(const T& obj);
void insert(T&& obj);
方法有两次重载。
push_back
为什么需要第一个版本(需要void push_back (const value_type& val);
void push_back (value_type&& val);
)?
答案 0 :(得分:4)
您需要注意函数模板与类模板的非模板方法。您的会员insert
本身不是模板。它是模板类的一种方法。
Container<int> c;
c.insert(...);
我们可以非常轻松地看到T
未在第二行推断,因为它已在第一行固定为int
,因为T
是模板类的参数,而不是方法。
类模板的非模板方法,只有在实例化类之后,才会以一种方式与常规方法不同:除非实际调用它们,否则它们不会被实例化。这很有用,因为它允许模板类使用类型,只有一些方法才有意义(STL容器中充满了这样的例子)。
最重要的是,在上面的示例中,由于T
固定为int
,因此您的方法变为:
void insert(int&& obj) { storage[size++] = std::forward<int>(obj); }
这根本不是转发引用,而只是通过右值引用,即它只绑定到rvalues。这就是为什么你通常会看到push_back
之类的两个重载,一个用于左值,一个用于右值。
答案 1 :(得分:0)
@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++]
,而如果对象是移入后,t
将使用提供的对象进行移动构建,然后移动分配到storage[size++]
。因此,您只需花费一次额外的移动分配就可以简化代码,许多编译器都会很乐意将其优化出来。