Skip to content

More C++ Idioms/Member Detector

Motivation

Compile-time reflection capabilities are the cornerstone of C++ template meta-programming. Type traits libraries such as Boost.TypeTraits and TR1 <type_traits> header provide powerful ways of extracting information about types and their relationships. Detecting the presence of a data member in a class is also an example of compile-time reflection.

Solution and Sample Code

Before C++11

Member detector idiom is implemented using the Substitution Failure Is Not An Error (SFINAE) idiom. The following class template DetectX<T> is a meta-function that determines whether type T has a data or function member named X in it or not. Note that the type of the data member X does not matter nor does the return value and arguments of the member function (if it is one).

template<typename T>
class DetectX
{
    struct Fallback
    {
        int X;
    }; // add member name "X"
    struct Derived: T, Fallback
    {
    };
    template<typename U, U>
    struct Check;
    typedef char ArrayOfOne[1]; // typedef for an array of size one.
    typedef char ArrayOfTwo[2]; // typedef for an array of size two.
    // Check<int Fallback::*, &U::X> 是入参类型
    template<typename U>
    static ArrayOfOne& func(Check<int Fallback::*, &U::X>*);

    template<typename U>
    static ArrayOfTwo& func(...);
public:
    typedef DetectX type;
    enum
    {
        value = sizeof(func<Derived>(0)) == 2
    };
};

NOTE:

一、理解上述code的前提是理解C++ pointer to member grammar,参见:

1、stackoverflow Pointer to class data member “::*” # A

#include <iostream>
using namespace std;

class Car
{
public:
  int speed;
};

int main()
{
  int Car::*pSpeed = &Car::speed;

  Car c1;
  c1.speed = 1;       // direct access
  cout << "speed is " << c1.speed << endl;
  c1.*pSpeed = 2;     // access via pointer to member
  cout << "speed is " << c1.speed << endl;
  return 0;
}
// g++ test.cpp

输出如下:

speed is 1
speed is 2

This idiom works by creating a controlled ambiguity during compilation and recovering from that using the SFINAE idiom. First proxy class, Fallback, has a data member of the same name that we want to detect the presence of. Class Derived inherits from both T and Fallback. As a result, Derived class will have at least one data member named X. Potentially, Derived class may have two X data members if T also has one.

Check template

The Check template is used to create controlled ambiguity. Check template takes two parameters. First is a type parameter and the second is an instance of that type. For example, Check<int, 5> is be a valid instantiation.

func

Two overloaded functions named func create an overload-set as often done in the SFINAE idiom. The first func function can be instantiated only if the address of data member U::X can be taken unambiguously.

Address of U::X can be taken if there is exactly one X data member in the Derived class; i.e., T does not have data member X. If T has X in it, the address of U::X can’t be taken without further disambiguation and therefore the instantiation of the first func fails and the other function is chosen, all without an error.

Note the difference between the return types of the two func functions. The first function returns a reference to array of size one whereas the second one returns a reference to an array of size two. This difference in the sizes allows us to identify which function was instantiated.

value

Finally, a boolean value is exposed, which is true only if the sizeof the return type of the function is two. That is, when the second func is instantiated only because T has X data member.

Macro

For every different member that is to be detected, the above class template needs to change. A macro would be preferable in such cases. The following sample code demonstrates the use of a macro.

#include <iostream>
#include <utility>

#define CREATE_MEMBER_DETECTOR(X)\
template<typename T>\
class Detect_##X\
{\
    struct Fallback\
    {\
        int X;\
    };\
    struct Derived: T, Fallback\
    {\
    };\
\
    template<typename U, U> struct Check;\
\
    typedef char ArrayOfOne[1];\
    typedef char ArrayOfTwo[2];\
\
    template<typename U> static ArrayOfOne & func(Check<int Fallback::*, &U::X> *);\
    template<typename U> static ArrayOfTwo & func(...);\
\
public:\
    typedef Detect_##X type; \
    enum\
    {\
        value = sizeof(func<Derived>(0)) == 2\
    };\
};

CREATE_MEMBER_DETECTOR(first);
CREATE_MEMBER_DETECTOR(second);
int main(void)
{
    typedef std::pair<int, double> Pair;
    std::cout << ((Detect_first<Pair>::value && Detect_second<Pair>::value) ? "Pair" : "Not Pair") << std::endl;
}
// g++ test.cpp

NOTE:

1、输出如下:

Pair

C++11

Using C++11 features, this example can be rewritten so that the controlled ambiguity is created using the decltype specifier instead of a Check template and a pointer to member. The result can then be wrapped inside a class inheriting from integral_constant to provide an interface identical to the type predicates present in the standard header <type_traits>.

#include <type_traits> // To use 'std::integral_constant'.
#include <iostream> // To use 'std::cout'.
#include <iomanip> // To use 'std::boolalpha'.

#define GENERATE_HAS_MEMBER(member)\
template<class T>\
class HasMember_ ##member\
{\
private:\
    using Yes = char[2];\
    using No = char[1];\
\
    struct Fallback\
    {\
        int member;\
    };\
\
    struct Derived: T, Fallback\
    {\
    };\
\
    template<class U>\
    static No& test(decltype(U::member)*);\
\
    template<typename U>\
    static Yes& test(U*);\
\
public:\
\
    static constexpr bool RESULT = sizeof(test<Derived>(nullptr)) == sizeof(Yes);\
};\
\
template < class T >\
struct has_member_ ##member\
: public std::integral_constant<bool, HasMember_##member<T>::RESULT>\
{\
\
};

// Creates 'has_member_att'.
GENERATE_HAS_MEMBER(att)
// Creates 'has_member_func'.
GENERATE_HAS_MEMBER(func)

struct A
{
    int att;
    void func(double);
};
struct B
{
    char att[3];
    double func(const char*);
};
struct C: A, B
{
};
// It will also work with ambiguous members.
int main()
{
    std::cout << std::boolalpha << std::endl;
    std::cout << "'att' in 'C' : " << has_member_att<C>::value << std::endl; // <type_traits>-like interface.
    std::cout << "'func' in 'C' : " << has_member_func<C>() << std::endl; // Implicitly convertible to 'bool'.
}
// g++ --std=c++11 test.cpp

NOTE: 上述程序的输出如下:

'att' in 'C' : true
'func' in 'C' : true

Detecting member types

The above example can be adapted to detect existence of a member type, be it a nested class or a typedef, even if it is incomplete. This time, the ambiguity would be created by adding to Fallback a nested type with the same name as the macro argument.

#include <type_traits> // To use 'std::integral_constant'.
#include <iostream> // To use 'std::cout'.
#include <iomanip> // To use 'std::boolalpha'

#define GENERATE_HAS_MEMBER_TYPE(Type)\
template<class T>\
class HasMemberType_ ##Type\
{\
private:\
    using Yes = char[2];\
    using No = char[1];\
    struct Fallback\
    {\
        struct Type\
        {\
        };\
    };\
    struct Derived: T, Fallback\
    {\
    };\
\
    template<class U>\
    static No& test(typename U::Type*);\
\
    template<typename U>\
    static Yes& test(U*);\
\
public:\
    static constexpr bool RESULT = sizeof(test<Derived>(nullptr)) == sizeof(Yes);\
};\
\
template<class T>\
struct has_member_type_ ##Type\
: public std::integral_constant<bool, HasMemberType_##Type<T>::RESULT>\
{\
};

// Creates 'has_member_type_Foo'.
GENERATE_HAS_MEMBER_TYPE(Foo)
struct A
{
    struct Foo;
};
struct B
{
    using Foo = int;
};
struct C: A, B
{
};

// Will also work on incomplete or ambiguous types.
int main()
{
    std::cout << std::boolalpha;
    std::cout << "'Foo' in 'C' : " << has_member_type_Foo<C>::value << "\n";
}
// g++ --std=c++11 test.cpp

This is much like the member detection macro that declares a data member of the same name as the macro argument. An overload set of two test functions is then created, just like the other examples. The first version can only be instantiated if the type of U::Type can be used unambiguously. This type can be used only if there is exactly one instance of Type in Derived, i.e. there is no Type in T. If T has a member type Type, it is garanteed to be different than Fallback::Type, since the latter is a unique type, hence creating the ambiguity. This leads to the second version of test being instantiated in case the substitution fails, meaning that T has indeed a member type Type. Since no objects are ever created (this is entirely resolved by compile-time type checking), Type can be an incomplete type or be ambiguous inside T; only the name matters. We then wrap the result in a class inheriting from integral_constant, just like before to provide the same interface as the standard library.

Detecting overloaded member functions

A variation of the member detector idiom can be used to detect existence of a specific member function in a class even if it is overloaded.

template<typename T, typename RESULT, typename ARG1, typename ARG2>
class HasPolicy
{
    template<typename U, RESULT (U::*)(ARG1, ARG2)>
    struct Check;
    template<typename U> static char func(Check<U, &U::policy> *);
    template<typename U> static int func(...);
public:
    typedef HasPolicy type;
    enum
    {
        value = sizeof(func<T>(0)) == sizeof(char)
    };
};

The HasPolicy template above checks if T has a member function called policy that takes two parameters ARG1, ARG2 and returns RESULT. Instantiation of Check template succeeds only if U has a U::policy member function that takes two parameters and returns RESULT. Note that the first type parameter of Check template is a type whereas the second parameter is a pointer to a member function in the same type. If Check template cannot be instantiated, the only remaining func that returns an int is instantiated. The size of the return value of func eventually determines the answer of the type-trait: true or false.

Known Issues

Will not work if the class checked for member is declared final (C++11 keyword).

Will not work to check for a member of a union (unions cannot be base classes).