Possible to use type_traits / SFINAE to find if a class defines a member TYPE?
Asked Answered
C

4

15

I have seen this question which allows one to check for the existence of a member function, but I'm trying to find out whether a class has a member type.

In the example below, both evaluate to "false", but I would like to find a way so that has_bar<foo1>::value evaluates to false, and has_bar<foo2>::value evaluates to true.

Is this possible?

#include <iostream>

struct foo1;
struct foo2 { typedef int bar; };

template <typename T>
class has_bar
{
    typedef char yes;
    typedef long no;

    template <typename C> static yes check( decltype(&C::bar) ) ;
    template <typename C> static no  check(...);
public:
    enum { value = sizeof(check<T>(0)) == sizeof(yes) };
};

int main()
{
    std::cout << has_bar<foo1>::value << std::endl;
    std::cout << has_bar<foo2>::value << std::endl;
    return 0;
}

Edit: implementing a specialisation in response to the answers below:

...if you use C::bar in the target template, the template will be discarded automatically for types that don't have that nested type.

I have tried to do this, but am clearly missing something

#include <iostream>

struct foo1;
struct foo2 { typedef int bar; };

template <typename T, typename U = void>
struct target
{
    target()
    {
        std::cout << "default target" << std::endl;
    }
};

template<typename T>
struct target<T, typename T::bar>
{
    target()
    {
        std::cout << "specialized target" << std::endl;
    }
};

int main()
{
    target<foo1>();
    target<foo2>();
    return 0;
}
Carhop answered 5/8, 2012 at 4:22 Comment(1)
foo1 is incomplete – is this intentional?Lonna
E
13

You cannot obtain a pointer to member to a type member:

template <typename C> static yes check( decltype(&C::bar) ) ;

The subexpression &C::bar will only be valid when bar is a non-type member of C. But what you need to check is whether it is a type. A minimal change to your template could be:

template <typename C> static yes check( typename C::bar* ) ;

If bar is a nested type of C, then that function overload will be a valid candidate (the 0 will be a pointer to whatever C::bar type is), but if C does not contain a nested bar then it will be discarded and the second test will be the only candidate.

There is a different question as of whether the trait is needed at all, since if you use C::bar in the target template, the template will be discarded automatically for types that don't have that nested type.


EDIT

What I meant is that in your approach you need to create a trait for each and every possible nested type, just to generate a template that does or does not hold a nested type (enable_if). Let's take a different approach... First we define a general utility to select a type based on a condition, this is not required for this problem, and a simpler template <typename T> void_type { typedef void type; }; would suffice, but the utility template can be useful in other cases:

// General utility: if_<Condition, Then, Else>::type
// Selects 'Then' or 'Else' type based on the value of 
// the 'Condition'
template <bool Condition, typename Then, typename Else = void>
struct if_ {
   typedef Then type;
};
template <typename Then, typename Else>
struct if_<false, Then, Else > {
   typedef Else type;
};

Now se just need to use SFINAE for class template specializations:

template <typename T, typename _ = void> 
struct target {
   // generic implementation
};

template <typename T>
struct target<T, typename if_<false,typename T::bar>::type> {
   // specialization for types holding a nested type `T::bar`
};

Note that the main difference with your approach is the use of an extra intermediate template (the one for which Substitution will Fail --and Is Not An Error) that yields a void type (on success). This is the reason why the void_type template above would also work: you just need to use the nested type as argument to a template, and have that fail, you don't really care what the template does, as long as the evaluation is a nested type (that must be void) if it succeeds.

In case it is not obvious (it wasn't at first for me) why your approach doesn't work, consider what the compiler needs to do when it encounters target<foo2>: The first step is finding that there is a template called target, but that template takes two arguments of which only one was provided. It then looks in the base template (the one that is not specialized) and finds that the second argument can be defaulted to void. From this point on, it will consider your instantiation to be: target<foo2,void> (after injecting the defaulted argument). And it will try to match the best specialization. Only specializations for which the second argument is void will be considered. Your template above will only be able to use the specialized version if T::bar is void (you can test that by changing foo2 to: struct foo2 { typedef void bar; }. Because you don't want the specialization to kick in only when the nested type is void you need the extra template that will take C::bar (and thus fail if the type does not contain a nested bar) but will always yield void as the nested type.

Expansive answered 5/8, 2012 at 4:54 Comment(6)
Thanks David. I have been able to get the trait to return what I wanted, but then tried to implement this without using a trait at all, as you suggested. I have edited my question to reflect this - would you be able to comment on how to achieve what you were suggesting?Carhop
@lori : David is not suggesting that a trait is never needed, only that it might not be needed in your particular use-case -- in many cases, a trait is exactly the appropriate solution, but in others it might be overkill. For example, see this demo which does not use a trait, but uses simple overloading instead. If you do need a trait, this demo shows one.Parade
This will not work for types that are hidden by non-types, such as struct A { void bar(); struct bar {}; };Aric
@Johannes : OTOH, I'd be okay with shooting anyone who writes code like that, so *shrug*. :-PParade
@lori: I have updated the answer to show how you don't really need to write a handcrafted trait class to detect this, and a generic approach will be both more concise and flexible.Smukler
With C++11, there's no need to define if_, you can use std::conditionalMensal
T
15

Try this

template<class T>
struct Void {
  typedef void type;
};

template<class T, class U = void>
struct has_bar {
    enum { value = 0 };
};

template<class T>
struct has_bar<T, typename Void<typename T::bar>::type > {
    enum { value = 1 };
};
Teocalli answered 5/8, 2012 at 13:54 Comment(3)
+1, This is probably the simplest approach to get the trait (although I would avoid writing a trait for that, as the same technique can be directly applied where needed)Smukler
this is coming to the standard under the name void_tHalfcock
I prefer struct has_bar final: public std::(false|true)_type, as then you get its typedefs, converting constructors, etc. for free.Roughrider
E
13

You cannot obtain a pointer to member to a type member:

template <typename C> static yes check( decltype(&C::bar) ) ;

The subexpression &C::bar will only be valid when bar is a non-type member of C. But what you need to check is whether it is a type. A minimal change to your template could be:

template <typename C> static yes check( typename C::bar* ) ;

If bar is a nested type of C, then that function overload will be a valid candidate (the 0 will be a pointer to whatever C::bar type is), but if C does not contain a nested bar then it will be discarded and the second test will be the only candidate.

There is a different question as of whether the trait is needed at all, since if you use C::bar in the target template, the template will be discarded automatically for types that don't have that nested type.


EDIT

What I meant is that in your approach you need to create a trait for each and every possible nested type, just to generate a template that does or does not hold a nested type (enable_if). Let's take a different approach... First we define a general utility to select a type based on a condition, this is not required for this problem, and a simpler template <typename T> void_type { typedef void type; }; would suffice, but the utility template can be useful in other cases:

// General utility: if_<Condition, Then, Else>::type
// Selects 'Then' or 'Else' type based on the value of 
// the 'Condition'
template <bool Condition, typename Then, typename Else = void>
struct if_ {
   typedef Then type;
};
template <typename Then, typename Else>
struct if_<false, Then, Else > {
   typedef Else type;
};

Now se just need to use SFINAE for class template specializations:

template <typename T, typename _ = void> 
struct target {
   // generic implementation
};

template <typename T>
struct target<T, typename if_<false,typename T::bar>::type> {
   // specialization for types holding a nested type `T::bar`
};

Note that the main difference with your approach is the use of an extra intermediate template (the one for which Substitution will Fail --and Is Not An Error) that yields a void type (on success). This is the reason why the void_type template above would also work: you just need to use the nested type as argument to a template, and have that fail, you don't really care what the template does, as long as the evaluation is a nested type (that must be void) if it succeeds.

In case it is not obvious (it wasn't at first for me) why your approach doesn't work, consider what the compiler needs to do when it encounters target<foo2>: The first step is finding that there is a template called target, but that template takes two arguments of which only one was provided. It then looks in the base template (the one that is not specialized) and finds that the second argument can be defaulted to void. From this point on, it will consider your instantiation to be: target<foo2,void> (after injecting the defaulted argument). And it will try to match the best specialization. Only specializations for which the second argument is void will be considered. Your template above will only be able to use the specialized version if T::bar is void (you can test that by changing foo2 to: struct foo2 { typedef void bar; }. Because you don't want the specialization to kick in only when the nested type is void you need the extra template that will take C::bar (and thus fail if the type does not contain a nested bar) but will always yield void as the nested type.

Expansive answered 5/8, 2012 at 4:54 Comment(6)
Thanks David. I have been able to get the trait to return what I wanted, but then tried to implement this without using a trait at all, as you suggested. I have edited my question to reflect this - would you be able to comment on how to achieve what you were suggesting?Carhop
@lori : David is not suggesting that a trait is never needed, only that it might not be needed in your particular use-case -- in many cases, a trait is exactly the appropriate solution, but in others it might be overkill. For example, see this demo which does not use a trait, but uses simple overloading instead. If you do need a trait, this demo shows one.Parade
This will not work for types that are hidden by non-types, such as struct A { void bar(); struct bar {}; };Aric
@Johannes : OTOH, I'd be okay with shooting anyone who writes code like that, so *shrug*. :-PParade
@lori: I have updated the answer to show how you don't really need to write a handcrafted trait class to detect this, and a generic approach will be both more concise and flexible.Smukler
With C++11, there's no need to define if_, you can use std::conditionalMensal
T
2

C++20 Update:

It is now much more easier to check whether a given type contains a specific type definition.

template<typename T>
concept has_bar = requires {
    typename  T::bar;
};

... so your example code evolves to this:

#include <iostream>

struct foo1;
struct foo2 { typedef int bar; };

template <typename T, typename U = void>
struct target
{
    target()
    {
        std::cout << "default target" << std::endl;
    }
};

template<typename T>
requires(has_bar<T>)
struct target<T>
{
    target()
    {
        std::cout << "specialized target" << std::endl;
    }
};

int main()
{
    target<foo1>();
    target<foo2>();
    return 0;
}

Example on gcc.godbolt: https://gcc.godbolt.org/z/a15G13

Tenure answered 1/1, 2021 at 10:27 Comment(0)
V
1

I prefer to wrap it in macro.

test.h:

#include <type_traits>

template<typename ...>
struct void_type
{
     using type = void;
};

template<typename ...T>
using void_t = typename void_type<T...>::type;

#define HAS_TYPE(NAME) \
template<typename, typename = void> \
struct has_type_##NAME: std::false_type \
{}; \
template<typename T> \
struct has_type_##NAME<T, void_t<typename T::NAME>>: std::true_type \
{} \

HAS_TYPE(bar);

test.cpp:

#include <iostream>

struct foo1;
struct foo2 { typedef int bar; };

int main()
{
    std::cout << has_type_bar<foo1>::value << std::endl;
    std::cout << has_type_bar<foo2>::value << std::endl;
    return 0;
}
Verdugo answered 27/3, 2015 at 13:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.