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

6

7

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

Epistasis answered 13/4, 2019 at 16:47 Comment(3)
I added the penultimate paragraph and the language lawyer tag about 45 minutes after I asked the question. I do not want a cargo cult answer. I want a chapter and verse answer, if at all possible.Epistasis
To be very clear on the bonus: I want a non-cargo cult answer, chapter and verse preferred, targeted at c++11 or c++14.Epistasis
You keep using the phrase "cargo cult". I do not think it means what you think it means, and you seem to be using it more to pointlessly insult people than to achieve any kind of resolution to your question.Albata
G
10

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

Gulgee answered 20/4, 2019 at 4:35 Comment(1)
I'm torn between this answer and the other top rated answer. This cites chapter and verse, but the chapter and verse are from the upcoming 2020 version of the standard. Compiler vendors are still address bug reports regarding c++11 compliance, let alone the current fiction of c++20. I very recently ran into a compiler bug with regard to a compiler than claimed to be c++14 compliant. (And it was a bug; later versions of clang and gcc had no problems.) OTOH, this answer does cites chapter and verse, and some of the concepts (but not concepts themselves) go all the way back to c++11.Epistasis
A
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>

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)

Aphorize answered 13/4, 2019 at 17:6 Comment(10)
The "two different types" is a bit misleading since they are both int don't you think?K
yes but the std::is_integral<...> and is_floating_point<...> make the differenceAphorize
There most certainly is a default parameter in both cases; one happens to be a typename default and the other is a non-type default. What's the difference? I would prefer a chapter and verse explanation.Epistasis
@DavidHammen I have added an update to the post, does it help?Aphorize
@PicaudVincent - Not really. Where in the standard does it say that an invalid default typename parameter can be ignored (and does not result in a substitution failure) but that an invalid default non-type parameter does result in a substitution failure (which cannot be ignored).Epistasis
@DavidHammen I am unable to give the precise reference of the standard on this point, I am sorry. It's more than 1000 pages rather difficult to read. Maybe someone else can help here.Aphorize
@PicaudVincent - Quoting from the standard, "[temp.deduct] When a function template specialization is referenced, all of the template arguments shall have values." Template parameter deduction should fail in both cases, and yet it apparently succeeds in the ***WRONG*** code, even though the ***WRONG*** code is just as wrong (which is good from an SFINAE perspective) as is the ***RIGHT*** code.Epistasis
@DavidHammen thanks for the feedback, but to be honest you lost me.Aphorize
@PicaudVincent - Quoting further from the standard, "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." I'll add this to my question because this gets to the heart of my confusion. Substitution failure should result with the ***WRONG*** code. But somehow it doesn't.Epistasis
@DavidHammen There's nothing to do with SFINAE: SFINAE happens when a function template is called (or used in some other way). But the mere definition of function templates with same signature is enough to cause compile error.Progenitor
G
10

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

Gulgee answered 20/4, 2019 at 4:35 Comment(1)
I'm torn between this answer and the other top rated answer. This cites chapter and verse, but the chapter and verse are from the upcoming 2020 version of the standard. Compiler vendors are still address bug reports regarding c++11 compliance, let alone the current fiction of c++20. I very recently ran into a compiler bug with regard to a compiler than claimed to be c++14 compliant. (And it was a bug; later versions of clang and gcc had no problems.) OTOH, this answer does cites chapter and verse, and some of the concepts (but not concepts themselves) go all the way back to c++11.Epistasis
P
6

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.

Progenitor answered 13/4, 2019 at 20:2 Comment(7)
And how is the first version wrong? The only difference between ***WRONG*** and ***RIGHT*** is a default type parameter that evaluates to garbage (but somehow passes through) and a default non-type parameter that evaluates to garbage and does not pass through. This is a cargo cult answer, sorry for the insult.Epistasis
@DavidHammen It seems like you made the comment without reading my answer, as you are repeating what you said without making any reference to the answer. If you think the answer is wrong, please be more specific.Progenitor
There is still an error even if the second default template argument is removed.Gulgee
@Gulgee That is because cpplearners quote from the standard only applies to declarations, not definitions. If you remove the function definition, it mostly works (except that it won't let you redeclare the constructor -- it works for free functions though). The issue really is that the template parameter lists are the same (see my answer), this will occur in the example ccplearner gave if you remove the default argument from one function, and give them definitionsSpacesuit
@Spacesuit There is still an error if we remove the definitions. My point is class scope is different from namespace scope, and redeclaration is not allowed in class scope.Gulgee
@Gulgee That issue is only because you can't re-declare a constructor. This works gcc.godbolt.org/z/GyhU0o. This does not work gcc.godbolt.org/z/DViyS0Spacesuit
@Gulgee After rereading your comment, I see you already realized the point I made in my last comment and we are in agreementSpacesuit
T
3

Lets try omitting default parameter values and different names (remember: default template parameters are not part of function template's signature, just like parameter names) and see how "Wrong" template function signatures will look like:

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

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

Wow, they are exactly the same! So T(Floating) is actually redefinition of the T(Integer) While Right version declares two templates that have different parameters:

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)

Also note that there is no need to use typename prior to std::enable_if_t<std::is_floating_point<Floating>::value, int> in "Right" template declaration because there are no dependent type names there.

Tympanist answered 13/4, 2019 at 17:2 Comment(8)
Re Also note that there is no need to use typename prior to std::enable_if_t<std::is_floating_point<Floating>::value, int> in "Right" template declaration because there are no dependent type names there. Do so. I've done so with multiple compilers. They all fail and suggest I add typename. The typename qualifier is needed because std::enable_if<std::is_whatever<FirstParameter>::value>::type is a dependent type name.Epistasis
@DavidHammen There are no std::enable_if<std::is_whatever<FirstParameter>::value>::type here. Note that dependent type name is ::type which is simply not present when std::enable_if_t is usedTympanist
You're right; I was using a c++11 transcription of the cppreference code where the typename is required. The typename is *not needed if one uses std::enable_if_t. That the linked cppreference.com does use typename leads me even further toward thinking that even cppreference is subject to cargo cult programming. They used it because someone else did.Epistasis
I would prefer a chapter and verse explanation as to why an invalid default typename parameter is ignored while an invalid default non-type parameter is not ignored. Both of the answers to date are cargo cult answers as far as I'm concerned, as is cppreference.Epistasis
@DavidHammen Both default parameters are completely ignored by compiler when it tries to distinguish between these two template functions. It does not matter whether values of default parameters are valid or not.Tympanist
Please cite chapter and verse. If what you wrote is true, SFINAE has very little, if any, value. It has lots of value, but it also has a lot of cargo cult programming behind it. I want to get beyond that cargo cult programming stuff.Epistasis
@DavidHammen I'm not sure why are you so focused on SFINAE, it has nothing to do with the problem of "Wrong" code variant. Actually whether template parameters are type template parameters or non-type template parameters is irrelevant as well. The problem is that compiler is not able to distinguish those template functions because their signatures are equivalent. So if one wants to utilize normal overloading or SFINAE he first must ensure that template function signatures are different.Tympanist
@DavidHammen Also note that approach with non-type template parameter used in "Right" variant is not mandatory, it is just one of the alternatives.Tympanist
C
1

It's not about type or non-type

The point is : Does it pass the first step of Two phase lookup.

Why ? Because SFINAE work in the second phase of the lookup, when the template is called (as @cpplearner said)

So :

This don't work (case 1):

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

And this work as well as your non-type case (case 2):

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

In the case one, the compiler see : same name, same number of template argument and the argument are not template dependent, same arugments => it's same thing => ERROR

In the case two, not the same number of argument, well let's see if it's works later => SFINAE => OK

In you RIGHT case : the compiler see : same name, same number of template argument and the argument ARE template dependent ( with a default value but he doesn't care for now) => let's see when it's call => SFINAE => OK

By the way, how do you call the constructor ?

From this post

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

And you realy can't :

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

error: cannot call constructor 'T::T' directly [-fpermissive]

You still can make it work with specialization and 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;
}

This is C++14 style, in 17 may be you can come up with a version that compile with just T t(1) but I am not an expert of Class template argument deduction

Cimon answered 18/4, 2019 at 17:56 Comment(3)
Your version without the = doesn't actually work. It just doesn't fail compilation until you actually try to instantiate it an instance of T. Not that it would add much insight if it did work, since it is very similar to the /* RIGHT */ version after removing the =Spacesuit
Aaaand I just figured out why it doesn't work when I try to instantiate it, because you don't have a default parameter for the second template argument (which you can't give since enable_if default type is void), it needs to be added it explicitly. See my answerSpacesuit
@Spacesuit thx for point it out, I know it won't work, my answer wasn't clear enough I will rephrase itCimon
S
1

I am going to do a minor rewrite of the wrong version to help talk about what is going on.

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

All I've done is given the previously anonymous second parameter a name -- U.

The reason this first version does not work is because there is not a way to decide between the two in the case that you explicitly give the second parameter. For example1

f<int,void>(1);

Which function should this be deduced to? If it's the integer version, it of course works -- but what about the float version. Well it has T = int but what about U? Well, we've just given it a type, bool, so we have U = bool. So there is no way to decide between the two in this case, they are identical. (Note that in the integer version we still have U = bool).

So if we explicitly name the second template parameter, the deduction fails. So what? In the actual use case this shouldn't happen. We're going to use something like

f(1.f);

where making the deduction is possible. Well, you will notice that the compiler gives you an error even without a declaration. This means it has decided that it cannot deduce before even giving it a type to deduce since it has detected the issue I point out above. Well from intro.defs we have the signature as

⟨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

And from temp.over.link we know that two template function definitions cannot have the same signature.

Unfortunately, the standard seems to be quite vague on exactly what "template parameter list" means. I searched through a couple different versions of the standard and none of them gave a clear definition that I could find. It is not clear if the "template parameter list" is the same if the a type parameter with a different default value constitutes as unique or not. Given that I am going to say that this is actually undefined behavior, and the a compiler error is an acceptable way to deal with that.

The verdict is still out, if someone can find an explicit definition in the standard for a "template parameter list", I would be happy to add it for a more satisfying answer.

Edit:

As xskxkr noted, the most updated draft does in fact give a more specific definition. Templates have a template-head which contains a template-parameter-list which is a series of template-parameters. It does not include default arguments in the definition. So according to the current draft, having two templates that are the same but with different default arguments is unambiguously wrong, but you are able to "fool" it into thinking that you have two separate template-heads by making the type of the second parameter depend on the outcome of the enable_if.


1 As a side note, I couldn't figure out a way to explicitly instantiate the template constructor of a non-template class. It's a weird construction. I used f in my examples since I could actually get it to work with a free function. Maybe someone else can figure out the syntax?

Spacesuit answered 18/4, 2019 at 22:32 Comment(1)
The wording is changed in the current draft, seemingly by a merge of the Concepts TS.Gulgee

© 2022 - 2024 — McMap. All rights reserved.