Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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

 

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"?

like image 547
David Hammen Avatar asked Apr 13 '19 16:04

David Hammen


3 Answers

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, ...

like image 133
xskxzr Avatar answered Oct 22 '22 02:10

xskxzr


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)

like image 34
Picaud Vincent Avatar answered Oct 22 '22 01:10

Picaud Vincent


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.

like image 6
cpplearner Avatar answered Oct 22 '22 02:10

cpplearner