C++ Template Trick: Detecting the Existence of Class Member at Compile Time

C++0x will provide a full set of type traits helpers to ease generic programming. However, there is no support for the detection of class members. The general problem is hard. Here we will try to tackle the more specific version: detecting the class member with given name and type.

In C++, function overload is one of the most widely used technique to implement type traits. However, function overload only cares about types. Default argument and access modifier are only considered after the overload resolution. What we want here is to find out whether the specific member exists. So we have to turn the member into the type. Fortunately, template supports non-type argument. And here is the magic:

namespace van {
    namespace type_traits {
        namespace detail {
            typedef char Small;
            struct Big {char dummy[2];};

            template<typename Type,Type Ptr>
            struct MemberHelperClass;

            template<typename T,typename Type>
            Small MemberHelper_f(MemberHelperClass<Type,&T::f> *);
            template<typename T,typename Type>
            Big MemberHelper_f(...);
        }

        template<typename T,typename Type>
        struct has_member_f
        {
            enum {value=sizeof(detail::MemberHelper_f<T,Type>(0))==sizeof(detail::Small)};
        };
    }
}

struct A
{
    static void f();
};
struct B
{
};

#include <iostream>
using namespace std;

int main()
{
    cout<<boolalpha;
    cout<<van::type_traits::has_member_f<A,void (*)()>::value<<endl;
    cout<<van::type_traits::has_member_f<B,void (*)()>::value<<endl;
}

If the member "f" is missing, the non-type (&T::f) to type (MemberHelperClass) conversion will be invalid, so the va-arg version will be chosen. Otherwise, the former will be chosen because the va-arg version is always the least preferable. Then has_member_f will distinguish these two cases by checking the size of the return value of the chosen MemberHelper_f function. The above code supports both static members and non-static members. It also supports both data members and function members. However, it has one drawback. If the detected member is non-public, there will be compiler error. That is because access control is applied after the overload resolution.

Because the member name itself cannot be used as a template argument, we have to use it explicitly in our helper. To prevent the redundant work, we can take advantage of macro to get a more general version:

#define DEFINEHASMEMBER(Name)\
namespace van {\
    namespace type_traits {\
        namespace detail {\
            template<typename T,typename Type>\
            Small MemberHelper_##Name(MemberHelperClass<Type,&T::Name> *);\
            template<typename T,typename Type>\
            Big MemberHelper_##Name(...);\
        }\
\
        template<typename T,typename Type>\
        struct has_member_##Name\
        {\
            enum {value=sizeof(detail::MemberHelper_##Name<T,Type>(0))==sizeof(detail::Small)};\
        };\
    }\
}

One usage of this type trait is to simplify dispatcher. For example, we want to provide different implementation for different architecture to get better performance. Instead of dispatch the code manually, we can automate the work using the member detection trait.

First, we group the different implementations into one helper class.

struct MemoryCopyHelper
{
    typedef void (*FunctionType)(const void *lpDest, void *lpSrc, size_t n);
    static void Default(const void *lpDest, void *lpSrc, size_t n){}
    static void MMX(const void *lpDest, void *lpSrc, size_t n){}
};

Second, we create the array to store the address of each implementation. If the implementation for some architecture is missing, we can use the default one instead (assume the default one is always present).

DEFINEHASMEMBER(Default)
DEFINEHASMEMBER(MMX)
DEFINEHASMEMBER(SSE2)

#define DEFINESELECTSTATICMEMBER(MemberName)\
    template<typename T,typename FunType,bool = van::type_traits::has_member_##MemberName<T,FunType>::value>\
    struct select_member_##MemberName;\
    template<typename T,typename FunType>\
    struct select_member_##MemberName<T,FunType,true> {static const FunType value;};\
    template<typename T,typename FunType>\
    struct select_member_##MemberName<T,FunType,false> {static const FunType value;};\
    template<typename T,typename FunType>\
    const FunType select_member_##MemberName<T,FunType,true>::value=&T::MemberName;\
    template<typename T,typename FunType>\
    const FunType select_member_##MemberName<T,FunType,false>::value=&T::Default;

DEFINESELECTSTATICMEMBER(Default)
DEFINESELECTSTATICMEMBER(MMX)
DEFINESELECTSTATICMEMBER(SSE2)

MemoryCopyHelper::FunctionType gDispatchArray_MemoryCopy[]={
    select_member_Default<MemoryCopyHelper, MemoryCopyHelper::FunctionType>::value,
    select_member_MMX<MemoryCopyHelper, MemoryCopyHelper::FunctionType>::value,
    select_member_SSE2<MemoryCopyHelper, MemoryCopyHelper::FunctionType>::value,
};

Then you can focus on the implementation. You can update the helper class to add the optimized version for the missing architecture in the future. The array will be automatically updated. (Notice: the above array will be initialized dynamically before entering into main)

BTW: The above code may fail on some old compilers. It is OK with VC8, VC9, gcc 3.4.5.