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