我应该通过const引用还是通过模板函数中的值传递?

时间:2017-07-03 11:36:45

标签: c++

当选择通过const引用而不是按值传递时,大型类的选择似乎很明确:必须使用const ref来避免昂贵的副本,因为只有{{3才允许复制省略(如果复制构造函数没有副作用,或者副本来自右值)。

对于limited circumstances来说,通过价值似乎更具吸引力:

  • 它与解除引用一样快
  • 它避免了别名(这既容易出错也不利于性能,因为它会迫使编译器更加保守)。
  • 无论如何都需要副本,它会在编译器可以使用copy elision的范围内制作副本

那么在编写模板函数时最好的做法是什么,哪个参数的类是大还是小并不总是很明显?

3 个答案:

答案 0 :(得分:4)

(假设您只想读取您传递的值。)

我认为最好的选择是通过const&,因为:

  • 某些对象无法复制,复制成本高昂,或者在复制构造函数中包含副作用。

  • 虽然按const& 采用原语可能会导致性能下降,但与上述要点中描述的问题相比,损失较小。

理想情况下,您可能希望执行类似的操作(我不小心这里的复制构造函数中有副作用的小类)

template <typename T>
using readonly = std::conditional_t<
    sizeof(T) <= sizeof(void*),
    T,
    const T&
>;

template <typename T>
void foo(readonly<T> x);

问题在于T无法通过对foo的调用推断出foo<int>(0),因为它位于"non-deducible context"

这意味着您的用户必须拨打foo(0)而不是T=int,因为T无法从编译器中推断出来。

(我想重申一下,我上面使用的条件非常幼稚且有潜在危险。您可能只想检查void*是原始的还是小于{{1}的POD而不是。)

您可以做的另一件事是使用std::enable_if_t来控制调用哪个函数:

template <typename T>
auto foo(T x) -> std::enable_if_t<(sizeof(T) <= sizeof(void*))>;

template <typename T>
auto foo(const T& x) -> std::enable_if_t<(sizeof(T) > sizeof(void*))>;

live example on wandbox

这显然需要大量额外的样板...当(如果?)我们将获得constexpr块和“元类”时,使用代码生成可能会有更好的解决方案

答案 1 :(得分:2)

我认为,作为基本规则,您应该通过const&,为一般情况下的昂贵复制对象准备通用模板代码。

例如,如果您查看std::vector's constructors,在带有计数和值的重载中,该值只是通过const&传递:

explicit vector( size_type count, 
                 const T& value = T(),
                 const Allocator& alloc = Allocator())

答案 2 :(得分:1)

你想要的是一种类型特征,它测试类型是否为 scalar ,然后启用

template <typename Type>
using ConstRefFast = std::conditional_t<
    std::is_scalar<std::decay_t<Type>>::value,
    std::add_const_t<Type>,
    std::add_lvalue_reference_t<std::add_const_t<std::decay_t<Type>>>
>;

然后像这样通过引用传递一个对象

template <typename Type>
void foo(typename ConstRefFast<Type>::type val);

请注意,这意味着该功能将无法再自动推断出T类型。但在某些情况下,它可能会给你你想要的东西。

请注意,当涉及到模板函数时,有时候所有权问题比仅仅想要通过const ref或值传递值更重要。

例如,考虑一种接受shared_ptr到某种类型T并对该指针进行一些处理的方法(在将来的某个时刻或立即),你有两个选项

void insert_ptr(std::shared_ptr<T> ptr);
void insert_ptr(const std::shared_ptr<T>& ptr);

当用户查看这两个函数时,一个人清楚地传达了意义和语义,而另一个只是在他们的脑海中留下了问题。第一个显然在方法开始之前复制指针,从而增加引用计数。但第二个人的语义并不十分清楚。在异步设置中,这可能会在用户的脑海中留下怀疑的余地。我的指针是否会被安全地使用(并且对象被安全地释放),例如,在未来的某个时刻异步使用指向的对象?

您还可以考虑另一种不考虑异步设置的情况。将类型T的值复制到内部存储

的容器
template <typename T>
class Container {
public:
    void set_value(T val) {
        this->insert_into_storage(std::move(val));
    }
};

template <typename T>
class Container {
public:
    void set_value(const T& val) {
        this->insert_into_storage(val);
    }
};

这里第一个确实传达了将值复制到方法中的事实,之后容器可能会在内部存储该值。但是如果复制对象的生命周期问题并不重要,那么第二个问题就更有效了,因为它避免了额外移动

因此,最终归结为您是否需要清晰的API或性能。