0

I want to be able to customize handling of a struct based on the presence of a type within the struct (without writing any additional code per custom struct), like:

struct Normal_t
{
};

struct Custom_t
{
    using my_custom_type = bool;
};

It seems like I should be able to do something like this, but it doesn't work:

template <class T, typename Enabler = void>
struct has_custom_type
{
    bool operator()() { return false; }
};

template <class T>
struct has_custom_type<T, typename T::my_custom_type>
{
    bool operator()() { return true; }
};

bool b_normal = has_custom_type<Normal_t>()();  // returns false
bool b_custom = has_custom_type<Custom_t>()();  // returns false, INCORRECT? should return true?

What I don't understand is that the standard library uses something similar but seemingly more convoluted for its type traits. For example, this works:

template<bool test, class T = void>
struct my_enable_if
{
};

template<class T>
struct my_enable_if<true, T>
{
    using type = T;
};

template <class T, class Enabler = void>
struct foo
{
    bool operator()() { return false; }
};

template <class T>
struct foo<T, typename my_enable_if<std::is_integral<T>::value>::type>
{
    bool operator()() { return true; }
};

bool foo_float = foo<float>()();    // returns false
bool foo_int = foo<int>()();        // returns true

In both cases, the specialization is happening based on the presence of a type within a struct, in one case typename T::my_custom_type and in the other typename my_enable_if<std::is_integral<T>::value>::type. Why does the second version work but not the first?

I came up with this workaround using the ... parameter pack syntax, but I'd really like to understand if there is a way to do this using normal template specialization without using the parameter pack syntax, and if not, why.

template<typename ...Args>                              
bool has_custom_type_2(Args&& ...args)      { return false; }   

template<class T, std::size_t = sizeof(T::my_custom_type)>  
bool has_custom_type_2(T&)                  { return true; }

template<class T, std::size_t = sizeof(T::my_custom_type)>  
bool has_custom_type_2(T&&)                 { return true; }    /* Need this T&& version to handle has_custom_type_2(SomeClass()) where the parameter is an rvalue */

bool b2_normal = has_custom_type_2(Normal_t()); // returns false
bool b2_custom = has_custom_type_2(Custom_t()); // returns true - CORRECT!
max66
  • 65,235
  • 10
  • 71
  • 111

3 Answers3

2

The problem is that you specify default void type for Enabler, but T::my_custom_type is not void. Either use bool as default type, or use std::void_t that always returns void:

template <class T, typename = void>
struct has_custom_type : std::false_type { };

template <class T>
struct has_custom_type<T, std::void_t<typename T::my_custom_type>> : std::true_type { };

This answer explains why types should match.

Evg
  • 25,259
  • 5
  • 41
  • 83
  • 1
    Thank you! That is exactly what I wanted. Changing that default type was the one thing I hadn't tried. Thank you also for pointing out std::void_t and the use of std::true_type/false_type. I think I now understand why the types need to match: when the template is instantiated by `has_custom_type`, the compiler first _only_ looks at the base template and says "ok, the template parameters are set to ", and only after that does it look for specializations that match and are more specific. – James Thrush Oct 25 '18 at 07:20
  • 1
    @JamesThrush, exactly. – Evg Oct 25 '18 at 08:25
2

As explained by others, if you set a void default value for the second template parameter, your solution works only if my_custom_type is void.

If my_custom_type is bool, you can set bool the default value. But isn't a great solution because loose generality.

To be more general, you can use SFINAE through something that fail if my_custom_type doesn't exist but return ever the same type (void, usually) if my_custom_type is present.

Pre C++17 you can use decltype(), std::declval and the power of comma operator

template <class T, typename Enabler = void>
struct has_custom_type
 { bool operator()() { return false; } };

template <class T>
struct has_custom_type<T,
   decltype( std::declval<typename T::my_custom_type>(), void() )>
 { bool operator()() { return true; } };

Starting from C++17 it's simpler because you can use std::void_t (see Evg's answer, also for the use of std::true_type and std::false_type instead of defining an operator()).

max66
  • 65,235
  • 10
  • 71
  • 111
1
template <class T, typename Enabler = void> // <== void set as default template parameter type
struct has_custom_type
{
    bool operator()() { return false; }
};

template <class T>
struct has_custom_type<T, typename T::my_custom_type>
{
    bool operator()() { return true; }
};

The specialization matches when it gets the template parameters <T, bool>. However, when you just specify <T>, without a second type, then it goes to the default type you specified =void to come up with the call <T, void>, which doesn't match your bool specialization.

Live example showing it matches with explicit <T, bool>: https://godbolt.org/z/MEJvwT

xaxxon
  • 19,189
  • 5
  • 50
  • 80