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

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>

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

and

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

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<>();

An example with non-type template parameter with default value

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

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

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) {}

would be better written as:

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

(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;

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


Mainly because [temp.over.link]/6 does not talk about template default argument:

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.

Then by [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.

... the two templates in your first example are equivalent, while the two templates in your second example are not. So the two templates in your first example declare the same entity and result in an ill-formed construct by [class.mem]/5:

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


The first version is wrong in the same way this snippet is wrong:

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

The reason has nothing to do with substitution failure: substitution only happens when the function templates are used (e.g. in a function invocation), but the mere declarations are enough to trigger the compile error.

The relevant standard wording is [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).

The second version is right because the function templates have different signature, and thus are not treated as the same entity by the compiler.