roschach
roschach

Reputation: 9396

Why adding "* = nullptr" to a template defined with enable_if avoids overloading of a function?

consider this code:

#include <iostream>
#include <type_traits>
#include <string>

struct A{
  int a;
};
struct B{
  std::string b;
};

template<typename T>
auto has_member_a(...) -> std::false_type;

template<typename T>
auto has_member_a(int v) -> decltype(std::declval<T>().a, std::true_type{});

template<typename T>
auto has_member_b(...) -> std::false_type;

template<typename T>
auto has_member_b(int v) -> decltype(std::declval<T>().b, std::true_type{});

template <typename T, std::enable_if_t<decltype(has_member_a<T>(3))::value>>
void customPrint()
{
  std::cout<<"Has member a\n";
}

template <typename T, std::enable_if_t<decltype(has_member_b<T>(3))::value>>
void customPrint()
{
  std::cout<<"Has member b\n";
}


int main()
{
  A a{5};
  B b{"Ciao"};
  customPrint<A>();
  customPrint<B>();
  return 0;
}

Now although the syntax is correct, the compiler gives a redefinition error because it's not able to distinguish between the two (no overloading can happen) since the input parameters are the same.

However, changing the template clauses to:

template <typename T, std::enable_if_t<decltype(has_member_a<T>(3))::value>* = nullptr>

makes everything works fine. Why though? What makes adding * = nullptr> everything fine?

Upvotes: -1

Views: 103

Answers (2)

nirlahori
nirlahori

Reputation: 11

Function template overloading works on the basis of different template signatures. NOTE: Even if multiple function templates have different values for default template parameters in exactly same signature, they do not count as different definitions.

As an example:

template<typename T, std::enable_if_t<std::is_same_v<T, int>>>
void func1(){}

template<typename T, std::enable_if_t<std::is_same_v<T, bool>>>
void func1(){}

In above code, the compiler will deduce only single definition of func1 as both the templates have equivalent signature. Their default parameter values do not come into play in function template overloading.

You have 2 definitions of identical function template (customPrint). Compiler will not overload the customPrint template only because you have two different expressions (has_member_a(3) and has_member_b(3)) inside std::enable_if_t clause. You have to modify the template signature of either of the function template.

The proper way of doing that might me like this:

template <typename T, typename = std::enable_if_t<decltype(has_member_a<T>(3))::value>>
void customPrint()
{
  std::cout<<"Has member a\n";
}

template <typename T, typename std::enable_if_t<decltype(has_member_b<T>(3))::value, int> = 0>
void customPrint()
{
  std::cout<<"Has member b\n";
}

This way both the function templates would have different template signatures. Now compiler will generate two different definitions after parsing their template signatures.

Upvotes: 1

pptaszni
pptaszni

Reputation: 8312

You didn't include your exact error message, but I guess it's something around

<source>:35:6: note:   template argument deduction/substitution failed:
<source>:74:17: note:   couldn't deduce template parameter '<anonymous>'
   74 |   customPrint<A>();

so it is not exactly because of disambiguating, but about compiler's inability to deduce the value of your second template parameter, which is of type

typename std::enable_if<decltype (has_member_b<T>(3))::value, void>::type

which is void. Now there are a couple of different ways to fix it, you can e.g. use some integral type instead of void:

template <typename T, std::enable_if_t<decltype(has_member_b<T>(3))::value, int> >

and instantiate your templates like this (because compiler still cannot deduce the value of your int template parameter, but at least int type can have a value)

  customPrint<A, 0>();
  customPrint<B, 0>();

The easy way to avoid writing 0 explicitly, is to just give the second template parameter a default value:

template <typename T, std::enable_if_t<decltype(has_member_b<T>(3))::value, int> = 0>

And now even more popular way (imho) to do it is to use pointer type (in this example it's void*), which can be assigned nullprt value:

template <typename T, std::enable_if_t<decltype(has_member_a<T>(3))::value>* = nullptr>

The last syntax is probably the shortest version you could get.

Upvotes: 2

Related Questions