Reputation: 33126
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"?
Upvotes: 7
Views: 1168
Reputation: 1777
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.
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?
Upvotes: 1
Reputation: 13040
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, ...
Upvotes: 10
Reputation: 2870
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
Upvotes: 1
Reputation: 15908
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.
Upvotes: 6
Reputation: 37587
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.
Upvotes: 3
Reputation: 10982
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
)
Upvotes: 10