James Thrush
James Thrush

Reputation: 46

Trying to specialize a template function based on the presence of a typedef within class

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!

Upvotes: 0

Views: 99

Answers (3)

Evg
Evg

Reputation: 26292

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.

Upvotes: 2

max66
max66

Reputation: 66200

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()).

Upvotes: 2

xaxxon
xaxxon

Reputation: 19761

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

Upvotes: 1

Related Questions