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接受构造函数中的任何类型对。演示如何处理它。这正是“传递值并移动”技术的使用案例。 虽然与左值/右值重载相比效率稍低,但也不算太差(多做一步),可以省去麻烦
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*>;
你想在兔子洞下面走多远 我知道解决这个问题有4种合适的方法。如果您符合它们的先决条件,通常应该使用前面的条件,因为后面的每一个条件都会显著增加复杂性
在大多数情况下,要么移动非常便宜,要么两次都是免费的,要么移动就是复制 如果move是copy,而copy是非free,则通过
const&
获取参数。如果不是,则按值计算
这将基本上达到最佳状态,并使您的代码更容易理解
LinearClassifier(Loss loss, Optimizer const& optimizer)
: _loss(std::move(loss))
, _optimizer(optimizer)
{}
对于便宜的移动丢失
和移动是复制优化器
在所有情况下,这比下面的“最佳”完美转发(注意:完美转发不是最佳)每值参数多移动1步。只要move便宜,这就是最好的解决方案,因为它会生成干净的错误消息,允许基于{}
的构造,并且比其他任何解决方案都容易阅读
考虑使用此解决方案
如果移动比复制便宜,但不是免费的,一种方法是基于完美的转发: 要么: 我们将构造推迟到
线性分类器内部
。这允许您在对象中拥有非复制/可移动的对象,并且可以说效率最高
要了解这是如何工作的,现在的示例分段构造
与std::pair
一起工作。首先传递分段构造,然后将参数作为元组向前传递(包括复制或移动向量)
通过直接构造对象,与上面的完美转发解决方案相比,我们可以消除每个对象的移动或复制。如果需要,还可以转发副本或移动
最后一个可爱的技巧是键入擦除结构。实际上,这需要像
std::experimental::optional
这样的东西可用,并且可能会使类变得更大一些
这并不比分段构造的速度快。它确实抽象了emplace构造所做的工作,使其在每次使用的基础上更加简单,并且允许您从头文件中拆分主体。但在运行时和空间上都有少量开销
你需要从一大堆样板开始。这将生成一个模板类,该类表示“稍后在其他人告诉我的位置构造对象”的概念
struct延迟放置{};
模板
结构延迟构造{
std::functionctor;
delayed_construct(delayed_construct&)=delete;//类是一次性使用的
延迟构造(延迟构造&&)=默认值;
延迟的_构造():
ctor([](自动操作){op.emplace();})
{}
模板*=nullptr
>
延迟构造(T&&T,Ts&&T…Ts):
延迟构造(延迟放置{},std::forward(t),std::forward(ts)…)
{}
模板
延迟施工(延迟安置、t&t、Ts&t…Ts):
ctor([tup=std::forward_as_tuple(std::forward(t),std::forward(ts)…])(自动和操作)可变{
ctor_helper(op,std::make_index_sequence{},std::move(tup));
})
模板
静态void-ctor\u-helper(std::experimental::optional&op,std::index\u-sequence,std::tuple&&tup){
op.emplace(std::get(std::move(tup))…);
}
void操作符()(标准::实验::可选和目标){
行政主任(目标);;
ctor={};
}
显式运算符bool()常量{return!!ctor;}
};
我们在哪里
template <class L, class O,
class = std::enable_if_t<decays_to<L, Loss>::value &&
decays_to<O, Optimizer>::value>>
LinearClassifier(L&& loss, O&& optimizer)
: _loss(std::forward<L>(loss))
, _optimizer(std::forward<O>(optimizer))
{ }
LinearClassifier(Loss loss, Optimizer optimizer)
: _loss(std::move(loss))
, _optimizer(std::move(optimizer))
{ }
LinearClassifier(Loss loss, Optimizer const& optimizer)
: _loss(std::move(loss))
, _optimizer(optimizer)
{}
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);
}