How to decompose a pointer-to-member in C++ (get class and member types)?

Viewed 147

I have this scenario:

#include <iostream>

class SomeClass
{
public:
    int _int;
};

#define DO_SOME_STUFF(ptr) std::cout << /* Print the typeid().hash_code() of the type which ptr is poiting to (int) */;

int main()
{
    int SomeClass::* ptr_to_int_member = &SomeClass::_int;
    DO_SOME_STUFF(ptr_to_int_member)
}

I want to know which type is ptr pointing at (which is currently int). Knowing which class owns that int is also useful (which is currently SomeClass).

2 Answers

You can do that with a "template trick":

template<typename T>
struct PointerToMemberDecomposer {};

template<typename T, typename P>
struct PointerToMemberDecomposer<P T::*>
{
    using ClassType = T;
    using MemberType = P;
};

And change your code to:

#include <iostream>

template<typename T>
struct PointerToMemberDecomposer {};

template<typename T, typename P>
struct PointerToMemberDecomposer<P T::*>
{
    using ClassType = T;
    using MemberType = P;
};

class SomeClass
{
public:
    int _int;
};

#define DO_SOME_STUFF(ptr) std::cout << typeid(PointerToMemberDecomposer<decltype(ptr)>::MemberType).hash_code();

int main()
{
    int SomeClass::* ptr_to_int_member = &SomeClass::_int;
    DO_SOME_STUFF(ptr_to_int_member)
}

Defining a couple of templated aliases can make the code a little bit cleaner:

#define GET_POINTER_TO_MEMBER_CLASS_TYPE(ptr) PointerToMemberDecomposer<decltype(ptr)>::ClassType
#define GET_POINTER_TO_MEMBER_MEMBER_TYPE(ptr) PointerToMemberDecomposer<decltype(ptr)>::MemberType

So you can change DO_SOME_STUFF to:

#define DO_SOME_STUFF(ptr) std::cout << typeid(GET_POINTER_TO_MEMBER_MEMBER_TYPE(ptr)).hash_code();

Explanation

This technique is called Partial template specialization. The second definition of PointerToMemberDecomposer will be used when a pointer-to-member type is passed as template argument; And will catch new T and P typenames. using those new typenames; It will define two type aliases (ClassType and MemberType) so T and P can be used outside of the PointerToMemberDecomposer struct.

When using PointerToMemberDecomposer; you should use decltype operator which acts like type in Python or typeof in C#. decltype(x) passes the type of x instead of x itself.


Update

As 463035818_is_not_a_number have mentioned; macros can be replaced with templated aliases

template <typename T>
using ClassTypeFromPtrToMember_t = typename PointerToMemberDecomposer<T>::ClassType;

template <typename T>
using MemberTypeFromPtrToMember_t = typename PointerToMemberDecomposer<T>::MemberType;

But you should still use decltype while DO_SOME_STUFF is a macro instead of a templated function and we cant access ptr's type directly (see 463035818_is_not_a_number's answer for templated function version of DO_SOME_STUFF):

#define DO_SOME_STUFF(ptr) std::cout << typeid(MemberTypeFromPtrToMember_t<decltype(ptr)>).hash_code();

In this case; DO_SOME_STUFF can be converted to a templated function. But you might want to for example fill a non capturing lambda with macro arguments; which requires DO_SOME_STUFF to be a macro.

Also, you might want to change ClassType and MemberType to type and create two separated structs (or classes) for retrieving those type aliases; If you want PointerToMemberDecomposer to look like C++'s standard library.

For more details; see 463035818_is_not_a_number's answer

Just summarizing comments to some otherwise great answer...

Member aliases are commonly named just type. Macros are better avoided (Why are preprocessor macros evil and what are the alternatives?) and for less verbosity on the caller you can use a function template:

#include <iostream>
#include <typeinfo>

template<typename T>
struct TypeFromPtrToMember; // needs no definition

template<typename T, typename P>
struct TypeFromPtrToMember<P T::*>
{
    using type = T;
};

class SomeClass
{
public:
    int _int;
};

template <typename T>
void do_some_stuff(T t){
    std::cout << typeid(typename TypeFromPtrToMember<T>::type).hash_code();;
}

int main()
{
    int SomeClass::* ptr_to_int_member = &SomeClass::_int;
    do_some_stuff(ptr_to_int_member);
}

Naming the member alias type is so common that I would do it even if you then need two traits. The other trait is basically the same just with using type = P;.

In the above, there is still the little annoyance of having to write typename when using the trait (because TypeFromPtrToMember<T>::type is a dependent name). Since C++11 we can use a template alias to help with that. Template aliases cannot be partially specialized, but we already have the trait and just need to forward to that:

template <typename T>
using TypeFromPtrToMember_t = typename TypeFromPtrToMember<T>::type;

Such that do_some_stuff can be:

template <typename T>
void do_some_stuff(T t){
    std::cout << typeid(TypeFromPtrToMember_t<T>).hash_code();;
}

I hope you agree that now no macros are needed anymore.

Related