你想去兔子洞多远?
我知道解决此问题的 4 种不错的方法。如果您符合它们的先决条件,您通常应该使用较早的那些,因为后面的每一个都会显着增加复杂性。
在大多数情况下,要么移动太便宜,重复两次是免费的,要么移动就是复制。
如果move是copy,copy是non-free,取const&的参数。如果没有,请按价值计算。
这将基本上表现最佳,并使您的代码更容易理解。
LinearClassifier(Loss loss, Optimizer const& optimizer)
: _loss(std::move(loss))
, _optimizer(optimizer)
{}
对于移动成本低廉的 Loss 和移动即复制 optimizer。
在所有情况下,每个值参数都会比下面的“最佳”完美转发(注意:完美转发不是最佳)多移动 1 次。只要 move 便宜,这是最好的解决方案,因为它会生成干净的错误消息,允许基于 {} 的构造,并且比任何其他解决方案都更容易阅读。
考虑使用此解决方案。
如果移动比复制便宜但非免费,则一种方法是基于完美转发:
要么:
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))
{}
这会使您无法基于{} 构建您的论点。此外,如果调用上述代码,最多可以生成指数数量的构造函数(希望它们会被内联)。
您可以删除std::enable_if_t 子句,但会导致 SFINAE 失败;基本上,如果你不小心 std::enable_if_t 子句,你的构造函数的错误重载就会被选中。如果您有具有相同数量参数的构造函数重载,或者关心早期故障,那么您需要std::enable_if_t 之一。否则,请使用更简单的。
这个解决方案通常被认为是“最优化的”。它是可以接受的最优值,但不是最优值。
下一步是使用带有元组的 emplace 构造。
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)
)
{}
我们将构建推迟到LinearClassifier 内部。这允许您在对象中包含不可复制/可移动的对象,并且可以说是最有效的。
要了解它是如何工作的,例如现在 piecewise_construct 与 std::pair 一起使用。您首先传递分段构造,然后 forward_as_tuple 传递参数以随后构造每个元素(包括复制或移动 ctor)。
与上面的完美转发解决方案相比,通过直接构造对象,我们可以消除每个对象的移动或复制。如果需要,它还允许您转发副本或移动。
最后一个可爱的技术是类型擦除构造。实际上,这需要std::experimental::optional<T> 之类的东西可用,并且可能会使类更大一些。
这不比分段构造快。它确实抽象了 emplace 构造所做的工作,使其在每次使用的基础上变得更简单,并且它允许您从头文件中拆分 ctor 主体。但是在运行时和空间方面都有少量开销。
您需要从一堆样板开始。这会生成一个模板类,代表“稍后在其他人会告诉我的地方构造一个对象”的概念。
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);
}
其中_loss 是std::experimental::optional<Loss>。要删除 _loss 的可选性,您必须使用 std::aligned_storage_t<sizeof(Loss), alignof(Loss)> 并非常小心地编写一个 ctor 来处理异常和手动销毁事物等。这是一个令人头疼的问题。
关于最后一种模式的一些好处是,ctor 的主体可以移出头部,并且最多生成线性数量的代码,而不是指数数量的模板构造函数。
此解决方案的效率略低于放置构造版本,因为并非所有编译器都能够内联 std::function 使用。但它也允许存储不可移动的对象。
代码未经测试,所以可能有错别字。
在保证省略的c++17 中,延迟 ctor 的可选部分已过时。任何返回 T 的函数都是 T 的延迟 ctor 所需要的。