How do I test if a type is within another class exists?
Asked Answered
M

4

5

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?

Marenmarena answered 10/11, 2021 at 17:30 Comment(0)
C
3

Why does the has_some_type trait fail?

It's because the deduction of the second template argument only uses 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 deducing the second template parameter.

Why does the has_some_function trait succeed?

Look at the second type you use in the specialization: decltype(std::declval<C>().some_function()). What is that type resolve to, for X? ... yes, it resolves to void. So when you substitute X for C, it's:

template <>
struct has_some_function<X, void> : std::true_type {};

and this will match has_some_function<X>, using the = void from the primary template.

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.

From the has_some_function example, we might be inspired to "fix" the type trait by replacing the primary template with template <typename T, typename = int> ; but while this would work for X and Y, it would not work if you had using some_type = double or any other non-int type. And that's not what we want :-(

So, can we have a type whose definition is: "check the validity of some_type, whatever it may be, but eventually just behave like a single fixed type"? ... 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 doesn't 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 CWG1558.
Coaction answered 10/11, 2021 at 17:43 Comment(2)
A defect, CWG 1558Hax
Thx. Very well explained.Marenmarena
T
5

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 (not to mention also remove the need for static_assert and enable_if in many scenarios) 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>);
Tagalog answered 10/11, 2021 at 17:37 Comment(1)
Yeah. I do like concepts. Not in the cards atm though.Marenmarena
V
3

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:

  1. has_some_type<X> is completed with the default arguments from the base template to has_some_type<X,void>.
  2. The compiler tries to find all matching specializations for has_some_type<X,void>.
    • Original - has_some_type<C, typename C::some_type> is considered, C can be deduced to X, X::some_type is valid but is not of type void, 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 time std::void_t<typename C::some_type> is valid expression of type void. This matches and is considered more specialized than the primary template and thus chosen.

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())>
Velour answered 10/11, 2021 at 17:33 Comment(10)
This is a C++17 solution...Coaction
@Coaction Thanks, I should start reading the tags. Give me a second.Velour
You also refer to OP's use of has_f(), while the failing code is has_some_type.Coaction
@Coaction but has_f only works because the function returns void, I think that is not what OP meant. It's not has_f, more like has_f_returning_void.Velour
I meant, you have your identifiers mixed up. In step 1 you talk about has_f, then in the middle of step 2 you switch to has_some_typeCoaction
@Coaction Oh, you are right, thanks again :) I just copy pasted the wrong thing.Velour
@Quimby, Oops. Yeah. Thanks.Marenmarena
Thanks all. I thought that I tried that before and then was trying everything else when it didn't work as expected. Must have been due to some other reason though.Marenmarena
Why are you defining void_t that way? Why not just use template<typename T> using void_t = void;;? Is that because void isn't a dependent type and this is another, "no diagnostic required" thing?Marenmarena
@Marenmarena Because cppreference warns about a defect in the standard CWG 1558. But I guess with up-to-date compilers either definition should work.Velour
C
3

Why does the has_some_type trait fail?

It's because the deduction of the second template argument only uses 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 deducing the second template parameter.

Why does the has_some_function trait succeed?

Look at the second type you use in the specialization: decltype(std::declval<C>().some_function()). What is that type resolve to, for X? ... yes, it resolves to void. So when you substitute X for C, it's:

template <>
struct has_some_function<X, void> : std::true_type {};

and this will match has_some_function<X>, using the = void from the primary template.

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.

From the has_some_function example, we might be inspired to "fix" the type trait by replacing the primary template with template <typename T, typename = int> ; but while this would work for X and Y, it would not work if you had using some_type = double or any other non-int type. And that's not what we want :-(

So, can we have a type whose definition is: "check the validity of some_type, whatever it may be, but eventually just behave like a single fixed type"? ... 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 doesn't 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 CWG1558.
Coaction answered 10/11, 2021 at 17:43 Comment(2)
A defect, CWG 1558Hax
Thx. Very well explained.Marenmarena
G
0

You can use type_traits:

#include <experimental/type_traits>

struct X { using some_type = int; };
struct Y {};


template <typename T>
using has_some_type_detector = T::some_type;

static_assert( std::experimental::is_detected_v <has_some_type_detector, X> );
static_assert( !std::experimental::is_detected_v <has_some_type_detector, Y> );
Gallo answered 14/11, 2021 at 3:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.