【问题标题】:Why is an ellipsis preferred to a variadic template when called with no arguments?为什么在没有参数的情况下调用省略号而不是可变参数模板?
【发布时间】:2014-07-11 08:13:19
【问题描述】:

我正在使用以下 SFINAE 模式来评估可变参数类型列表上的谓词:

#include <type_traits>

void f(int = 0);  // for example

template<typename... T,
    typename = decltype(f(std::declval<T>()...))>
std::true_type check(T &&...);
std::false_type check(...);

template<typename... T> using Predicate = decltype(check(std::declval<T>()...));

static_assert(!Predicate<int, int>::value, "!!");
static_assert( Predicate<int>::value, "!!");
static_assert( Predicate<>::value, "!!");  // fails

int main() {
}

令我惊讶的是,当使用空参数列表调用 check 时选择了省略号重载,因此即使 SFINAE 表达式有效,Predicate&lt;&gt; 也是 std::false_type

不应该总是首选可变参数函数模板而不是省略号函数吗?

有什么解决方法吗?

【问题讨论】:

  • 我认为这取决于这个神秘的部分>> “一些涉及 T... 的 SFINAE 表达式” 我相信当T... 扩展为空集时,这部分会拒绝重载.你需要揭开谜底。
  • 你能写出实际的代码吗?还是产生此问题的代码?
  • 有趣的角落案例。它有什么用?
  • 当非模板函数都是可行的重载候选并且同样匹配良好时,这是否只是一种偏爱非模板函数而不是模板的情况?
  • @Casey 正是我的想法。省略号通常排名较低,因为 省略号转换序列 在此处未应用,因为没有参数。

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


【解决方案1】:

T...为空时,编译器会执行重载解析来判断是哪一个

std::true_type check(); // instantiated from the function template
std::false_type check(...);

是最佳可行的候选者,如 [over.match.best] 13.3.3/1(引用 N3936)中所述:

如下定义ICSi(F):

  • 如果 F 是静态成员函数,则定义 ICS1 (F) 使得对于任何函数 G,ICS1 (F) 既不比 ICS1 (G) 好也不差,并且对称地,ICS1 (G) 是不比 ICS1 (F)132 好也不差;否则,

  • 让 ICSi(F) 表示将列表中的第 i 个参数转换为可行函数 F 的第 i 个参数的类型的隐式转换序列。13.3.3.1 定义了隐式转换序列,13.3 .3.2 定义了一个隐式转换序列比另一个更好或更差的转换序列意味着什么。

鉴于这些定义,如果对于所有参数 i,ICSi(F1) 不是比 ICSi(F2) 更差的转换序列,则可行函数 F1 被定义为比另一个可行函数 F2 更好的函数,然后

  • 对于某些参数 j,ICSj(F1) 是比 ICSj(F2) 更好的转换序列,或者,如果不是这样,

  • 上下文是通过用户定义的转换(见 8.5、13.3.1.5 和 13.3.1.6)和从 F1 的返回类型到目标类型(即实体的类型)的标准转换序列initialized) 是比从 F2 的返回类型到目标类型的标准转换序列更好的转换序列。 [ 例子:

    struct A {
      A();
      operator int();
      operator double();
    } a;
    int i = a; // a.operator int() followed by no conversion
    // is better than a.operator double() followed by
    // a conversion to int
    float x = a; // ambiguous: both possibilities require conversions,
    // and neither is better than the other
    

    结束示例]或者,如果不是这样,

  • 上下文是对函数类型的引用的直接引用绑定(13.3.1.6)的转换函数的初始化,F1的返回类型是与被初始化的引用相同类型的引用(即左值或右值),而F2的返回类型不是[例子:

    template <class T> struct A {
      operator T&(); // #1
      operator T&&(); // #2
    };
    typedef int Fn();
    A<Fn> a;
    Fn& lf = a; // calls #1
    Fn&& rf = a; // calls #2
    

    结束示例]或者,如果不是这样,

  • F1 不是函数模板特化,F2 是函数模板特化,或者,如果不是,则

  • F1 和 F2 是函数模板特化,根据 14.5.6.2 中描述的部分排序规则,F1 的函数模板比​​ F2 的模板更特化。

在这种情况下,两个候选的转换序列都是空的,因为没有参数。倒数第二个项目符号是决定因素:

  • F1 不是函数模板特化,F2 是函数模板特化,或者,如果不是,

因此首选非模板std::false_type check(...);


我首选的解决方法 - 显然有很多 - 将通过省略号转换 [over.ics.ellipsis] 13.3.3.1.3/1 来制作候选模板并进行区分:

当函数调用中的参数与被调用函数的省略号参数规范匹配时,就会发生省略号转换序列(参见 5.2.2)。

通过为“首选”模板声明提供一个明显更好匹配的无关参数,因为根据 [over.ics.rank] 13.3.3.2/2,任何其他转换序列都将优于省略号转换:

比较隐式转换序列的基本形式时(定义见 13.3.3.1)

  • 标准转换序列 (13.3.3.1.1) 是比用户定义的转换序列或省略号转换序列更好的转换序列,并且
  • 用户定义的转换序列 (13.3.3.1.2) 是比省略号转换序列 (13.3.3.1.3) 更好的转换序列。

Example:

template<typename... T,
    typename = decltype(f(std::declval<T>()...))>
std::true_type check(int);
template<typename...>
std::false_type check(...);

template<typename... T> using Predicate = decltype(check<T...>(0));

【讨论】:

  • 可能比必要的更标准的引用,但我更喜欢完整的参考,这是一个 big 段落。
  • 很好的解释,谢谢 - 以及更好的写作方式Predicate
  • 第二个模板检查功能(带省略号的那个),不一定是省略号功能。 template&lt;typename...&gt; std::false_type check(float); 也可以正常工作。
  • @BЈовић True - 转换序列比标识序列差的任何可行签名将导致选择另一个重载。我更喜欢省略号转换以使其非常明显,并且在 C++ 中使用省略号来区分类型特征中的重载几乎是惯用的。
【解决方案2】:

这也让我感到惊讶。

一种解决方法可能是将int(例如0)作为第一个参数传递给check(),并强制编译器尝试模板版本first

template<typename... T, typename = decltype(f(std::declval<T>()...))>
std::true_type check(int &&, T &&...); //ADDED `int &&` as the first parameter type

std::false_type check(...);

template<typename... T> using Predicate = decltype(check(0, std::declval<T>()...));

注意,从0 创建的临时对象会尝试绑定到int&amp;&amp; first(这在这里非常重要),然后如果 value-sfinae 失败,那么它会尝试第二次重载。

希望对您有所帮助。

【讨论】:

  • 是的,这行得通——我可能会使用这个解决方法。不过,省略号重​​载中的int const &amp;&amp; 是不必要的。
  • @ecatmur:哦。是的。删除它。
  • &amp;&amp; 上的int 是不必要的顺便说一句。
  • @dyp:是的。但是我是因为我的习惯才写的(这里还不错);如果您有很多重载,那么转换序列int&amp;&amp; -&gt; int const&amp;&amp; -&gt; int const volatile &amp;&amp; -&gt; int const &amp; -&gt; long &amp;&amp; -&gt; etc 会强制编译器以特定 顺序尝试 重载!
猜你喜欢
  • 1970-01-01
  • 2020-09-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-11-29
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多