Develop your own RTTI in C++
This article will show the reader how to implement an own simple RTTI alternative in standard C++.
Introduction
This article will show how to implement an alternative to the built-in RTTI
mechanism with just standard C++.
The presented implementation has not the pretense to be a complete replacement for the C++ RTTI
system.
Rather it should improve certain issues which can appear when using RTTI
.
For instance, there are problems when using RTTI
across shared boundaries.
Furthermore the alternative should be more performant, in terms of execution speed, than the standard RTTI
.
The first part will show how to implement the typeid
operator and a type_info
class.
In the second part an alternative to the dynamic_cast
operator will be presented.
Unnecessary implementation details are left out, only the general concept and the important parts are shown in detail.
RTTI
RTTI
stands for Run-Time Type Information. It is used to retrieve information about an object at runtime (as opposed to compile time).
In non-polymorphic languages there is no need for this information, because the type is always known at compile time and so in runtime.
In a polymorphic language like C++, there are situations where the concrete type is not known at compile time.
A pointer can point to an object of a base class type or to an object of any class type derived from the base class
Therefore RTTI
helps to determine the concrete type at runtime.
Normally RTTI
is an optional compiler setting, which is usually enabled.
When enabled, RTTI
is automatically generated for both built-in and non-polymorphic user-defined types.
It is then not possible to disable/enable RTTI
just for certain types selectively.
In C++ the RTTI
mechanism consists of:
typeid
operatortype_info
classdynamic_cast
operator
typeid and type_info
The typeid
operator will be used to determine the dynamic type of an object at runtime.
Therefore the typeid
operator returns a constant reference to a type_info
object.
The programmer can then compare this object with other ones or retrieve the name.
When the typeid
operator should work with polymorphic types, at least one virtual function has to be declared in the class.
The reason for that is, the pointer to the type_info
object is stored in the virtual table of the class.
So RTTI
will not increase the data per object, it will increase the data per class.
It is not specified in the standard how big this metadata is.
In some special situation the typeid operator doesn't work, more precisely there are problems when used across shared boundaries.
In C++ the concept of shared or dynamic libraries is not defined, so it is not a problem with the standard.
Nevertheless, there exist a workaround for this problem:
Using the type_info::name
function for comparison instead of the built in compare operator will fix this issue, but this is of course slower.
Another drawback is that the type_info
objects are not copyable. That means it is not possible to use them directly in a container.
Creating a wrapper class around type_info
object will fix this issue.
Custom type_info
One of the standard approaches for implementing typeid
is the following snippet:
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. | static int generateId() { static int typeID; // automatically init with 0 return ++typeID; } template <typename T> struct MetaTypeInfo { static int getTypeInfo() { static const int typeID = generateId(); return typeID; }; }; |
For every type T that will instantiate the struct template MetaTypeInfo, it will return a unique integer from the function getTypeInfo
.
Unfortunately this code will not work correctly across dynamic loaded libraries (DLL); it will return different integer values for the same type. In windows every DLL gets its own copy of local and global static variables.
To work around this problem, a register functionality is needed, which uses the windows dllexport/dllimport attribute and returns for every type T
always the same typeID.
The presented solution in this article will use a unique string literal which correspond to the type T
.
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. | template <typename T> struct MetaTypeInfo { static TypeInfo getTypeInfo() { // when you get this error, // you have to declare first the type with this macro return T::TYPE_NOT_REGISTERED__USE_RTTR_DECLARE_META_TYPE(); } }; // returns for every name a unique TypeInfo object RTTR_EXPORT TypeInfo registerOrGetType(const char* name); #define RTTR_DECLARE_META_TYPE(T) \ template<> \ struct MetaTypeInfo< T > \ { \ static RTTR::TypeInfo getTypeInfo() \ { \ static const TypeInfo val = registerOrGetType(#T); \ return val; \ } \ }; // use it like this: RTTR_DECLARE_META_TYPE(int) RTTR_DECLARE_META_TYPE(bool) //... |
The integer value was replaced with the class TypeInfo
, which holds the integer (ID) value.
This class is the replacement for the type_info
class.
The template specialization for every supported type is required to register the given type T
with its unique string literal,
which will be extracted with the macro expression #T
.
The register function registerOrGetType
maps the given string literal to a unique ID.
When the key not yet exist in the map it will be stored together with the ID, otherwise the already existing ID will be returned.
The Macro RTTR_EXPORT
hides the dllexport/dllimport functionality required on windows.
The default implementation of MetaTypeInfo<T>::getTypeInfo()
will now generate a compile time error,
when a TypeInfo object should be retrieved for a type which has not be specialized.
To avoid unnecessary rewrite work for every type it will be put inside the macro RTTR_DECLARE_META_TYPE
.
This code has a serious issue; it is not thread-safe. There is a race condition during the initialization of the local static variable val
.
When one thread is executing the registerOrGetType
function, another thread could already return the uninitialized value of val
.
There are several solutions to make the code thread-safe, one is to use the static initialization time, which is thread-safe.
Therefore the following implementation uses a little helper class which automatically register a type before main is called.
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. | #define RTTR_CAT_IMPL(a,b) a##b #define RTTR_CAT(a,b) RTTR_CAT_IMPL(a,b) #define RTTR_DEFINE_META_TYPE(T) \ template<> \ struct AutoRegisterType<T> \ AutoRegisterType() \ { \ MetaTypeInfo<T>::getTypeInfo(); \ } \ }; \ static const AutoRegisterType<T> RTTR_CAT(autoRegisterType,__COUNTER__); // __COUNTER__ gets incremented by 1 every time it is used in a source file // use it like this in a cpp files: RTTR_DEFINE_META_TYPE(int) RTTR_DEFINE_META_TYPE(bool) |
In general global variables should be avoided where possible, but in this particular case it is very handy to avoid any kind of locking. Furthermore the code which will be executed is very trivial and should not lead to any problems.
The TypeInfo
class, which is the wrapper for the ID, provides also the interface for retrieving the TypeInfo's
itself.
They are declared as public member functions.
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. | class RTTR_EXPORT TypeInfo { public: typedef unsigned short TypeId; // to use TypeInfo in variause std container // following functions are definied: TypeInfo(const TypeInfo& other); TypeInfo& operator=(const TypeInfo& other); bool operator<(const TypeInfo& other) const; bool operator>(const TypeInfo& other) const; bool operator>=(const TypeInfo& other) const; bool operator<=(const TypeInfo& other) const; bool operator==(const TypeInfo& other) const; bool operator!=(const TypeInfo& other) const; // Via following getter can a TypeInfo object retrieved: template<typename T> static TypeInfo get(); template<typename T> static TypeInfo get(T* object); template<typename T> static TypeInfo get(T& object); //... private: // Constructs an empty and invalid TypeInfo object. TypeInfo(); // private to avoid creation by client TypeInfo(TypeId id); // this function can only create valid TypeInfo objects friend TypeInfo impl::registerOrGetType(const char* name); private: TypeId m_id; }; |
The client retrieves the TypeInfo
object with a call to the static function TypeInfo::get<T>()
when only the type is given or via a call to TypeInfo::get(myInstance)
when an instance is given.
A TypeInfo
object can only be created with a call to these getters, that's why the constructors of TypeInfo are private.
These getters are implemented like defined in the standard (5.2.8.6):
If the type of the expression or type-id is a cv-qualified type, the result of the typeid expression refers to a std::type_info object representing the cv-unqualified type.
Following example code demonstrate this behaviour:
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. | int intVar = 42; const int constIntVar = 42; TypeInfo::get(intVar) == TypeInfo::get(constIntVar); // true TypeInfo::get<int>() == TypeInfo::get(constIntVar); // true TypeInfo::get<int>() == TypeInfo::get<const int>(); // true TypeInfo::get<int>() == TypeInfo::get<const int &>(); // true TypeInfo::get<int*>() == TypeInfo::get(&intVar); // true TypeInfo::get<int*>() == TypeInfo::get<int *const>(); // true TypeInfo::get<const int*>() == TypeInfo::get(&constIntVar); // true TypeInfo::get<const int*>() == TypeInfo::get<const int *const>(); // true const int& intConstRef = intVar; TypeInfo::get<int>() == TypeInfo::get(intConstRef); // true |
With the typeid
operator it is possible to retrieve the type_info
of the most derived type, although only a reference to a parent class is given.
This is defined in following requirement (5.2.8.3):
When typeid is applied to a glvalue expression whose type is a polymorphic class type (10.3), the result refers to a std::type_info object representing the type of the most derived object (1.8) (that is, the dynamic type) to which the glvalue refers.
In order to retrieve this information, the given class type must be polymorphic and so at least one virtual function must be defined.
The reason for that is, the compiler will add an additional pointer (virtual pointer, vptr) in the virtual table (vtable) to the corresponding type_info
object.
Although the standard does not mention anything about virtual tables, it was intended to be implemented RTTI
in the same way as virtual function are implemented.
With the help of virtual functions, it is possible to recreate this feature.
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. | template<typename T> static TypeInfo getTypeInfoFromInstance(const T*) { return MetaTypeInfo<T>::getTypeInfo(); } struct Base { virtual TypeInfo getTypeInfo() const { return getTypeInfoFromInstance(this); } }; struct Derived : Base { virtual TypeInfo getTypeInfo() const { return getTypeInfoFromInstance(this); } }; Derived d; Base b = d; TypeInfo::get<D>() == TypeInfo::get(b); // true TypeInfo::get<D>() == TypeInfo::get(&b); // false |
The expression TypeInfo::get(b);
looks at compile time with the help of SFINAE
whether the given object has a member function TypeInfo T::getTypeInfo() const
declared or not.
When there is such a function with this name declared it will be called.
Then the most derived member function implementation will be executed.
Where finally the concrete type T
can be retrieved.
When no function is defined the TypeInfo
of the current type will be returned.
The virtual function is put again into a macro (RTTR_ENABLE
) to avoid unnecessary rewrite work.
1. 2. 3. 4. 5. 6. 7. | #define RTTR_ENABLE() \ public:\ virtual TypeInfo getTypeInfo() const {return getTypeInfoFromInstance(this);} \ private: struct Base { RTTR_ENABLE() }; struct Derived : Base { RTTR_ENABLE() }; |
Like defined in the standard, only when TypeInfo::get()
is called with a reference type, then the returned TypeInfo
represents the concrete derived class type.
When it is required to check against a target type, which is in the middle of the class hierarchy it won't work.
Therefore, the use of dynamic_cast
is the right tool.
dynamic_cast
The dynamic_cast
operator is used to cast pointers or references to a target type using RTTI
.
Thus, the class itself must be polymorphic to allow the usage of dynamic_cast
.
When the types are incompatible a null pointer will be returned (when using pointers) or an exception will be thrown (when using references).
The client can perform three kinds of casts with dynamic_cast
: upcasts, downcasts and crosscasts.
The upcast is the operation of converting a derived class pointer or reference to its base (or parent) class.
A downcast is the opposite, casting from a base class to a derived class.
A crosscast means casting from one class hierarchy level to another.
The involved classes has thereby only in common that they share the same derived class, they must not be related through inheritance.
In order to perform a cross cast the dynamic_cast
operator needs to adjust the current base vptr to the vptr of the target type.
This is done through offset calculations.
The offsets to every other vptr in the class hierarchy is also stored in the vtable.
The virtual table is not described by the standard, so it is not possible to obtain these offsets in a portable way.
That is reason why this alternative RTTI
does not support crosscast's or virtual inheritance. Therefore, dynamic_cast
is still necessary.
Custom dynamic_cast
Supporting upcasts and downcasts (without virtual inheritance), requires that the complete inheritance graph (DAG) needs to be stored. Retrieving this information automatically is not possible with C++. The client has to register these kinds of meta information by himself.
For storing this meta information a loki like type list was used. The client will again only use macros, so everything is hidden.
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. | struct nil {}; template<class T, class U = nil> struct typelist { typedef T head; typedef U tail; }; #define TYPE_LIST() typelist<nil> #define TYPE_LIST_1(A) typelist<A, TYPE_LIST() > #define TYPE_LIST_2(A,B) typelist<A, TYPE_LIST_1(B) > //... #define RTTR_ENABLE_DERIVED_FROM(A) \ public:\ virtual TypeInfo getTypeInfo() const {return getTypeInfoFromInstance(this);} \ typedef TYPE_LIST_1(A) baseClassList;\ private: #define RTTR_ENABLE_DERIVED_FROM_2(A,B) \ public:\ virtual TypeInfo getTypeInfo() const {return getTypeInfoFromInstance(this);} \ typedef TYPE_LIST_2(A,B) baseClassList;\ private: // Example usage struct Base1 { RTTR_ENABLE() }; struct Base2 { RTTR_ENABLE() }; struct Multiple : Base1, Base2 { RTTR_ENABLE_DERIVED_FROM_2(Base1, Base2) }; |
Adding this meta information inside the class itself, will not increase the size of the class. The register process is also less error prone because the client can directly see from what class he derives.
The list of the base classes needs to be converted to its corresponding TypeInfo
objects.
This is done with following code:
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. | template<class> struct TypeInfoFromBaseClassList; /*! * This class fills from a given typelist the, * corresponding TypeInfo objects into a std::vector. */ template<> struct TypeInfoFromBaseClassList<typelist<nil> > { static void fill(std::vector<TypeInfo>&) { } }; template<class T, class U> struct TypeInfoFromBaseClassList<typelist<T, U> > { static void fill(std::vector<TypeInfo>& v) { v.push_back(MetaTypeInfo<T>::getTypeInfo()); TypeInfoFromBaseClassList<typename T::baseClassList>::fill(v); TypeInfoFromBaseClassList<U>::fill(v); } }; /*! * This helper trait returns a vector with TypeInfo object of all base classes. * When there is no typelist named baseClassList defined, * an empty vector is returned. */ template<class T> struct BaseClasses { private: // extract the info static void retrieve_impl(std::vector<TypeInfo>& v, Traits::true_type) { TypeInfoFromBaseClassList<typename T::baseClassList>::fill(v); } // no type list defined static void retrieve_impl(std::vector<TypeInfo>& v, Traits::false_type) { } public: static std::vector<TypeInfo> retrieve() { std::vector<TypeInfo> result; // check with SFINAE whether a typedef for baseClassList is defined or not retrieve_impl(result, typename has_base_class_list<T>::type()); return result; } }; |
The BaseClasses<T>::retrieve()
function returns from a given T
a std::vector<TypeInfo>
,
which contains for every item in the typelist the corresponding TypeInfo
.
The recursive function TypeInfoFromBaseClassList<T>::fill(std::vector<TypeInfo>&)
iterates through the typelist and fills the given
vector with its corresponding TypeInfo
.
The register function is adjusted to store TypeInfos
for every type and its base classes, thus the function MetaTypeInfo<T>::getTypeInfo()
is adjusted in the following way:
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. | #define RTTR_DECLARE_META_TYPE(T) \ template<> \ struct MetaTypeInfo< T > \ { \ static RTTR::TypeInfo getTypeInfo() \ { \ static const TypeInfo val = registerOrGetType(#T, RawTypeInfo<T>::get(), \ BaseClasses<T>::retrieve());\ return val; \ } \ }; |
The RawTypeInfo<T>::get()
returns for every given type T
the TypeInfo
of its raw type.
A raw type is a type without any cv-qualifier nor any pointer or reference.
This raw type is necessary because it is used for the mapping to the base class meta information.
Without this raw type every type (e.g. a pointer type) would need such a mapping.
rttr_cast
The custom cast operator is called rttr_cast
, its implementation is straightforward:
It uses a public member function TypeInfo::isTypeDerivedFrom
which returns true if the given target type is derived from the operand type, otherwise false.
For the cast itself the implementation makes usage of static_cast
, because when multiple inheritance is involved the vptr needs to be adjusted.
The static_cast
operator does this automatically at compile time, reinterpret_cast
does not perform any vptr adjustment.
Besides the already mentioned not supported crosscasts and virtual inheritance, the other difference to dynamic_cast
is that only pointer types can be cast.
The reason for that is, exceptions are not supported.
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. | template<typename T, typename Arg> T rttr_cast(Arg object) { using namespace RTTR::Traits; RTTR_STATIC_ASSERT(is_pointer<T>::value, RETURN_TYPE_MUST_BE_A_POINTER); RTTR_STATIC_ASSERT(is_pointer<Arg>::value, ARGUMENT_TYPE_MUST_BE_A_POINTER); RTTR_STATIC_ASSERT(RTTR::impl::has_getTypeInfo_func<Arg>::value, CLASS_HAS_NO_TYPEINFO_DEFINIED__USE_MACRO_ENABLE_RTTI); typedef typename remove_pointer<T>::type ReturnType; typedef typename remove_pointer<Arg>::type ArgType; RTTR_STATIC_ASSERT((is_const<ArgType>::value && is_const<ReturnType>::value)|| (!is_const<ArgType>::value && is_const<ReturnType>::value)|| (!is_const<ArgType>::value && !is_const<ReturnType>::value), RETURN_TYPE_MUST_HAVE_CONST_QUALIFIER) if (object && object->getTypeInfo().template isTypeDerivedFrom<T>()) return static_cast<T>(object); else return NULL; } |
Benchmark results
The following bar chart shows the benchmark results of rttr_cast
versus dynamic_cast
.
Every benchmark was running 5 times and then the average was taken.
On the same machine it was tested on two different operating systems and compilers.
- Test System: Intel Core 2 Duo @ 2.40 Ghz, 3 GB RAM
- OS: WinXP 32 bit SP3 / Ubuntu 12.04 32 bit
- Compiler: Visual Studio 2008 SP1 / g++ 4.6.3
Operation | Time |
---|---|
Base to Level 1 [dynamic_cast, WIN] | 48ms |
Base to Level 1 [dynamic_cast, LINUX] | 37ms |
Base to Level 1 [rttr_cast, WIN] | 17ms |
Base to Level 1 [rttr_cast, LINUX] | 14ms |
Base to Level 3 [dynamic_cast, WIN] | 81ms |
Base to Level 3 [dynamic_cast, LINUX] | 77ms |
Base to Level 3 [rttr_cast, WIN] | 26ms |
Base to Level 3 [rttr_cast, LINUX] | 22ms |
Base to Level 6 [dynamic_cast, WIN] | 132ms |
Base to Level 6 [dynamic_cast, LINUX] | 122ms |
Base to Level 6 [rttr_cast, WIN] | 29ms |
Base to Level 6 [rttr_cast, LINUX] | 27ms |
MultiBase to Level 4 [dynamic_cast, WIN] | 51ms |
MultiBase to Level 4 [dynamic_cast, LINUX] | 34ms |
MultiBase to Level 4 [rttr_cast, WIN] | 7ms |
MultiBase to Level 4 [rttr_cast, LINUX] | 7ms |
MultiBase to Level 7 [dynamic_cast, WIN] | 31ms |
MultiBase to Level 7 [dynamic_cast, LINUX] | 28ms |
MultiBase to Level 7 [rttr_cast, WIN] | 15ms |
MultiBase to Level 7 [rttr_cast, LINUX] | 11ms |
Base to Level 6 [typeid, WIN] | 37ms |
Base to Level 6 [typeid, LINUX] | 30ms |
Base to Level 6 [TypeInfo::get, WIN] | 17ms |
Base to Level 6 [TypeInfo::get, LINUX] | 8s |
Conclusion
Altough the comparison of rttr_cast
and dynamic_cast
is not 100% fair, because the custom implemented operator does not implement all operation which dynamic_cast
support, it is clearly visible that rttr_cast
outperforms the built-in operator.
Especially as deeper as the hierachy goes rttr_cast
can show his strength. In one case rttr_cast
does the cast nearly 5 times faster.
Advantage over RTTI
- faster then then the built-in
RTTI
(especially with deep hierachies) - possible to activate the
RTTI
mechanism only on certain types - no hacks needed when used across shared libraries
- the
TypeInfo
objects are ready to used in standard container (no wrapper objects needed)
Disadvantage over RTTI
- necessary to register the types manually
- when used in polymorphic classes, a macro needs to be placed inside the class
- linking an additonal library (no header only)
- no support for crosscasts and virtual inheritance (this may change)