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

Dav*_*men 7 c++ templates sfinae language-lawyer template-meta-programming

From the cppreference.com article on std::enable_if,

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
};
Run Code Online (Sandbox Code Playgroud)

 

I'm having a hard time wrapping my head around why the *** WRONG *** version doesn't compile while the *** RIGHT*** version does. The explanation and the example are cargo cult to me. All that has been done in the above is to change a type template parameter to a non-type template parameter. To me, both versions should be valid because both rely on std::enable_if<boolean_expression,T> having a typedef member named type , and std::enable_if<false,T> does not have such a member. A substitution failure (which is not an error) should result in both versions.

Looking at the standard, it says that in [temp.deduct] that

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

and later that

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.

That this type deduction failure is not necessarily an error is what SFINAE is all about.

Why does changing the typename template parameter in the *** WRONG *** version to a non-typename parameter make the *** RIGHT *** version "right"?

Pic*_*ent 10

Rewording the cppreference citation, in the wrong case we have:

 typename = std::enable_if_t<std::is_integral<Integer>::value>
 typename = std::enable_if_t<std::is_floating_point<Floating>::value>
Run Code Online (Sandbox Code Playgroud)

which are both default template arguments and are not part of function template's signature. Hence in the wrong case you come up with two identical signatures.

In the right case:

typename std::enable_if_t<std::is_integral<Integer>::value, int> = 0
Run Code Online (Sandbox Code Playgroud)

and

typename std::enable_if_t<std::is_floating_point<Floating>::value, int> = 0
Run Code Online (Sandbox Code Playgroud)

you do not have default template arguments anymore, but two different types with default value (=0). Hence the signatures are differents


Update from comment: to clarify the difference,

An example with template parameter with default type :

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

// usage
foo<double>();
foo<>();
Run Code Online (Sandbox Code Playgroud)

An example with non-type template parameter with default value

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

// usage
foo<4>();
foo<>();
Run Code Online (Sandbox Code Playgroud)

One last thing that can be confusing in your example is the usage of enable_if_t, in fact in your right case code your have a superfluous typename:

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

would be better written as:

template <
    typename Floating,
    std::enable_if_t<std::is_floating_point<Floating>::value, int> = 0
>
Run Code Online (Sandbox Code Playgroud)

(the same holds for the second declaration).

This is precisely the role of enable_if_t:

template< bool B, class T = void >
using enable_if_t = typename enable_if<B,T>::type;
Run Code Online (Sandbox Code Playgroud)

to do not have to add typename (compared to the older enable_if)

  • @DavidHammen与SFINAE无关:当函数模板被_call_(或以其他方式使用)时,发生SFINAE。但是仅具有相同签名的功能模板的_definition_就足以引起编译错误。 (2认同)

xsk*_*xzr 10

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

如果两个模板头模板参数列表具有相同的长度,相应的模板参数是等效的,并且两个模板头都具有需求子句,则它们都具有需求子句,并且对应的约束表达式是等效的。在以下情况下,两个模板参数是等效的:

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

  • 如果任何一个都声明了模板参数包,它们都会这样做,

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

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

  • 如果任何一个都用qualified-concept-name声明,则两者都是,并且qualified-concept-names是等效的。

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

两个函数模板是等价如果他们在同一范围内声明的,具有相同的名称,具有相当的模板头,并有返回类型,参数列表,以及尾随需要子句(如果有的话)是等价使用上述规则比较涉及模板参数的表达式。

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

成员不得在成员规范中两次声明。


cpp*_*ner 6

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

template<int=7>
void f();
template<int=8>
void f();
Run Code Online (Sandbox Code Playgroud)

其中的原因无关替换故障:当函数模板替换只发生使用(例如,在一个函数调用),但单纯的声明都足以引发编译错误。

相关的标准措辞为[dcl.fct.default]

只能在[...]或模板参数([temp.param])中指定默认参数。[...]

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

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

  • @DavidHammen似乎您在没有_reading_我的回答的情况下发表了评论,因为您在重复发言时未提及任何答案。如果您认为答案有误,请更具体。 (3认同)