14

概要

给定具有将参数转发给实现类的可变参数模板构造函数的类型,是否可以限制使用 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 左值引用。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;
}

(也在LWS。)

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

4

2 回答 2

19

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

将使用这种特殊形式的 EnableIf,但这是一个实现细节,不会改变此处概述的技术的本质。还假设存在AndNot别名来组合不同的元计算。egAnd<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_implfrom的道德效果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>::value两者std::is_constructible<foo, Ts...>::value == std::is_constructible<foo_impl, T>::value都(几乎)成立。

退税? 如果is egfoo f { 0, 1, 2, 3, 4 };则不起作用,因为约束是根据 style 的构造。可以添加一个受约束的进一步超载承受(作为练习留给读者),甚至是超载承受(约束也作为练习留给读者——但请记住,从多个“转换”论证不是构造!)。请注意,我们不需要修改以避免重叠。foo_implstd::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

我们当中更谄媚的人也会确保将狭隘的皈依与其他类型的皈依区分开来。

于 2012-11-11T04:12:16.870 回答
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;
}

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

于 2012-11-08T20:22:23.000 回答