Search code examples
c++templatessfinaetype-traits

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?


Solution

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