Adventures with Templates – Basic C++ Type Introspection without RTTI

Template Metaprogramming

C++ works in weird and wonderful ways, and there is undoubtedly some level of mysticism surrounding anything involving templates. Templating was initially introduced to C++ as a means of writing generic statically typed containers without using macros, and in general, to reduce the amount of code required to perform tasks. But it wasn’t long before its power was harnessed for metaprogramming purposes (by accident), allowing for chunks of code to be evaluated at compile time rather than runtime. I doubt that even devout fans of C++ would deny that its syntax can be downright ugly and bewildering at times, but its beauty is in the sheer power and control it gives the programmer. Part of the C++ philosophy is that the programmer is not restricted to what the language designers had in mind… and there are many innovative design patterns and idioms (examples) resulting from programmers twisting C++ in ways which could never have been imagined by its designers.

Basic ‘Reflection’ without RTTI

A major drawback of C++ is its lack of full runtime reflection. The closest inbuilt feature we have is typeid, the minimal type identification mechanism supported in RTTI (Run-Time Type Information) mode. Unfortunately, its behaviour is undefined and the type_info object it returns is compiler-specific. So typeid(int).name() could return “class int”, it could return “i”, or even “cellar door” if a compiler writer should be in a whimsical mood. Furthermore, the mere thought of the overhead incurred by RTTI is enough to make embedded devs shriek in the night.

Type traits are often used to accomplish tasks for which reflection is usually used in other languages, and a common non-RTTI solution for type comparisons is given below. int and char are considered the same if is_same<int,char>::result == true. In the general case, result is statically initialised to false but the compiler chooses the template specialisation (which initialises result to true) when T is equal to U. Note that this of course will not work for polymorphic types.

template<typename T, typename U>
struct is_same       { static bool const result = false; };

template<typename T>
struct is_same<T, T> { static bool const result = true;  };

SFINAE (Substitution Failure is not an Error)

In the last month or so, I’ve become more interested in template hax and the concept of SFINAE intrigued me. As this article explains, SFINAE revolves around the idea that in some situations, an invalid substitution of template parameters will not result in a compiler error. One of these situations is function overload resolution, as demonstrated in the below example from that article. An f<Test>(10) invocation would be delegated to #1 as Test encapsulates a NestedType typedef alias, and so the substitution is valid. When matching an f<int>(10) invocation, the compiler would silently remove #1 from the set of candidate overloads as int::NestedType is invalid, and finally delegate to #2.

struct Test { typedef int NestedType; };

template<typename T>
void f(typename T::NestedType) {}        // Definition #1

template<typename T>
void f(T) {}                             // Definition #2

Basic ‘Reflection’ using SFINAE

After seeing SFINAE solutions posted here for determining whether a type governs a function, I decided to experiment with it and roll my own SFINAE type comparison for the hell of it (and to learn something along the way). Note that this solution is far inferior to the standard non-RTTI one for reasons outlined below, but I thought I’d post it anyway as it’s an interesting specimen of code.

template<class S, class T>
struct SameClass
{
    template<class Identical, Identical>
    struct type_match;                    

    template<class _1>
    static char (& func(
         type_match< T& (_1::*)(const _1&), &_1::operator => *) )[1];

    template<class >
    static char (& func(...))[2];

    static bool const value = sizeof(func<S>(0)) == 1;
};

template<class c1, class c2>
bool IsSameClassAs() { return SameClass<c1,c2>::value == 1; }

func is a templated function with two overloads. The first definition takes a pointer to a type_match struct and returns a char[1] array by reference (for which sizeof is 1). The second takes any parameter(s) and returns a char[2] array by reference. SameClass<S,T>::value will equal 1 if func<S>(0) delegates to the first overload. This can only happen if both of the type_match template arguments can be successfully substituted as IDENTICAL (implying they must be equivalent). If they cannot, the compiler will silently delegate to the second overload in SFINAE fashion. The first argument is a member function pointer to a method in S which takes a const S& parameter and returns T&. The second is a member function pointer to an operator= method defined in S.

Essentially, for the first overload to be chosen (and S and T to be considered equal), S must have an operator= method defined which takes const S& and returns T&. If S and T are indeed the same types, S is guaranteed to have a copy assignment operator which satisfies this criteria (whether it is explicitly implemented or not). However, a false positive will result if S offers an op= method which returns T& – I’m not sure why anyone would implement such a method, but it is permitted by MSVC. Furthermore, this implementation will not work for primitive types or pointer types as the scope operator is applicable to neither. I must reiterate that this is not a sensible solution for reflection when there are far simpler and more robust solutions available. But interesting nonetheless.

This entry was posted in C++ and tagged , , , , , , . Bookmark the permalink.

4 Responses to Adventures with Templates – Basic C++ Type Introspection without RTTI

Leave a Reply

Your email address will not be published. Required fields are marked *