Default Template Argument When Using Std::Enable_If as Templ. Param.: Why Ok with Two Template Functions That Differ Only in the Enable_If Parameter

Default template argument when using std::enable_if as templ. param.: why OK with two template functions that differ only in the enable_if parameter?

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.

Your functions don't differ only in their default template arguments, they differ in their template parameters, so have different signatures.

In both cases the default template argument is nullptr, but the second template parameter is different in each case.

Duplicate Definitions?

The problem of the first snippet is described here (see how /* WRONG */ vs /* RIGHT */ snippets of code map to your commented and uncommented code respectively).

A common mistake is to declare two function templates that differ only in their default template arguments. This does not work because the declarations are treated as redeclarations of the same function template (default template arguments are not accounted for in function template equivalence).


Here's my understanding of why that's the case.

When the compiler sees this (the correct version)

template <class A, std::enable_if_t<!std::is_same_v<A, double>, bool> = true>
void test() {}

template <class A, std::enable_if_t<std::is_same_v<A, double>, bool> = true>
void test() {}

it doesn't see a redeclaration, because it doesn't know if the two std::enable_if_t will resolve to the same type. If it knew, then it'd be a hard compile time error, just like this is an error:

template <class A, bool = true>
void test() {}

template <class A, bool = false> // no matter the value; signature is the same
void test() {}

This doesn't exclude that the ambiguity can happen at the substitution level. For instance, there's no problem with these overload declarations, in principle

template <class A, std::enable_if_t<std::is_convertible_v<A, double>, bool> = true>
void test() {}

template <class A, std::enable_if_t<std::is_convertible_v<A, int>, bool> = true>
void test() {}

but as soon as you call test<int>(), the ambiguity will pop up, because the compiler will be able to "successfully" instantiate each of the overloads, which both result in template<int, bool = whatever>, which makes them ambiguous.

As regards the wrong version:

template <class A, typename = std::enable_if_t<!std::is_same_v<A, double>>>
void test() {}

template <class A, typename = std::enable_if_t<std::is_same_v<A, double>>>
void test() {}

the problem is that before even seeing how it's used, the compiler complains already, because default template arguments are not accounted for in function template equivalence. Indeed, a much simpler example shows the problem of the previous snippet:

template <class A, typename = typename A::foo>
void test() {}

template <class A, typename = typename A::bar>
void test() {}

Notice that, exactly like in the previous snippet, in this last snippet each of the overloads is correct by itself, until you try using it with a specific A for which the expression of the default argument doesn't make sense, thus causing a hard error.

But if you put the two overloads together, that creates an ambiguity, because default template arguments are not accounted for in function template equivalence.

Issue with multiple overloads using enable_if

From cppreference, which has this very example as a Note:

A common mistake is to declare two function templates that differ only in their default template arguments. This does not work because the declarations are treated as redeclarations of the same function template (default template arguments are not accounted for in function template equivalence).

So what you need to do is not make the sfinae'd type a default argument. Instead, you could make it resolve to some type, e.g. int, and give it a default value, like this:

template <typename T, enable_if_t<is_same_v<T, int>, int> = 0>
void qw(T t)
{
std::cout << "int " << endl;
}

template <typename T, enable_if_t<is_same_v<T, float>, int> = 0>
void qw(T t)
{
cout << "float" << endl;
}

enable_if fails without decltype in return type

Those are not overloads; they are redeclarations (and redefinitions) of the same function template:

template<class, int, class> auto data();

Default template parameters do not make a different function template; nor does a different deduced return type, however swapping auto for decltype(auto) does make a different function template.

If suitable, the best solution would be to use concepts, since a different template-constraint makes for a different function template:

template <typename T, int N = 0>
data_type data() requires (N == 0 && std::is_same_v<T, data_type>) { return 1; }
template <typename T, int N = 0>
auto data() requires (N == 0 && !std::is_same_v<T, data_type>) { return 0; }
template <typename T, int N>
auto data() requires (N != 0) { return -1; }

Otherwise, you could add more dummy template arguments:

template <typename T, int N = 0, typename = typename std::enable_if<N == 0 && !std::is_same<T, data_type>::value>::type>
auto data() { return 0; }
template <typename T, int N, int = 0, typename = typename std::enable_if<N != 0>::type>
auto data() { return -1; } // ^^^^^^ add dummy template parameter

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

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

moving std::enable_if from parameters to template parameters

The problem is that the second template parameter in std::enable_if defaults to void. So your doing something which is pretty much the same as:

template <typename T, void = 0>

Which makes substitution fail always. You could use a non-type template argument of type int, which you can give a default 0 value:

template <typename T, typename std::enable_if< std::is_same<T, int>::value, int >::type = 0>

Do the same for both overloads, and it will work.

Demo here.

Why enable_if_t needs to have datatype identifier and a default value?

The following in itself is completely fine:

template<class T, std::enable_if_t<std::is_integral_v<T>, bool>>
void fun(T value)
{
std::cout << "\n In Integral version";
}

template<class T, std::enable_if_t<std::is_floating_point_v<T>, bool>>
void fun(T value)
{
std::cout << "\n In Floating point version";
}

Its templates with two template arguments. The issue arises when you want to call them. If you try to call them like you do in main:

int main()
{
fun(4);
fun(4.4);
}

You will get an error along the line of:

<source>:18:5: error: no matching function for call to 'fun'
fun(4);
^~~
<source>:5:6: note: candidate template ignored: couldn't infer template argument ''
void fun(T value)
^
<source>:11:6: note: candidate template ignored: couldn't infer template argument ''
void fun(T value)
^
<source>:19:5: error: no matching function for call to 'fun'
fun(4.4);
^~~
<source>:5:6: note: candidate template ignored: couldn't infer template argument ''
void fun(T value)
^
<source>:11:6: note: candidate template ignored: couldn't infer template argument ''
void fun(T value)
^

The templates have 2 template arguments. One is T the other is either a bool or a substitution failure. The second argument cannot be deduced from the function parameters, hence the error.

Consider what you get in case of T==int. std::enable_if_t is just an alias. In your case either for bool or the alias is not defined:

template<int, bool>  // because int is integral
void fun(T value)
{
std::cout << "\n In Integral version";
}

template<int, "substitution failure"> // because int is integral
void fun(T value)
{
std::cout << "\n In Floating point version";
}

The second is a substitution failure, so overload resolution picks the first. And it has two template arguments.

The second template argument is not used for anything but to fail when the condition is false. You do not want the user to call

 fun<int,true>(42);

You want the caller to call it via

 fun(42);

because explicitly specifying the tempalte argument would defeat the whole purpose of SFINAE here. The way to not reqiure the caller to specify the template argument in case it cannot be deduced is to supply a default. And thats the K = true. K = false would work as well. And as you do not have to name the argument the following works as well:

template<class T, std::enable_if_t<std::is_integral_v<T>, bool> = true>
void fun(T value)
{
std::cout << "\n In Integral version";
}

template<class T, std::enable_if_t<std::is_floating_point_v<T>, bool> = true>
void fun(T value)
{
std::cout << "\n In Floating point version";
}

enable_if, SFINAE and template parameters?

There are two kinds of template parameters:

  • type template parameters
  • non-type template parameters

The name of the template parameter is optional

A template parameter can have a default value

Type template parameter

named, non-defaulted

template <class T>
struct X {};

Usage:

X<int> x{};

named, defaulted

template <class T = int>
struct X {};

Usage:

X<> x1{};
X<char> x2{};

unnamed non-defaulted

template <class>
struct X {};

Usage:

X<int> x{};

unnamed, defaulted

template <class = int>
struct X {};

Usage:

X<> x1{};
X<char> x2{};

Non-type template parameter

named, non-defaulted

template <int I>
struct X {};

Usage:

X<24> x{};

named, defaulted

template <int I = 24>
struct X {};

Usage:

X<> x1{};
X<11> x2{};

unnamed, non-defaulted

template <int>
struct X {};

Usage:

X<11> x{};

unnamed, defaulted

template <int = 24>
struct X {};

Usage:

X<> x1{};
X<11> x2{};

For SFINAE the technique requires a default value. Since you don't care for the parameter inside the class you can skip its name. As for the type or non-type you can chose which one is easier to write in your particular case.

Use a enable_if that does not depend on method's template parameter

Rather than try to SFINAE your way into two implementations, just use normal overload resolution.

#include <type_traits>
#include <iostream>

template<class T1, class T2>
class Foo {
template<class InnerT, class ... Args>
void do_bar(InnerT param, std::true_type, Args... args) { std::cout << "same" << std::endl; }

template<class InnerT, class ... Args>
void do_bar(InnerT param, std::false_type, Args... args) { std::cout << "not same" << std::endl; }

public:
template<class InnerT, class ... Args>
void bar(InnerT&& param, Args&&... args)
{
do_bar(std::forward<InnerT>(param), std::is_same<T1, T2>{}, std::forward<Args>(args)...);
}

};

int main() {
Foo<int, int> f1;
Foo<int, double> f2;

f1.bar(1, 2, 3);
f2.bar("Hello");
}

See it live

function template parameter deduction of template parameter vs of default template parameter vs of return type

You can remove everything about enable_if in the question. It boils down to these three:

void is an invalid anonymous template parameter:

template<class T, void>
void SwapInPlace( T& left, T& right );

A void* as an anonymous template parameter with a default value is ok:

template<class T, void* = nullptr >
void SwapInPlace( T& left, T& right );

void as return type is ok:

template<class T>
void SwapInPlace( T& left, T& right );

If you'd changed the first case to a valid anonymous template parameter without a default value, like an int or void*, it would compile:

template<class T, int>
void SwapInPlace( T& left, T& right );

... until you tried actually using it. You'd then get "couldn't deduce template parameter '<anonymous>'" or similar.



Related Topics



Leave a reply



Submit