Reputation: 46
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
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
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
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