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

How does changing a template argument from a type to a non-type make SFINAE work?

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

Notes
A common mistake is to declare two function templates that differ only in their default template arguments. This is illegal because default template arguments are not part of function template's signature, and declaring two different function templates with the same signature is illegal.

/*** 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> 有一个名为 type 的 typedef 成员,而 std::enable_if<false,T> 没有这样的成员。替换失败(不是错误)应该会导致两个版本。

查看标准,它说 [temp.deduct]

when a function template specialization is referenced, all of the template arguments shall have values

后来

if a template argument has not been deduced and its corresponding template parameter has a default argument, the template argument is determined by substituting the template arguments determined for preceding template parameters into the default argument. If the substitution results in an invalid type, as described above, type deduction fails.

这种类型推导失败不一定是错误,这就是 SFINAE 的全部意义所在。

为什么将 *** WRONG *** 版本中的 typename 模板参数更改为非 typename 参数会使 *** RIGHT *** 版本 "right"?

让我们尝试省略默认参数值和不同的名称(记住:默认模板参数不是函数模板签名的一部分,就像参数名称一样),然后看看 "Wrong" 模板函数签名会是什么样子:

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

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

哇,他们一模一样!所以 T(Floating) 实际上是对 T(Integer) 的重新定义,而右版本声明了两个具有不同参数的模板:

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)

另请注意,在 "Right" 模板声明中,无需在 std::enable_if_t<std::is_floating_point<Floating>::value, int> 之前使用 typename,因为那里没有依赖类型名称。

改写 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相比)

第一个版本的错误与此片段的错误方式相同:

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

原因与替换失败无关:替换仅在使用函数模板时发生(例如在函数调用中),但仅 声明 就足以触发编译错误。

相关的标准写法是[dcl.fct.default]:

A default argument shall be specified only in [...] or in a template-parameter ([temp.param]); [...]

A default argument shall not be redefined by a later declaration (not even to the same value).

第二个版本是正确的,因为函数模板具有不同的签名,因此编译器不会将其视为同一实体。

不是类型或非类型

关键是:是否通过了Two phase lookup的第一步。

为什么?因为SFINAE工作在查找的第二阶段,当调用模板时

所以:

这不起作用(案例 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
    >

在第一种情况下,编译器看到:相同的名称,相同数量的模板参数并且参数不依赖于模板[​​=52=],相同的参数=>它是同一件事=>错误

如果是两个,参数的数量不一样,那么让我们看看它稍后是否有效=> SFINAE => OK

在你正确的情况下:编译器看到:相同的名称,相同数量的模板参数并且参数是模板相关的(具有默认值但他不关心现在)=> 让我们看看它什么时候调用 => SFINAE => OK

顺便问一下,你如何调用构造函数?

来自this post

There is no way to explicitly specify templates for a constructor, as you cannot name a constructor.

而你真的不能:

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

error: cannot call constructor 'T::T' directly [-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

的专家

我将对错误的版本进行小幅重写,以帮助讨论正在发生的事情。

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 我们得到签名

⟨class member function template⟩ name, parameter-type-list, class of which the function is a member, cv-qualifiers (if any), ref-qualifier (if any), return type (if any), and template parameter list

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

不幸的是,该标准似乎对 "template parameter list" 的确切含义相当模糊。我搜索了几个不同版本的标准,其中 none 给出了我能找到的明确定义。如果具有不同默认值的类型参数是否构成唯一,则 "template parameter list" 是否相同尚不清楚。考虑到我要说这实际上是未定义的行为,编译器错误是一种可接受的处理方式。

结论还没有定论,如果有人能在标准中找到 "template parameter list" 的明确定义,我很乐意添加它以获得更令人满意的答案。

编辑:

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

的结果

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

主要是因为[temp.over.link]/6不讲模板默认参数:

Two template-heads are equivalent if their template-parameter-lists have the same length, corresponding template-parameters are equivalent, and if either has a requires-clause, they both have requires-clauses and the corresponding constraint-expressions are equivalent. Two template-parameters are equivalent under the following conditions:

  • they declare template parameters of the same kind,

  • if either declares a template parameter pack, they both do,

  • if they declare non-type template parameters, they have equivalent types,

  • if they declare template template parameters, their template parameters are equivalent, and

  • if either is declared with a qualified-concept-name, they both are, and the qualified-concept-names are equivalent.

然后 [temp.over.link]/7:

Two function templates are equivalent if they are declared in the same scope, have the same name, have equivalent template-heads, and have return types, parameter lists, and trailing requires-clauses (if any) that are equivalent using the rules described above to compare expressions involving template parameters.

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

的结构不正确

A member shall not be declared twice in the member-specification, ...