你想要深入了解这个问题吗?
我知道4种解决这个问题的方法。如果你符合早期方法的前提条件,通常应该使用它们,因为每个后续方法的复杂性都会显著增加。
在大多数情况下,无论是移动还是复制,两次操作的成本都很低。如果移动是复制,并且复制是非免费的,则通过
const&
获取参数。否则,通过值获取参数。这将基本上以最优方式运行,并使您的代码更易于理解。
LinearClassifier(Loss loss, Optimizer const& optimizer)
: _loss(std::move(loss))
, _optimizer(optimizer)
{}
对于一个成本低廉的Loss
和移动即复制的optimizer
。
相对于下面的“最优”完美转发,每个值参数在所有情况下都会多进行1次移动(注意:完美转发并不是最优)。 只要移动成本低廉,这就是最佳解决方案,因为它可以生成清晰的错误消息,允许基于{}
的构造,并且比其他任何解决方案都容易阅读。
考虑使用此解决方案。
如果移动操作比复制操作便宜但不免费,一种完美的转发方法是基于以下两种方式之一:
要么:
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
。否则,请使用更简单的一个。
通常认为此解决方案是“最优”的。它是可接受的最优解,但不是最优解。
下一步是使用元组进行就地构造。
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
内部。这样可以在对象中拥有不可复制/移动的对象,并且可能是最大效率的。
为了看看它是如何工作的,现在让我们以std::pair
为例来说明piecewise_construct
的工作方式。您首先传递piecewise construct,然后使用forward_as_tuple
构造每个元素的参数(包括复制或移动构造函数)。
通过直接构造对象,我们可以消除每个对象相对于上面的完美转发解决方案的移动或复制。它还允许您转发复制或移动(如果需要)。
一种可爱的最终技巧是类型擦除构造。实际上,这需要类似于
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;
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中,通过保证省略,延迟构造函数的可选部分已经过时。任何返回
T
的函数都足以成为
T
的延迟构造函数。请参考
c++17。
Loss
和Optimizer
必须是推导类型。 - Quentin_loss(loss)
。即使loss
是类型为Loss&&
,该初始化程序仍将把loss
视为lvalue。这很重要,但不直观。@Federico,你是否认为_loss(loss)
会从Loss&& loss
中“移动”?实际上,它将被复制进去。 - Aaron McDaid_loss
和_optimizer
是值还是引用? - M.M