将模板参数从类型更改为非类型如何使SFINAE起作用?

时间:2019-04-13 16:47:58

标签: c++ templates language-lawyer template-meta-programming sfinae

摘自std::enable_if上的cppreference.com文章,

  

注释
  一个常见的错误是声明两个函数模板,它们的默认模板参数仅不同。这是非法的,因为默认模板参数不是功能模板签名的一部分,并且声明两个具有相同签名的不同功能模板是非法的。

/*** WRONG ***/

struct T {
    enum { int_t,float_t } m_type;
    template <
        typename Integer,
        typename = std::enable_if_t<std::is_integral<Integer>::value>
    >
    T(Integer) : m_type(int_t) {}

    template <
        typename Floating,
        typename = std::enable_if_t<std::is_floating_point<Floating>::value>
    >
    T(Floating) : m_type(float_t) {} // error: cannot overload
};

/* RIGHT */

struct T {
    enum { int_t,float_t } m_type;
    template <
        typename Integer,
        typename std::enable_if_t<std::is_integral<Integer>::value, int> = 0
    >
    T(Integer) : m_type(int_t) {}

    template <
        typename Floating,
        typename std::enable_if_t<std::is_floating_point<Floating>::value, int> = 0
    >
T(Floating) : m_type(float_t) {} // OK
};

我很难理解为什么*** WRONG ***版本不能编译*** RIGHT***版本的原因。解释和例子对我来说是个迷。上面完成的所有操作都是将类型模板参数更改为非类型模板参数。对我来说,这两个版本都应该有效,因为它们都依赖于std::enable_if<boolean_expression,T>的typedef成员名为type,而std::enable_if<false,T>没有这样的成员。替换失败(这不是错误)应该同时导致两种版本。

查看标准,它在[temp.deduct]中表示

  

当引用功能模板专门化时,所有模板参数均应具有值

以及后来的

  

如果尚未推导出模板参数,并且其对应的模板参数具有默认参数,则通过将为先前模板参数确定的模板参数替换为默认参数来确定模板参数。 如上所述,如果替换导致无效的类型,则类型推导失败。

这种类型推导失败不一定是错误,这是SFINAE的全部目的。

为什么将*** WRONG ***版本中的typename模板参数更改为非typename参数会使*** RIGHT ***版本“正确”?

6 个答案:

答案 0 :(得分:10)

改写cppreference引用,在错误的情况下,我们有:

 typename = std::enable_if_t<std::is_integral<Integer>::value>
 typename = std::enable_if_t<std::is_floating_point<Floating>::value>

都是默认模板参数,并且不属于功能模板的签名。因此,在错误的情况下,您会想到两个相同签名。

在正确的情况下:

typename std::enable_if_t<std::is_integral<Integer>::value, int> = 0

typename std::enable_if_t<std::is_floating_point<Floating>::value, int> = 0

您不再具有默认模板参数,但两种不同类型,具有默认值(= 0)。因此签名是不同的


根据评论更新:以澄清差异,

带有默认类型的模板参数的示例:

template<typename T=int>
void foo() {};

// usage
foo<double>();
foo<>();

带有默认值的非类型模板参数的示例

template<int = 0>
void foo() {};

// usage
foo<4>();
foo<>();

示例中的最后一件令人困惑的事情是使用enable_if_t,实际上,在正确的案例代码中,您有多余的typename

 template <
    typename Integer,
    typename std::enable_if_t<std::is_integral<Integer>::value, int> = 0
>
T(Integer) : m_type(int_t) {}

最好写成:

template <
    typename Floating,
    std::enable_if_t<std::is_floating_point<Floating>::value, int> = 0
>

(第二个声明也是如此)。

这是精确的角色,enable_if_t

template< bool B, class T = void >
using enable_if_t = typename enable_if<B,T>::type;

不必添加typename(相比之下,较早的enable_if

答案 1 :(得分:10)

主要是因为[temp.over.link]/6没有谈论模板默认参数:

  如果两个 template-headers 的长度相同,相应的 template-parameters 相等,则

两个 template-heads 是等价的如果其中一个具有 requires-clause ,则它们都具有require-clauses,并且 corresponding-expressions 是等效的。在以下条件下,两个 template-parameters 是等效的:

     
      
  • 它们声明相同类型的模板参数,

  •   
  • 如果其中一个声明了模板参数包,则两者都声明了

  •   
  • 如果它们声明非类型模板参数,则它们具有等效类型,

  •   
  • 如果它们声明模板模板参数,则它们的模板参数是等效的,并且

  •   
  • 如果其中任何一个都用 qualified-concept-names 声明,则它们都是,并且 qualified-concept-names 是等价的。

  •   

然后通过[temp.over.link]/7

  

如果两个函数模板在相同的范围内声明,具有相同的名称,具有等效的 template-heads 以及返回类型,参数列表,和尾随的 requires-clauses (如果有),它们使用上述规则比较包含模板参数的表达式。

...第一个示例中的两个模板是等效的,而第二个示例中的两个模板则不是。因此,第一个示例中的两个模板声明相同的实体,并导致[class.mem]/5的格式错误:

  

成员不得在成员规范,...

中声明两次

答案 2 :(得分:6)

第一个版本是错误的,就像该片段是错误的一样:

template<int=7>
void f();
template<int=8>
void f();

原因与替换失败无关:替换仅在使用功能模板 时发生(例如,在功能 invocation 中),而仅发生在声明足以触发编译错误。

相关标准文字为[dcl.fct.default]

  

仅应在[...]或模板参数([temp.param])中指定默认参数; [...]

     

默认参数不能由以后的声明重新定义(甚至不能具有相同的值)。

第二个版本是正确的,因为功能模板具有不同的签名,因此编译器不会将它们视为相同的实体。

答案 3 :(得分:3)

让我们尝试省略默认参数值和不同的名称(请记住:默认模板参数不像参数名称一样是函数模板签名的一部分),并查看“错误”模板函数签名的外观:

template
<
     typename FirstParamName
,    typename SecondParamName
>
T(FirstParamName)

template
<
    typename FirstParamName
,   typename SecondParamName
>
T(FirstParamName)

哇,它们是完全一样的!因此T(Floating)实际上是T(Integer)的重新定义,而Right版本则声明了两个具有不同参数的模板:

template
<
     typename FirstParamName
,    std::enable_if_t<std::is_integral<FirstParamName>::value, int> SecondParamName
> 
T(FirstParamName)

template
<
    typename FirstParamName
,   std::enable_if_t<std::is_floating_point<FirstParamName>::value, int> SecondParamName
>
T(FirstParamName)

还要注意,在“正确”模板声明中,typename之前不需要使用std::enable_if_t<std::is_floating_point<Floating>::value, int>,因为那里没有相关的类型名称。

答案 4 :(得分:1)

不是关于类型或非类型

重点是:它是否通过了Two phase lookup的第一步。

为什么?因为SFINAE在查找的第二阶段工作,所以当模板称为(as @cpplearner said)

所以:

这不起作用(情况1):

 template <
        typename Integer,
        typename = std::enable_if_t<std::is_integral<Integer>::value>
    >

这项工作以及您的非类型案例(案例2):

template <
        typename Integer,
        typename = std::enable_if_t<std::is_integral<Integer>::value>,  
        typename = void
    >

在一种情况下,编译器看到:名称相同,模板参数相同的数量,并且该参数与模板无关,相同的修饰符=>是同一件事=>错误

在两个不同数量的参数的情况下,让我们看看以后是否可以使用=> SFINAE => OK

在您的情况下:编译器看到:相同名称,相同数量的模板参数和参数是否取决于模板(具有默认值,但他现在不在乎)=>让我们看看何时打电话=> SFINAE =>确定

顺便说一句,如何调用构造函数?

来自this post

  

由于无法命名构造函数,因此无法为构造函数明确指定模板。

您真的不能:

T t =T::T<int,void>(1);
  

错误:无法直接调用构造函数'T :: T'[-fpermissive]

您仍然可以使其与专业化和SFINAE一起使用:

#include <iostream>
#include <type_traits>
using namespace std;


template <
        typename Type,
        typename = void
    >
struct T {
};

template < typename Type>
struct T<
    Type,
    std::enable_if_t<std::is_integral<Type>::value>
>  {
    float m_type;

    T(Type t) : m_type(t) { cout << __PRETTY_FUNCTION__ << endl; }
};

template < typename Type>
struct T<
    Type,
    std::enable_if_t<std::is_floating_point<Type>::value>
>  {
    int m_type;

    T(Type t) : m_type(t) { cout << __PRETTY_FUNCTION__ << endl; }

};

int main(){

    T<int> t(1); // T<Type, typename std::enable_if<std::is_integral<_Tp>::value, void>::type>::T(Type) [with Type = int; typename std::enable_if<std::is_integral<_Tp>::value, void>::type = void]
    cout << endl;
    T<float> t2(1.f);// T<Type, typename std::enable_if<std::is_floating_point<_Tp>::value, void>::type>::T(Type) [with Type = float; typename std::enable_if<std::is_floating_point<_Tp>::value, void>::type = void]

    return 0;
}

这是C ++ 14风格,在17年代您可能会想出一个仅用T t(1)编译的版本,但我不是Class template argument deduction的专家

答案 5 :(得分:1)

我将对错误的版本进行较小的重写,以帮助说明发生了什么情况。

struct T {
    enum { int_t,float_t } m_type;
    template <
        typename Integer,
        typename U = std::enable_if_t<std::is_integral<Integer>::value>
    >
    T(Integer) : m_type(int_t) {}

    template <
        typename Floating,
        typename U = std::enable_if_t<std::is_floating_point<Floating>::value>
    >
    T(Floating) : m_type(float_t) {} // error: cannot overload
};

我所做的所有工作都赋予了先前匿名的第二个参数一个名称-U

该第一个版本不起作用的原因是,在显式给出第二个参数的情况下,无法在两者之间做出决定。例如 1

f<int,void>(1);

应该推导哪个函数?如果是整数版本,那么它当然可以工作-但是浮动版本呢。好吧,它有T = int,但是U又如何呢?好吧,我们刚刚给它指定了一个类型bool,所以我们有了U = bool。因此,在这种情况下,无法在两者之间做出决定,因为它们是相同的。 (请注意,在整数版本中,我们还有U = bool)。

因此,如果我们显式命名第二个模板参数,则推导将失败。所以呢?在实际用例中,这不应发生。我们将使用类似的

f(1.f);

可以推论的地方。好吧,您会注意到编译器即使没有声明也会给出错误 。这意味着它已经决定,在检测到我上面指出的问题之前,甚至无法给出推断类型,就无法推断。从intro.defs开始,我们的签名为

  

“类成员函数模板”的名称,参数类型列表,该函数为成员的类,cv限定符(如果有),ref限定符(如果有),返回类型(如果有)和模板参数列表

temp.over.link中我们知道两个模板函数定义不能具有相同的签名。

不幸的是,该标准对于“模板参数列表”的含义似乎还很模糊。我搜索了几个不同版本的标准,但没有一个给出我可以找到的明确定义。如果具有不同默认值的a类型参数是否构成唯一,则不清楚“模板参数列表”是否相同。鉴于我要说的是这实际上是未定义的行为,并且可以通过编译器错误来解决此问题。

结论仍然存在,如果有人可以在标准中为“模板参数列表”找到明确的定义,那么我很乐意将其添加为更令人满意的答案。

编辑:

正如xskxkr所指出的,实际上最新的草案确实给出了更具体的定义。模板的 template-head 包含一个 template-parameter-list ,它是一系列的 template-parameters 。它在定义中不包含默认参数。因此,根据当前草案,拥有两个相同但具有不同默认参数的模板无疑是错误的,但是您可以“欺骗”它,以为您拥有两个单独的 template-heads 使第二个参数的类型取决于enable_if的结果。


1 作为旁注,我想不出一种方法来显式实例化非模板类的模板构造函数。这是一个奇怪的建筑。我在示例中使用了f,因为实际上我可以使它与自由函数一起工作。也许其他人可以弄清楚语法?