2017-08-31 73 views
4

常量左值参考我最近一直在寻找到转发引用C++和下面是我目前的概念的理解一个简单的总结。转发参考VS模板代码

比方说,我有一个模板功能foo采取的转发参考T类型的一个参数。

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

如果我把这种功能与左值然后T将推导出T&使arg参数是T&类型的由于参考塌陷规则T& && -> T&

如果该函数获得与无名暂时的,称为诸如函数调用的结果,然后T将推导出T使arg参数是T&&类型。

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

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

据我了解,cv-qualifiers不受此转发影响。这意味着,如果我调用foo与命名常量变量然后T将推导出const T&以及因此arg类型也将是const T&由于参考塌陷规则。为常量右值T将推导出const T因此argconst T&&类型。

这也意味着如果我在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我可以使用std::forward如果insert是INFACT传递一个临时对象取所存储的类型T的移动赋值操作的优点。

以前,当我不知道任何关于转发引用时,我会写这个成员函数采用一个常量左值引用: void insert(const T& obj)

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

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

是否有任何理由,提供两个重载插入功能?一个采取常数左值参考,另一个参考转发参考。

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

我问的原因是the reference documentation for std::vector指出push_back方法有两种重载。

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

为什么第一个版本(采取const value_type&)需要的?

+5

'insert(T && obj)'不是转发引用。它由班级固定。 – Jarod42

回答

4

您必须小心功能模板,而非类模板的非模板方法。您的会员insert本身不是模板。这是一个模板类的方法。

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

我们可以很容易地看到,T不是推断在第二行,因为它已经固定int在第一行,因为T是类的模板参数,而不是方法。

类模板的非模板方法,只有在类被实例化后才会以一种方式区别于常规方法:除非实际调用它们,否则它们不会被实例化。这很有用,因为它允许模板类使用类型,对于这些类型只有一些方法有意义(STL容器充满了这样的例子)。

的底线是,在我上面的例子中,由于T固定为int,你的方法就变成了:

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

这不是一个forwaring参考可言,而只是通过右值引用需要,即它只绑定到右值。这就是为什么你通常会看到两个重载,例如push_back,一个用于左值,另一个用于右值。

+0

非常清晰和简洁的答案。我知道我错过了一些东西!谢谢! – JonatanE

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++]。所以你已经简化了你的代码,只需要一个额外的移动分配代码,许多编译器会很高兴地进行优化。但是,这种方法有一个主要缺点:如果对象定义了一个复制构造函数并且没有定义一个移动构造函数(对于旧代码中的旧类型是常见的),那么这会导致所有的复制副本案例。您的编译器可能能够优化它(因为编译器可以优化为完全不同的代码,只要用户可见的效果不变),但可能不会。如果你不得不使用不实现移动语义的重对象,那么这可能是一个重大的性能问题。这可能是STL容器不使用这种技术的原因(他们重视性能而不是简洁)。但是,如果您正在寻找一种方法来减少您编写的样板代码的数量,并且不担心必须使用“仅复制”对象,那么这对您来说可能会很好。