(Im)使用可变模板完美转发 [英] (Im)perfect forwarding with variadic templates

查看:131
本文介绍了(Im)使用可变模板完美转发的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

概要



给定一个具有可变参数模板构造函数的类型,它将参数转发给一个实现类,是否可以限制使用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 是eg std :: 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 and Not aliases to combine different metacomputations. E.g. And<std::is_integral<T>, Not<is_const<T>>> is more convenient than std::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 from Args, 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 is std::vector<double>, then yes, the code is valid. Because std::vector<double> v(42); is a valid way to construct a vector of such type, then it is valid to convert from int to foo. In other words, std::is_convertible<T, foo>::value == std::is_constructible<foo_impl, T>::value, putting aside the matter of other constructors for foo (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 is std::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 around foo_impl. Here is what I think is a more annoying drawback, assuming foo_impl is std::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 type foo 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 like std::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 of foo. That is to say, std::is_convertible<T, foo>::value == std::is_convertible<T, foo_impl>::value and std::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 if foo_impl is e.g. std::vector<int>, because the constraint is in terms of a construction of the style std::vector<int> v(0, 1, 2, 3, 4);. It is possible to add a further overload taking std::initializer_list<T> that is constrained on std::is_convertible<std::initializer_list<T>, foo_impl> (left as an exercise to the reader), or even an overload taking std::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 modify is_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屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆