I was thinking that I could test (with C 14) if a class contained a type, I could do this:
#include <type_traits>
struct X {
using some_type = int;
};
struct Y {};
template <typename T, typename = void>
struct has_some_type : std::false_type {};
template <typename C>
struct has_some_type<C, typename C::some_type> : std::true_type {};
static_assert(has_some_type<X>::value); // unexpectedly fails
static_assert(has_some_type<Y>::value); // correctly fails
But the static_assert
fails, which surprised me, since checking for a member function would work in a similar way.
#include <type_traits>
struct X {
void some_function();
};
struct Y {};
template <typename T, typename = void>
struct has_some_function : std::false_type {};
template <typename C>
struct has_some_function<C, decltype(std::declval<C>().some_function())> : std::true_type {};
static_assert(has_some_function<X>::value); // correctly succeeds
static_assert(has_some_function<Y>::value); // correctly fails
Why doesn't this work and how would I test if a class has a type?
CodePudding user response:
You are using void_t
idiom incorrectly. The correct way is:
template <typename C>
struct has_some_type<C, std::void_t<typename C::some_type>> : std::true_type {};
static_assert(has_some_type<X>::value);
static_assert(!has_some_type<Y>::value);
For C 14, define void_t
as:
template<typename T> struct void_impl { using type=void;};
template<typename T> using void_t = typename void_impl<T>::type;
To recap the idiom:
has_f<X>
is completed with the default arguments from the base template tohas_f<X,void>
.- The compiler tries to find all matching specializations for
has_f<X,void>
. -
- Original -
has_some_type<C, typename C::some_type>
is considered,C
can be deduced toX
,X::some_type
is valid but is not of typevoid
, so the specialization doesn't match an the primary template is used. - Fixed -
has_some_type<C, std::void_t<typename C::some_type>>
is considered, C can again be deduced, this timestd::void_t<typename C::some_type>
is valid expression of typevoid
. This matches and is considered more specialized than the primary template and thus chosen.
- Original -
Meaning, you always want the expression inside the argument to evaluate to whatever is the default type. The expression just cleverly contains the syntax you want to test.
Second example:
struct has_some_function<C, std::void_t<decltype(std::declval<C>().some_function())>>
Cast to void will also work in this case:
<C, decltype((void)std::declval<C>().some_function())>
CodePudding user response:
I know the question is tagged C 14, but for the sake of other people landing here:
In C 20 and above, concepts provide a clean syntax for checking if a type has a given type member:
template<typename T>
concept has_some_type = requires {
typename T::some_type;
};
struct X {
using some_type = int;
};
struct Y {};
static_assert(has_some_type<X>);
static_assert(!has_some_type<Y>);
CodePudding user response:
Why does the has_some_type
trait fail?
That is because the deduction of the second template parameter follows the primary template, and doesn't "take hints" from your template specialization.
Think of your partial specialization of has_some_type
when you substitute X
for C
:
template <>
struct has_some_type<X, int> : std::true_type {};
this substitution doesn't fail, but - why would it match has_some_type<X>
? When the compiler sees has_some_type<X>
, it will ruthlessly ignore your attempts to catch its attention, and simply use the = void
for deduction.
How can we fix the has_some_type
trait?
While C 17 and C 20 offer easier solutions (as suggested by @Frank and @Quimby) - let's stick to C 14 and think about fixing what we've already written.
If some_type
were void
, has_some_type
would work for X
and Y
(really!); and it would also work if you had written template <typename T, typename = int>
... but then it would only work for some_type
being int
. That's not good enough :-(
So, can we have a type whose definition is: "check the validity of some_type
, whatever it may be, but be void
"? Yes, it turns out that we can.
#include <type_traits>
struct X {
using some_type = int;
};
struct Y {};
template<typename T>
using void_t = void;
// Please Mr. Compiler, make sure T is a legit type, then use void.
template<typename, typename = void>
struct has_some_type : std::false_type { };
template<typename T>
struct has_some_type<T, void_t<typename T::some_type>> : std::true_type { };
static_assert(has_some_type<X>::value, "Unfortunately, X doesn't have some_type");
static_assert(has_some_type<Y>::value, "Unfortunately, Y _does_ have some_type");
Only the second assertion fails.
See this in action on GodBolt.
Notes:
- This solution is actually valid C 11.
- C 17 introduced
std::void_t
to save you some typing. It also supports variadic parameter packs rather than just single types. - If you use an old compiler, the code above may fail due to C standard defect CWG1358.