(Im)使用可变模板完美转发 [英] (Im)perfect forwarding with variadic templates
问题描述
概要
给定一个具有可变参数模板构造函数的类型,它将参数转发给一个实现类,是否可以限制使用SFINAE转发的类型? p>
详细信息
首先,考虑一个构造函数采用通用引用的非可变情况。这里可以禁止通过SFINAE转发非常量值引用,以使用复制构造函数。
struct foo
{
foo()= default;
foo(foo const&)
{
std :: cout< copy<< std :: endl;
}
template<
typename T,
typename Dummy = typename std :: enable_if<
!std :: is_same<
T,
typename std :: add_lvalue_reference< foo> :: type
> :: value
> :: type
>
foo(T& x)
:impl(std :: forward< T>(x))
{
std :: cout< uref< std :: endl;
}
foo_impl impl;
};
这种通用引用的限制是有用的,因为否则实现类将接收一个非常量的引用的 foo
类型,它不知道。
完整示例 LWS 。
问题
但是这对可变模板如何工作?是可能的吗?如果是,如何?原生扩展无效:
模板<
typename ... Args,
typename Dummy = typename std :: enable_if<
!std :: is_same<
Args ...,
typename std :: add_lvalue_reference< foo> :: type
> :: value
> :: type
>
foo(Args& ... args)
:impl(std :: forward< Args>(args)...)
{
std :: cout< ;& uref< std :: endl;
}
(另外在LWS 。)
编辑:我发现R. Martinho Fernandez介绍了此问题的变化在2012年: http://flamingdangerzone.com/cxx11/2012/06/05/is_related.html
这里有不同的方式来编写一个正确约束的构造函数模板,
将使用此特定形式的EnableIf ,但这是一个实现细节,不会改变此处概述的技术的本质。还假设有和
和不是
别名来组合不同的元计算。例如。 和
std :: integral_constant& std :: is_integral< T> :: value&& !is_const< T> :: value>
。
我不推荐任何特定策略,因为约束是多少,比没有约束在任何时候,当涉及到构造函数模板。
自我约束
如果可能,避免前两种技术具有非常明显的缺点 - 其余都是同一主题的阐述。
模板< typename T>
using Unqualified = typename std :: remove_cv<
typename std :: remove_reference< T> :: type
> :: type;
struct foo {
template<
typename ... Args
,EnableIf<
Not< std :: is_same< foo,Unqualified< Args>> ...>
> ...
>
foo(Args& ... args);
};
好处:可避免构造函数在以下情况下参与重载解析:
foo f;
foo g = f; //典型的复制构造函数取foo const&不是首选!
缺点:每个
由于构造函数具有构造一个<$ c的道德效果$ c> foo_impl 从 Args
,表示对这些确切术语的约束是自然的:
template<
typename ... Args
,EnableIf<
std :: is_constructible< foo_impl,Args ...>
> ...
>
foo(Args& ... args);
好处:此功能现已正式成为受限模板,
$ b b
//函数声明
void fun(foo f);
fun(42);
例如,如果 foo_impl
code> std :: vector< double> ,那么是的,代码是有效的。因为 std :: vector< double> v(42);
是构造这种类型的向量的有效方法,然后有效从 int
到 foo
。换句话说, std :: is_convertible< T,foo> :: value == std :: is_constructible< foo_impl,T> :: value
,放弃其他 foo
的构造函数(注意参数的交换顺序 - 这是不幸的)。
明确地
自然地,以下是立即想到的:
;
typename ... Args
,EnableIf<
std :: is_constructible< foo_impl,Args ...>
> ...
>
explicit foo(Args& ... args);
第二次尝试标记构造函数 explicit
效益:避免上述缺点!它不需要太多 - 只要你不忘记显式
。
缺点:如果 foo_impl
是 std :: string
,则以下可能不方便: / p>
void fun(foo f)
// No:
// fun(hello);
fun(foo {hello});
这取决于 foo
意味着在 foo_impl
周围的薄包装。这里是我认为是一个更讨厌的缺点,假设 foo_impl
是 std :: pair< int,double *>
。
foo make_foo()
{
// No:
// return {42,nullptr};
return foo {42,nullptr};
}
我不喜欢 c $ c>实际上从这里的任何东西保存我:在大括号中有两个参数,所以它显然不是一个转换,并且类型
foo
已经出现在签名中,所以我当我觉得它是多余的,我喜欢它。 std :: tuple
遇到了这个问题(虽然工厂像 std :: make_tuple
做缓解疼痛一点)。
分开约束构建转换
让我们分别表示转换约束:
// []() - > T {return {std :: declval< Args>()...}; }
template< typename T,typename ... Args>
struct is_perfectly_convertible_from:std :: is_constructible< T,Args ...> {};
template< typename T,typename U>
struct is_perfectly_convertible_from:std :: is_convertible< U,T> {};
//新的可构造的trait将作为一个约束来处理
//与SFINAE
template< typename T的目的上面的trait不重叠,类型名U>
struct is_perfectly_constructible
:<
std :: is_constructible< T,U>
,Not< std :: is_convertible< U,T>
> {};
用法:
code> struct foo {
//一般构造函数
template<
typename ... Args
,EnableIf< is_perfectly_convertible_from< foo_impl,Args ...> > ...
>
foo(Args& ... args);
//特殊的一元,不可兑换的case
模板<
typename Arg
,EnableIf< is_perfectly_constructible< foo_impl,Arg> > ...
>
explicit foo(Arg&& arg);
}; / code>现在是构建和转换foo
所需的足够条件。也就是说,std :: is_convertible< T,foo> :: value == std :: is_convertible< T,foo_impl> :: value
和std :: is_constructible< foo,Ts ...> :: value == std :: is_constructible< foo_impl,T> :: value
$ b缺点?
foo f {0,1,2,3,4};
foo_impl
是egstd :: vector< int>
,因为约束是根据样式std :: vector< int& v(0,1,2,3,4);
。可以在std :: is_convertible< std :: initializer_list< T>上约束的
(作为练习留给读者),甚至是std :: initializer_list< T>
;,foo_impl>std :: initializer_list< T>,Ts&& ...
(约束也留给读者作为练习 - 但是记住,从多于一个参数的转换不是一个结构!)。请注意,我们不需要修改is_perfectly_convertible_from
以避免重叠。
一定要区分狭窄转换与其他类型的转换。
Synopsis
Given a type with a variadic template constructor that forwards the arguments to an implementation class, is it possible to restrict the types being forwarded with SFINAE?
Details
First, consider the non-variadic case with a constructor taking a universal reference. Here one can disable forwarding of a non-const lvalue reference via SFINAE to use the copy constructor instead.
struct foo { foo() = default; foo(foo const&) { std::cout << "copy" << std::endl; } template < typename T, typename Dummy = typename std::enable_if< !std::is_same< T, typename std::add_lvalue_reference<foo>::type >::value >::type > foo(T&& x) : impl(std::forward<T>(x)) { std::cout << "uref" << std::endl; } foo_impl impl; };
This restriction of the universal reference is useful because otherwise the implementation class would receive a non-const lvalue reference of type
foo
, which it does not know about. Full example at LWS.Question
But how does this work with variadic templates? Is it possible at all? If so, how? The naive extension does not work:
template < typename... Args, typename Dummy = typename std::enable_if< !std::is_same< Args..., typename std::add_lvalue_reference<foo>::type >::value >::type > foo(Args&&... args) : impl(std::forward<Args>(args)...) { std::cout << "uref" << std::endl; }
(Also at LWS.)
EDIT: I found that R. Martinho Fernandez blogged about a variation of this issue in 2012: http://flamingdangerzone.com/cxx11/2012/06/05/is_related.html
解决方案Here are the different ways to write a properly constrained constructor template, in increasing order of complexity and corresponding increasing order of feature-richness and decreasing order of number of gotchas.
This particular form of EnableIf will be used but this is an implementation detail that doesn't change the essence of the techniques that are outlined here. It's also assumed that there are
And
andNot
aliases to combine different metacomputations. E.g.And<std::is_integral<T>, Not<is_const<T>>>
is more convenient thanstd::integral_constant<bool, std::is_integral<T>::value && !is_const<T>::value>
.I don't recommend any particular strategy, because any constraint is much, much better than no constraint at all when it comes to constructor templates. If possible, avoid the first two techniques which have very obvious drawbacks -- the rest are elaborations on the same theme.
Constrain on self
template<typename T> using Unqualified = typename std::remove_cv< typename std::remove_reference<T>::type >::type; struct foo { template< typename... Args , EnableIf< Not<std::is_same<foo, Unqualified<Args>>...> >... > foo(Args&&... args); };
Benefit: avoids the constructor from participating in overload resolution in the following scenario:
foo f; foo g = f; // typical copy constructor taking foo const& is not preferred!
Drawback: participates in every other kind of overload resolution
Constrain on construction expression
Since the constructor has the moral effects of constructing a
foo_impl
fromArgs
, it seems natural to express the constraints on those exact terms:template< typename... Args , EnableIf< std::is_constructible<foo_impl, Args...> >... > foo(Args&&... args);
Benefit: This is now officially a constrained template, since it only participates in overload resolution if some semantic condition is met.
Drawback: Is the following valid?
// function declaration void fun(foo f); fun(42);
If, for instance,
foo_impl
isstd::vector<double>
, then yes, the code is valid. Becausestd::vector<double> v(42);
is a valid way to construct a vector of such type, then it is valid to convert fromint
tofoo
. In other words,std::is_convertible<T, foo>::value == std::is_constructible<foo_impl, T>::value
, putting aside the matter of other constructors forfoo
(mind the swapped order of parameters -- it is unfortunate).Constrain on construction expression, explicitly
Naturally, the following comes immediately to mind:
template< typename... Args , EnableIf< std::is_constructible<foo_impl, Args...> >... > explicit foo(Args&&... args);
A second attempt that marks the constructor
explicit
.Benefit: Avoids the above drawback! And it doesn't take much either -- as long as you don't forget that
explicit
.Drawbacks: If
foo_impl
isstd::string
, then the following may be inconvenient:void fun(foo f); // No: // fun("hello"); fun(foo { "hello" });
It depends on whether
foo
is for instance meant to be a thin wrapper aroundfoo_impl
. Here is what I think is a more annoying drawback, assumingfoo_impl
isstd::pair<int, double*>
.foo make_foo() { // No: // return { 42, nullptr }; return foo { 42, nullptr }; }
I don't feel like
explicit
actually saves me from anything here: there are two arguments in the braces so it's obviously not a conversion, and the typefoo
already appears in the signature, so I'd like to spare with it when I feel it is redundant.std::tuple
suffers from that problem (although factories likestd::make_tuple
do ease that pain a bit).Separately constrain conversion from construction
Let's separately express construction and conversion constraints:
// New trait that describes e.g. // []() -> T { return { std::declval<Args>()... }; } template<typename T, typename... Args> struct is_perfectly_convertible_from: std::is_constructible<T, Args...> {}; template<typename T, typename U> struct is_perfectly_convertible_from: std::is_convertible<U, T> {}; // New constructible trait that will take care that as a constraint it // doesn't overlap with the trait above for the purposes of SFINAE template<typename T, typename U> struct is_perfectly_constructible : And< std::is_constructible<T, U> , Not<std::is_convertible<U, T>> > {};
Usage:
struct foo { // General constructor template< typename... Args , EnableIf< is_perfectly_convertible_from<foo_impl, Args...> >... > foo(Args&&... args); // Special unary, non-convertible case template< typename Arg , EnableIf< is_perfectly_constructible<foo_impl, Arg> >... > explicit foo(Arg&& arg); };
Benefit: Construction and conversion of
foo_impl
are now necessary and sufficient conditions for construction and conversion offoo
. That is to say,std::is_convertible<T, foo>::value == std::is_convertible<T, foo_impl>::value
andstd::is_constructible<foo, Ts...>::value == std::is_constructible<foo_impl, T>::value
both hold (almost).Drawback?
foo f { 0, 1, 2, 3, 4 };
doesn't work iffoo_impl
is e.g.std::vector<int>
, because the constraint is in terms of a construction of the stylestd::vector<int> v(0, 1, 2, 3, 4);
. It is possible to add a further overload takingstd::initializer_list<T>
that is constrained onstd::is_convertible<std::initializer_list<T>, foo_impl>
(left as an exercise to the reader), or even an overload takingstd::initializer_list<T>, Ts&&...
(constraint also left as an exercise to the reader -- but remember that 'conversion' from more than one argument is not a construction!). Note that we don't need to modifyis_perfectly_convertible_from
to avoid overlap.The more obsequious amongst us will also make sure to discriminate narrow conversions against the other kind of conversions.
这篇关于(Im)使用可变模板完美转发的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!