C++ 避免构造函数中常量引用和右值引用的指数增长

C++ 避免构造函数中常量引用和右值引用的指数增长,c++,c++11,rvalue-reference,const-reference,c++17,C++,C++11,Rvalue Reference,Const Reference,C++17,我正在为一个机器学习库编写一些模板类,并且我经常遇到这个问题。我主要使用策略模式,其中类接收不同功能的模板参数策略,例如: template <class Loss, class Optimizer> class LinearClassifier { ... } 有没有办法避免这种情况 事实上,这正是引入的原因。将构造函数重写为 template <typename L, typename O> LinearClassifier(L && loss, O

我正在为一个机器学习库编写一些模板类,并且我经常遇到这个问题。我主要使用策略模式,其中类接收不同功能的模板参数策略,例如:

template <class Loss, class Optimizer> class LinearClassifier { ... }

有没有办法避免这种情况

事实上,这正是引入的原因。将构造函数重写为

template <typename L, typename O>
LinearClassifier(L && loss, O && optimizer)
    : _loss(std::forward<L>(loss))
    , _optimizer(std::forward<O>(optimizer))
{}
模板
线性分类器(L&&loss、O&&optimizer)
:_损失(标准::远期(损失))
,_优化器(std::forward(优化器))
{}
但是,按照伊利亚·波波夫在他的报告中的建议去做可能会简单得多。老实说,我通常是这样做的,因为搬家是为了便宜,再多搬一次也不会让事情发生戏剧性的变化


正如Howard Hinnant,我的方法可能不友好,因为现在LinearClassifier接受构造函数中的任何类型对。显示了如何处理它。

事实上,这就是引入它的确切原因。将构造函数重写为

template <typename L, typename O>
LinearClassifier(L && loss, O && optimizer)
    : _loss(std::forward<L>(loss))
    , _optimizer(std::forward<O>(optimizer))
{}
模板
线性分类器(L&&loss、O&&optimizer)
:_损失(标准::远期(损失))
,_优化器(std::forward(优化器))
{}
但是,按照伊利亚·波波夫在他的报告中的建议去做可能会简单得多。老实说,我通常是这样做的,因为搬家是为了便宜,再多搬一次也不会让事情发生戏剧性的变化


正如Howard Hinnant,我的方法可能不友好,因为现在LinearClassifier接受构造函数中的任何类型对。演示如何处理它。

这正是“传递值并移动”技术的使用案例。 虽然与左值/右值重载相比效率稍低,但也不算太差(多做一步),可以省去麻烦

LinearClassifier(Loss loss, Optimizer optimizer) 
    : _loss(std::move(loss)), _optimizer(std::move(optimizer)) {}
在左值参数的情况下,将有一个拷贝和一个移动,在右值参数的情况下,将有两个移动(前提是类
Loss
Optimizer
实现移动构造函数)


更新:一般来说,更高效。另一方面,此解决方案避免了不总是需要的模板化构造函数,因为当不受SFINAE约束时,它将接受任何类型的参数,如果参数不兼容,则会导致构造函数内部出现硬错误。换句话说,无约束的模板构造函数对SFINAE不友好。有关避免此问题的约束模板构造函数,请参见

模板化构造函数的另一个潜在问题是需要将其放置在头文件中


更新2:Herb Sutter在其2014年CppCon演讲“回归基本”中谈到了这个问题。他首先讨论了按值传递,然后在右值ref上重载,然后是包括约束在内的完美转发。最后,他谈到构造函数是传递值的唯一好用例。

这正是“传递值并移动”技术的用例。 虽然与左值/右值重载相比效率稍低,但也不算太差(多做一步),可以省去麻烦

LinearClassifier(Loss loss, Optimizer optimizer) 
    : _loss(std::move(loss)), _optimizer(std::move(optimizer)) {}
在左值参数的情况下,将有一个拷贝和一个移动,在右值参数的情况下,将有两个移动(前提是类
Loss
Optimizer
实现移动构造函数)


更新:一般来说,更高效。另一方面,此解决方案避免了不总是需要的模板化构造函数,因为当不受SFINAE约束时,它将接受任何类型的参数,如果参数不兼容,则会导致构造函数内部出现硬错误。换句话说,无约束的模板构造函数对SFINAE不友好。有关避免此问题的约束模板构造函数,请参见

模板化构造函数的另一个潜在问题是需要将其放置在头文件中


更新2:Herb Sutter在其2014年CppCon演讲“回归基本”中谈到了这个问题。他首先讨论了按值传递,然后在右值ref上重载,然后是包括约束在内的完美转发。最后,他谈到构造函数是传递值的唯一好的用例。

为了完整性起见,最佳2参数构造函数将使用两个转发引用,并使用SFINAE确保它们是正确的类型。我们可以引入以下别名:

template <class T, class U>
using decays_to = std::is_convertible<std::decay_t<T>*, U*>;

为了完整性,最佳的2参数构造函数将采用两个转发引用,并使用SFINAE确保它们是正确的类型。我们可以引入以下别名:

template <class T, class U>
using decays_to = std::is_convertible<std::decay_t<T>*, U*>;

你想在兔子洞下面走多远

我知道解决这个问题有4种合适的方法。如果您符合它们的先决条件,通常应该使用前面的条件,因为后面的每一个条件都会显著增加复杂性


在大多数情况下,要么移动非常便宜,要么两次都是免费的,要么移动就是复制

如果move是copy,而copy是非free,则通过
const&
获取参数。如果不是,则按值计算

这将基本上达到最佳状态,并使您的代码更容易理解

LinearClassifier(Loss loss, Optimizer const& optimizer)
  : _loss(std::move(loss))
  , _optimizer(optimizer)
{}
对于便宜的移动
丢失
和移动是复制
优化器

在所有情况下,这比下面的“最佳”完美转发(注意:完美转发不是最佳)每值参数多移动1步。只要move便宜,这就是最好的解决方案,因为它会生成干净的错误消息,允许基于
{}
的构造,并且比其他任何解决方案都容易阅读

考虑使用此解决方案


如果移动比复制便宜,但不是免费的,一种方法是基于完美的转发: 要么:

我们将构造推迟到
线性分类器内部
。这允许您在对象中拥有非复制/可移动的对象,并且可以说效率最高

要了解这是如何工作的,现在的示例
分段构造
std::pair
一起工作。
template<class L, class O    >
LinearClassifier(L&& loss, O&& optimizer)
  : _loss(std::forward<L>(loss))
  , _optimizer(std::forward<O>(optimizer))
{}
template<class L, class O,
  std::enable_if_t<
    std::is_same<std::decay_t<L>, Loss>{}
    && std::is_same<std::decay_t<O>, Optimizer>{}
  , int> * = nullptr
>
LinearClassifier(L&& loss, O&& optimizer)
  : _loss(std::forward<L>(loss))
  , _optimizer(std::forward<O>(optimizer))
{}
private:
template<std::size_t...LIs, std::size_t...OIs, class...Ls, class...Os>
LinearClassifier(std::piecewise_construct_t,
  std::index_sequence<LIs...>, std::tuple<Ls...>&& ls,
  std::index_sequence<OIs...>, std::tuple<Os...>&& os
)
  : _loss(std::get<LIs>(std::move(ls))...)
  , _optimizer(std::get<OIs>(std::move(os))...)
{}
public:
template<class...Ls, class...Os>
LinearClassifier(std::piecewise_construct_t,
  std::tuple<Ls...> ls,
  std::tuple<Os...> os
):
  LinearClassifier(std::piecewise_construct_t{},
    std::index_sequence_for<Ls...>{}, std::move(ls),
    std::index_sequence_for<Os...>{}, std::move(os)
  )
{}
struct delayed_emplace_t {};
template<class T>
struct delayed_construct {
  std::function< void(std::experimental::optional<T>&) > ctor;
  delayed_construct(delayed_construct const&)=delete; // class is single-use
  delayed_construct(delayed_construct &&)=default;
  delayed_construct():
    ctor([](auto&op){op.emplace();})
  {}
  template<class T, class...Ts,
    std::enable_if_t<
      sizeof...(Ts)!=0
      || !std::is_same<std::decay_t<T>, delayed_construct>{}
    ,int>* = nullptr
  >
  delayed_construct(T&&t, Ts&&...ts):
    delayed_construct( delayed_emplace_t{}, std::forward<T>(t), std::forward<Ts>(ts)... )
  {}
  template<class T, class...Ts>
  delayed_construct(delayed_emplace_t, T&&t, Ts&&...ts):
    ctor([tup = std::forward_as_tuple(std::forward<T>(t), std::forward<Ts>(ts)...)]( auto& op ) mutable {
      ctor_helper(op, std::make_index_sequence<sizeof...(Ts)+1>{}, std::move(tup));
    })
  template<std::size_t...Is, class...Ts>
  static void ctor_helper(std::experimental::optional<T>& op, std::index_sequence<Is...>, std::tuple<Ts...>&& tup) {
    op.emplace( std::get<Is>(std::move(tup))... );
  }
  void operator()(std::experimental::optional<T>& target) {
    ctor(target);
    ctor = {};
  }
  explicit operator bool() const { return !!ctor; }
};
LinearClassifier( delayed_construct<Loss> loss, delayed_construct<Optimizer> optimizer ) {
  loss(_loss);
  optimizer(_optimizer);
}