(Im)使用可变参数模板完美转发

时间:2012-11-08 19:38:30

标签: c++ c++11 variadic-templates sfinae variadic-functions

概要

给定一个带有可变参数模板构造函数的类型,它将参数转发给实现类,是否可以限制使用SFINAE转发的类型?

详细

首先,考虑带有通用引用的构造函数的非变量情形。这里可以禁止通过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的非const左值引用,它不知道。 完整示例at LWS

问题

但是如何使用可变参数模板?有可能吗?如果是这样,怎么样?天真的扩展不起作用:

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;
}

(另外at LWS。)

编辑:我发现R. Martinho Fernandez在2012年发表了关于此问题变体的博客:http://flamingdangerzone.com/cxx11/2012/06/05/is_related.html

2 个答案:

答案 0 :(得分:19)

以下是编写正确约束的构造函数模板的不同方法,按照复杂度的增加顺序和相应的特征丰富度的递增顺序和陷阱数量的递减顺序。

将使用

This particular form of EnableIf,但这是一个实现细节,不会改变此处概述的技术的本质。还假设有AndNot别名来组合不同的元计算。例如。 And<std::is_integral<T>, Not<is_const<T>>>std::integral_constant<bool, std::is_integral<T>::value && !is_const<T>::value>更方便。

我不推荐任何特定的策略,因为任何约束在构造函数模板方面都比没有约束要好得多。如果可能的话,避免前两种技术有明显的缺点 - 其余的都是对同一主题的阐述。

自我约束

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);
};

好处:避免构造函数在以下场景中参与重载解析:

foo f;
foo g = f; // typical copy constructor taking foo const& is not preferred!

缺点:参与其他其他类型的重载解析

约束构造表达式

由于构造函数具有从foo_impl构造Args的道德效应,因此表达对这些确切术语的约束似乎很自然:

    template<
        typename... Args
        , EnableIf<
            std::is_constructible<foo_impl, Args...>
        >...
    >
    foo(Args&&... args);

好处:现在这是一个受约束的模板,因为只有满足某些语义条件才会参与重载解析。

缺点:以下是否有效?

// function declaration
void fun(foo f);
fun(42);

例如,如果foo_implstd::vector<double>,那么是,代码有效。由于std::vector<double> v(42);构造这种类型的向量的有效方式,因此将转换int转换为foo是有效的。换句话说,std::is_convertible<T, foo>::value == std::is_constructible<foo_impl, T>::value,将foo的其他构造函数放在一边(注意参数的交换顺序 - 很不幸)。

明确

对构造表达式的约束

自然,以下是立即想到的:

    template<
        typename... Args
        , EnableIf<
            std::is_constructible<foo_impl, Args...>
        >...
    >
    explicit foo(Args&&... args);

第二次尝试标记构造函数explicit

好处:避免上述缺点!它也不需要太多 - 只要你不忘记explicit

缺点:如果foo_implstd::string,则以下内容可能不方便:

void fun(foo f);
// No:
// fun("hello");
fun(foo { "hello" });

这取决于foo是否意味着是foo_impl周围的薄包装。假设foo_implstd::pair<int, double*>,我认为这是一个更令人烦恼的缺点。

foo make_foo()
{
    // No:
    // return { 42, nullptr };
    return foo { 42, nullptr };
}

我不觉得explicit实际上让我免于此处的任何事情:大括号中有两个参数,因此它显然不是转换,类型foo已经出现在签名中,所以当我觉得它是多余的时候,我想随意使用它。 std::tuple遇到了这个问题(尽管像std::make_tuple这样的工厂确实缓解了这种痛苦。)

单独约束构造转换

让我们分别表达构造转换约束:

// 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>>
> {};

用法:

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);
};

好处: foo_impl的构建和转换现在是构建和转换foo的必要条件。也就是说,std::is_convertible<T, foo>::value == std::is_convertible<T, foo_impl>::valuestd::is_constructible<foo, Ts...>::value == std::is_constructible<foo_impl, T>::value都持有(差不多)。

如果foo f { 0, 1, 2, 3, 4 };是例如,那么

缺点? foo_impl不起作用std::vector<int>,因为约束是根据样式std::vector<int> v(0, 1, 2, 3, 4);的构造。可以添加进一步的重载,std::initializer_list<T>约束std::is_convertible<std::initializer_list<T>, foo_impl>(作为练习留给读者),甚至是超载std::initializer_list<T>, Ts&&...(约束也留作练习)对读者 - 但请记住,不止一个参数的'转换'不是一个结构!)。请注意,我们无需修改is_perfectly_convertible_from以避免重叠。

我们中间更加痴迷的人也会确保将缩小转化与其他类型的转换区分开来。

答案 1 :(得分:4)

您可以将Args置于更复杂的表达式中,并将其展开为expression(Args)...。因此

!std::is_same<Args, typename std::add_lvalue_reference<foo>::type>::value...

将为每个参数提供逗号分隔的is_same列表。您可以将其用作模板的模板参数,相应地组合这些值,为您提供如下内容。

template<bool... Args> struct and_;

template<bool A, bool... Args>
struct and_<A, Args...>{
  static constexpr bool value = A && and_<Args...>::value;
};
template<bool A>
struct and_<A>{
  static constexpr bool value = A;
};

//...
template <typename... Args,
          typename Dummy = typename std::enable_if<
              and_<!std::is_same<Args, 
                                 typename std::add_lvalue_reference<foo>::type
                                >::value...>::value
              >::type
         >
foo(Args&&... args) : impl(std::forward<Args>(args)...)
{
  std::cout << "uref" << std::endl;
}

我不完全确定你想要如何限制参数。因此,我不确定这是否会做你想要的,但你应该能够使用这个原则。