模板代码

时间:2017-08-31 20:22:51

标签: c++ c++11 templates

我最近一直在研究C ++中的转发引用,下面简要概述了我目前对该概念的理解。

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

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); )?

2 个答案:

答案 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++]。因此,您只需花费一次额外的移动分配就可以简化代码,许多编译器都会很乐意将其优化出来。

但是,这种方法存在一个主要的缺点:如果对象定义了一个复制构造函数并且没有定义一个移动构造函数(对于旧代码中的旧类型通用),这会产生双重复制所有情况。您的编译器可能能够优化它(因为只要用户可见的效果不变,编译器就可以优化到完全不同的代码),但可能没有。如果您必须使用不实现移动语义的重型对象,那么这可能是一个重大的性能损失。这可能是STL容器不使用这种技术的原因(他们重视性能而不是简洁)。但是,如果您正在寻找一种方法来减少您编写的样板代码的数量,并且不担心必须使用&#34;仅复制&#34;对象,那么这对你来说可能会很好。