Simple runtime reflection in C++
While I was working on my game engine, I started learning about scripting and scripting systems, and I thought it would be nice to add some scripting capabilities to it. Being able to create objects or to call engine functions from scripts, to separate game logic from the engine innards, to make modification to the game without the need to hardcode stuff and recompile everything seemed like nice features to have. Besides, all game engines worthy of the name allow the use of a scripting language. I picked Lua as a scripting language/environment, since it’s fairly popular in game development, it’s lightweight and I had already used it in the past. Soon into the process of binding my engine code to Lua I realized that needed to get informations about the properties of objects on the engine side of my code (which data members they have, which member functions, constructors and so on and so forth), and, msot importantly, that I needed that informations at runtime. Say, when the game loads a level it runs a script that has a list of entities (and their components) to be spawned: I soon ran into the problem of creating an object by knowing only it’s name from a string in the script. After a little (I have to admit, rather unsuccesful) thinkering, I decided to code a little reflection library myself.
Reflection and introspection, a program reasoning about itself
A more appropriate title for this article should be “Simple runtime introspection in C++”. The terms reflection and introspection are used somewhat interchangeably, even if their meanings are not quite the same. Let’s clear things up first by providing some definitions: in programming jargon, introspection is the ability of a program to find out information about its structure at runtime; reflection takes this a step further by enabling the structure of a program to be modified at runtime. In order to do this, the program must have a representation of itself (its structure and its objects) available during its execution. This kind of information is called metadata. The C++ language supports the OOP paradigm, so it’s a natural choice for metadata to be represented as objects, called metaobjects. The word meta derives from the Greek μετα-, meaning “after” or “beyond”. It is data that provides information about other data, in other words, it is data about data.
In this article I’ll use the term reflection to indicate both reflection and introspection, safe in the knowledge that we’ve agreed upon what is what.
Reflecting a Type (saving type information)
When we write a C++ program, type information is embedded inside the source code, and the compiler uses those informations to perform type checking, but when the compiler is done with our source files all those informations about types are gone, erased from the executable, never to be seen again. As of now, C++ doesn’t provide language support for reflection as other languages do (for example Java, C# or dynamic languages like Python). As we said, if we want to retrieve that information during program execution we need to store it into some metaobjects containing the necessary metadata, before that information is removed from the program source.
When we store information about a type inside another object we say that the type has been reflected: a type becomes a runtime object, and the properties of that type become members of the corresponding object .It is said that the type and its properties, which are abstract language concepts are reified, and thus can be manipulated during the program execution. Once the type has been exposed to the reflection system we can attach other metadata about that type to its reflected counterpart, in the form of metaobjects. Each type has its own runtime representation in the form of a unique TypeDescriptor object. The TypeDescriptor acts as a container of metadata/metaobjects:
class TypeDescriptor { template <typename> friend class TypeFactory; // friend template for TypeFactory (see below) public: template <typename Type, typename... Args> void AddConstructor() { Constructor *constructor = new ConstructorImpl<Type, Args...>(); mConstructors.push_back(constructor); } template <typename B, typename T> void AddBase() { Base *base = new BaseImpl<B, T>; mBases.push_back(base); } template <typename C, typename T> void AddDataMember(T C::*dataMemPtr, const std::string &name) { DataMember *dataMember = new RawDataMember<C, T>(dataMemPtr, name); mDataMembers.push_back(dataMember); } template <auto Setter, auto Getter, typename Type> void AddDataMember(const std::string &name) { DataMember *dataMember = new SetGetDataMember<Setter, Getter, Type>(name); mDataMembers.push_back(dataMember); } template <typename Ret, typename... Args> void AddMemberFunction(Ret freeFun(Args...), const std::string &name) { MemberFunction *memberFunction = new FreeFunction<Ret, Args...>(freeFun, name); mMemberFunctions.push_back(memberFunction); } template <typename C, typename Ret, typename... Args> void AddMemberFunction(Ret(C::*memFun)(Args...), const std::string &name) { MemberFunction *memberFunction = new MemberFunctionImpl<C, Ret, Args...>(memFun, name); mMemberFunctions.push_back(memberFunction); } template <typename C, typename Ret, typename... Args> void AddMemberFunction(Ret(C::*memFun)(Args...) const, const std::string &name) { MemberFunction *memberFunction = new ConstMemberFunctionImpl<C, Ret, Args...>(memFun, name); mMemberFunctions.push_back(memberFunction); } template <typename From, typename To> void AddConversion() { Conversion *conversion = new ConversionImpl<From, To>; mConversions.push_back(conversion); } std::string const &GetName() const { return mName; } std::vector<Constructor*> GetConstructors() const { return mConstructors; } template <typename... Args> const Constructor *GetConstructor() const { for (auto *constructor : mConstructors) if (constructor->CanConstruct<Args...>(std::index_sequence_for<Args...>())) return constructor; return nullptr; } std::vector<Base*> GetBases() const { return mBases; } template <typename B> Base *GetBase() const { for (auto base : mBases) if (base->GetType() == Resolve<B>) return base; return nullptr; } std::vector<DataMember*> GetDataMembers() const { std::vector<DataMember*> dataMembers(mDataMembers); for (auto *base : mBases) for (auto dataMember : base->GetType()->GetDataMembers()) dataMembers.push_back(dataMember); return dataMembers; } DataMember *GetDataMember(const std::string &name) const { for (auto *dataMember : mDataMembers) if (dataMember->GetName() == name) return dataMember; for (auto *base : mBases) if (auto *baseDataMember = base->GetType()->GetDataMember(name)) return baseDataMember; return nullptr; } std::vector<MemberFunction*> GetMemberFunctions() const { std::vector<MemberFunction*> memberFunctions(mMemberFunctions); for (auto *base : mBases) for (auto memberFunction : base->GetType()->GetMemberFunctions()) memberFunctions.push_back(memberFunction); return memberFunctions; } const MemberFunction *GetMemberFunction(const std::string &name) const { for (auto *memberFunction : mMemberFunctions) if (memberFunction->GetName() == name) return memberFunction; for (auto *base : mBases) if (auto *memberFunction = base->GetType()->GetMemberFunction(name)) return memberFunction; return nullptr; } std::vector<Conversion*> GetConversions() const { return mConversions; } template <typename To> Conversion *GetConversion() const { for (auto conversion : mConversions) if (conversion->GetToType() == Resolve<To>()) return conversion; return nullptr; } private: std::string mName; std::vector<Base*> mBases; std::vector<Conversion*> mConversions; std::vector<Constructor*> mConstructors; std::vector<DataMember*> mDataMembers; std::vector<MemberFunction*> mMemberFunctions; };
A TypeDescriptor has functions to add metadata as well as member functions to retrieve those metadata. When a type is reflected, all its constructors, data members, member functions, base classes and type conversion operators can be attached to it. Free functions can be attached as well, which will come in handy when the reflection system is used to serialize objects, or to create glue code between the application and a scripting system.
It all starts by reflecting a type, giving a name to the reflected type:
// variable template (one object of type TypeFactory for each reflected object) template <typename Type> TypeFactory<Type> typeFactory; template <typename Type> TypeFactory<Type> &Reflect(const std::string &name) { return typeFactory<Type>.ReflectType(name); }
The Reflect function calls TypeFactory::ReflectType() and returns an object of type TypeFactory<Type>, which is a class template parameterized by the type to be reflected. The returned typeFactory object is a variable template, so exactly one Typefactory exists for each reflected type. Each TypeFactory is, unsurprisingly, a static factory and contains static member functions that are used to reflect the type as well as to attach metadata to the reflected type:
template <typename Type> class TypeFactory { public: static TypeFactory &ReflectType(const std::string &name) { TypeDescriptor *typeDescriptor = Details::Resolve<Type>(); typeDescriptor->mName = name; Details::mTypeRegistry[name] = typeDescriptor; return typeFactory<Type>; } template <typename... Args> static TypeFactory &AddConstructor() { Details::Resolve<Type>()->AddConstructor<Type, Args...>(); return typeFactory<Type>; } template <typename Base> static TypeFactory &AddBase() { static_assert(std::is_base_of<Base, Type>::value); // Base must be a base of Type Details::Resolve<Type>()->AddBase<Base, Type>(); return typeFactory<Type>; } template <typename T, typename U = Type> // default template type param to allow for non class types static TypeFactory &AddDataMember(T U::*dataMemPtr, const std::string &name) { Details::Resolve<Type>()->AddDataMember(dataMemPtr, name); return typeFactory<Type>; } template <auto Setter, auto Getter> static TypeFactory &AddDataMember(const std::string &name) { Details::Resolve<Type>()->AddDataMember<Setter, Getter, Type>(name); return typeFactory<Type>; } template <typename Ret, typename... Args> static TypeFactory &AddMemberFunction(Ret(*freeFun)(Args...), const std::string &name) { Details::Resolve<Type>()->AddMemberFunction(freeFun, name); return typeFactory<Type>; } template <typename Ret, typename... Args, typename U = Type> static TypeFactory &AddMemberFunction(Ret(U::*memFun)(Args...), const std::string &name) { Details::Resolve<Type>()->AddMemberFunction(memFun, name); return typeFactory<Type>; } template <typename Ret, typename... Args, typename U = Type> static TypeFactory &AddMemberFunction(Ret(U::*constMemFun)(Args...) const, const std::string &name) { Details::Resolve<Type>()->AddMemberFunction(constMemFun, name); return typeFactory<Type>; } template <typename To> static TypeFactory &AddConversion() { static_assert(std::is_convertible_v<Type, To>); // a conversion Type -> To must exist Details::Resolve<Type>()->AddConversion<Type, To>(); return typeFactory<Type>; } };
Each static member function returns the TypeFactory object so many calls can be concatenated when we reflect a type and add type informations to it, as in the named parameter idiom. A TypeDescriptor associated with a type can be retrieved by calling the Resolve function (each type or function in the reflection system lives in the namespace Reflect). This function is overloaded to accept the name of the reflected type as a string argument, an instance of the reflected type, or the name of the type as a template type argument:
template <typename Type> TypeDescriptor typeDescriptor; template <typename Type> TypeDescriptor *typeDescriptorPtr = nullptr; extern std::map<std::string, TypeDescriptor*> mTypeRegistry; template <typename Type> TypeDescriptor *Resolve() { if (!typeDescriptorPtr<Type>) { typeDescriptorPtr<Type> = &typeDescriptor<Type>; } return typeDescriptorPtr<Type>; } inline TypeDescriptor *Resolve(const std::string &name) { if (auto it = mTypeRegistry.find(name); it != mTypeRegistry.end()) return it->second; return nullptr; } template <typename Type> TypeDescriptor *Resolve(Type &&object) { using DecayedType = typename std::decay<Type>::type; if (!typeDescriptorPtr<DecayedType>) { typeDescriptorPtr<DecayedType> = &typeDescriptor<DecayedType>; } return typeDescriptorPtr<DecayedType>; }
Any as in “any kind of object”
Any is a type whose objects can contain any kind of objects, storing a copy of the object by default. The any object uses small buffer optimization (SBO) to limit allocations, and it is responsible for the management of any associated resource. A non-managing version of any is the Handle class, which doesn’t permorm copies and is just a reference wrapper around the object. An Any can be constructed from an Handle, so the resulting any acts like a reference to the object (an Handle can be constructed from an Any as well). Any stores a type-erased object in a void*, but has a TypeDescriptor as well, and manages conversions and casts from the type of the stored object to any other type, using the type information inside the metadata of the reflection system (full implementation on GitHub):
class Handle { template <std::size_t> friend class Any; public: Handle() : mInstance(nullptr), mType(nullptr) {} template <typename T, typename T_ = std::remove_cv_t<T>, typename = std::enable_if_t<!std::is_same_v<T_, Handle>>> Handle(T &object) : mInstance(&object), mType(Resolve<T_>()) {} template <std::size_t SIZE> Handle(Any<SIZE> &any) : mInstance(any.mInstance), mType(any.mType) {} private: void *mInstance; TypeDescriptor const *mType; }; template <std::size_t SIZE> void swap(Any<SIZE> &any1, Any<SIZE> &any2) { any1.Swap(any2); } template <std::size_t SIZE> class Any { friend class Handle; public: Any(); template <typename T, typename T_ = std::decay_t<T>, typename = typename std::enable_if<!std::is_same_v<T_, Any>>::type> Any(T &&object); Any(const Any &other); Any(Any &&other); Any(Handle handle); ~Any(); template <typename T, typename T_ = typename std::remove_cv<typename std::remove_const<T>::type>::type, typename = typename std::enable_if<!std::is_same<T_, Any>::value>::type> Any &operator=(T &&object); Any &operator=(const Any &other); Any &operator=(Any &&other); void Swap(Any &other); explicit operator bool() const { return Get() != nullptr; } const TypeDescriptor *GetType() const; const void *Get() const; void *Get(); template <typename T> const T *TryCast() const; template <typename T> T *TryCast(); template <typename T> Any TryConvert() const; private: void *mInstance; Details::AlignedStorageT<SIZE> mStorage; const TypeDescriptor *mType; typedef void *(*CopyFun)(void*, const void*); typedef void *(*MoveFun)(void*, void*); typedef void (*DestroyFun)(void*); CopyFun mCopy; MoveFun mMove; DestroyFun mDestroy; template <typename T, typename = std::void_t<> /* void */> struct TypeTraits { template <typename... Args> static void *New(void *storage, Args&&... args) { T *instance = new T(std::forward<Args>(args)...); new(storage) T*(instance); return instance; } static void *Copy(void *to, const void *from) { T *instance = new T(*static_cast<const T*>(from)); new(to) T*(instance); return instance; } static void *Move(void *to, void *from) { T *instance = static_cast<T*>(from); new(to) T*(instance); return instance; } static void Destroy(void *instance) { delete static_cast<T*>(instance); } }; template <typename T> struct TypeTraits<T, typename std::enable_if<sizeof(T) <= SIZE>::type> { template <typename... Args> static void *New(void *storage, Args&&... args) { new(storage) T(std::forward<Args>(args)...); return storage; } static void *Copy(void *to, const void *from) { new(to) T(*static_cast<const T*>(from)); return to; } static void *Move(void *to, void *from) { T &instance = *static_cast<T*>(from); new(to) T(std::move(instance)); instance.~T(); return to; } static void Destroy(void *instance) { static_cast<T*>(instance)->~T(); } }; };
Any objects are used to pass arguments to Constructor and MembeFunction metaobjects, and are returned from those same metaobjects when Invoked.
Constructors
The constructor metaobject abstracts the real constructor of a reflected type and it’s invoked to create a new instance of that type. The Constructor base class exposes an interface to create a new instance of a type and to query the type of the constructed object as well as the type of the constructor arguments. The ConstructorImpl class template derives from Contructor and is parameterized by the type of the object to construct and the constructor arguments, and implements the NewInstance method, which returns an Any containing the new object:
class Constructor { public: any NewInstance(std::vector<any> &args) { if (args.size() == mParamTypes.size()) return NewInstanceImpl(args); return any(); } template <typename... Args> any NewInstance(Args&&... args) const { if (sizeof...(Args) == mParamTypes.size()) { auto argsAny = std::vector<any>({ any(std::forward<Args>(args))... }); return NewInstanceImpl(argsAny); } return any(); } TypeDescriptor const *GetParent() const { return mParent; } TypeDescriptor const *GetParamType(size_t index) const { return mParamTypes[index]; } size_t GetNumParams() const { return mParamTypes.size(); } template <typename... Args, size_t... indices> bool CanConstruct(std::index_sequence<indices...> indexSequence = std::index_sequence_for<Args...>()) const { return GetNumParams() == sizeof...(Args) && ((Reflect::CanCastOrConvert(Details::Resolve<Args>(), GetParamType(indices))) && ...); } protected: Constructor(TypeDescriptor *parent, const std::vector<const TypeDescriptor*> ¶mTypes) : mParent(parent), mParamTypes(paramTypes) {} private: virtual any NewInstanceImpl(std::vector<any> &args) const = 0; TypeDescriptor *mParent; std::vector<TypeDescriptor const*> mParamTypes; }; template <typename Type, typename... Args> class ConstructorImpl : public Constructor { public: ConstructorImpl() : Constructor(Details::Resolve<Type>(), { Details::Resolve<Args>()... }) {} private: any NewInstanceImpl(std::vector<any> &args) const override { return NewInstanceImpl(args, std::make_index_sequence<sizeof...(Args)>()); } template <size_t... indices> any NewInstanceImpl(std::vector<any> &args, std::index_sequence<indices...> indexSequence) const { std::tuple argsTuple = std::make_tuple(args[indices].TryCast<std::remove_cv_t<std::remove_reference_t<Args>>>()...); std::vector<any> convertedArgs{ (std::get<indices>(argsTuple) ? Handle(*std::get<indices>(argsTuple)) : args[indices].TryConvert<std::remove_cv_t<std::remove_reference_t<Args>>>())... }; argsTuple = std::make_tuple(convertedArgs[indices].TryCast<std::remove_cv_t<std::remove_reference_t<Args>>>()...); if ((std::get<indices>(argsTuple) && ...)) return Type(*std::get<indices>(argsTuple)...); return any(); } };
There are two contructor overloads, one taking an array of Any as parameter, while the other is a templated constructor that takes a variadic list of arguments. The NewInstanceImpl member function checks the validity of the arguments: if the arguments are the same as the parameters of the constructor, or if they can be converted to those types, the function returns an Any containing the new instance, otherwise it returns an empty/invalid Any.
Data members, getters and setters
The DataMember metaobject represent a member variable, or field, of the reflected type. It contains the name of the reflected field and the type descriptors of the type of the field and of the class it belongs to, as well as member functions to retrieve them. Setting and getting the value of an object’s field is a matter of invoking the Set and Get methods of the DataMember metaobject, passing an Handle to an Any object that contains the object whose field we are accessing. The accessor methods Get and Set are pure virtual functions:
class DataMember { public: std::string GetName() const { return mName; } const TypeDescriptor *GetParent() const { return mParent; } const TypeDescriptor *GetType() const { return mType; } virtual void Set(Handle objectHandle, const any value) = 0; virtual any Get(any object) = 0; protected: DataMember(const std::string &name, const TypeDescriptor *type, const TypeDescriptor *parent) : mName(name), mType(type), mParent(parent) {} private: std::string mName; const TypeDescriptor *mType; const TypeDescriptor *mParent; };
Two derived classes implement the accessor methods, depending on how the data member is stored: one class stores the raw pointer to data member and every access uses the provided Any object and the raw pointer to member. The class is a template, parameterized by the type of the data member (Type), and the type of the class it belongs to (Class), so the stored pointer type is Type Class::*. The Set member function uses tag dispatch to detect at compile time if the code is trying to set a const data member, and if so it raises a static assertion:
template <typename Class, typename Type> class RawDataMember : public DataMember { public: RawDataMember(Type Class::*dataMemberPtr, const std::string name) : DataMember(name, Details::Resolve<Type>(), Details::Resolve<Class>()), mDataMemberPtr(dataMemberPtr) {} void Set(Handle objectHandle, const any value) override { SetImpl(objectHandle, value, std::is_const<Type>()); } any Get(any object) override { Class *obj = object.TryCast<Class>(); if (!obj) throw BadCastException(Resolve<Class>()->GetName(), object.GetType()->GetName()); return obj->*mDataMemberPtr; } private: Type Class::*mDataMemberPtr; void SetImpl(any object, const any value, std::false_type) { Class *obj = object.TryCast<Class>(); // pointers to members of base class can be used with derived class Type const *casted = nullptr; if (casted = value.TryCast<Type>(); !casted) { any val = value.TryConvert<Type>(); casted = val.TryCast<Type>(); } if (!obj) throw BadCastException(Details::Resolve<Class>()->GetName(), object.GetType()->GetName(), "object:"); if (!casted) throw BadCastException(Details::Resolve<Type>()->GetName(), value.GetType()->GetName(), "value:"); obj->*mDataMemberPtr = *casted; } void SetImpl(any object, const any value, std::true_type) { static_assert(false, "can't set const data member"); } };
To be able to reflect a data member this way we need access to a public field, but good encapsulation practice madates that data members be declared as private in a class definition. Getter and setter methods are generally used to access the private fields in a properly abstracted data type. A DataMember metaobject can also be created when the type has private member variables that can be accessed through accessor functions. This requires a little bit of C++17 templates, since the getter and setter functions are passed as template arguments to auto deducted template non-type parameters:
// helper meta function to get info about functions passed as auto non type params template <typename> struct FunctionHelper; template <typename Ret, typename... Args> struct FunctionHelper<Ret(Args...)> { using ReturnType = Ret; using ParamsTypes = std::tuple<Args...>; }; template <typename Class, typename Ret, typename... Args> FunctionHelper<Ret(Args...)> ToFunctionHelper(Ret(Class::*)(Args...)); template <typename Class, typename Ret, typename... Args> FunctionHelper<Ret(Args...)> ToFunctionHelper(Ret(Class::*)(Args...) const); template <typename Ret, typename... Args> FunctionHelper<Ret(Args...)> ToFunctionHelper(Ret(*)(Args...)); template <auto Setter, auto Getter, typename Class> class SetGetDataMember : public DataMember { private: using MemberType = typename decltype(ToFunctionHelper(Getter))::ReturnType; public: SetGetDataMember(const std::string name) : DataMember(name, Details::Resolve<MemberType>(), Details::Resolve<Class>()) {} void Set(Handle objectHandle, const any value) override { any a = objectHandle; Class *obj = a.TryCast<Class>(); MemberType const *casted = nullptr; any val; if (casted = value.TryCast<MemberType>(); !casted) { val = value.TryConvert<MemberType>(); casted = val.TryCast<MemberType>(); } if (!obj) throw BadCastException(Resolve<Class>()->GetName(), any(objectHandle).GetType()->GetName(), "object:"); if (!casted) throw BadCastException(Resolve<MemberType>()->GetName(), value.GetType()->GetName(), "value:"); if constexpr (std::is_member_function_pointer_v<decltype(Setter)>) (obj->*Setter)(*casted); else Setter(*obj, *casted); } any Get(any object) override { Class *obj = object.TryCast<Class>(); if (!obj) throw BadCastException(Resolve<Class>()->GetName(), object.GetType()->GetName()); if constexpr (std::is_member_function_pointer_v<decltype(Setter)>) return (obj->*Getter)(); else { static_assert(std::is_function_v<std::remove_pointer_t<decltype(Getter)>>); return Getter(*obj); } } };
In this way we can reflect a type’s fields, wheter they’re public data members or encapsulated, private fields. The DataMember metaobject’s Set and Get methods can be used to access the member, to change or to get its current value.
Member Functions
We can attach member functions or free functions to a reflected type using the MemberFunction metaobject. Member functions can then be invoked on an instance of the object (in case of member functions), or with an empty any object (in case of free functions). Being able to attach free functions to the meta type is very convenient when we use the reflection system to serialize data or to expose types to a scripting system, as we’ll se later on.
class MemberFunction { public: std::string GetName() const { return mName; } const TypeDescriptor *GetParent() const { return mParent; } any Invoke(Handle object, std::vector<any> &args) const { if (args.size() == mParamTypes.size()) return InvokeImpl(object, args); return any(); } template <typename... Args> any Invoke(Handle object, Args&&... args) const { if (sizeof...(Args) == mParamTypes.size()) { std::vector<any> anyArgs{ any(std::forward<Args>(args))... }; return InvokeImpl(object, anyArgs); } return any(); } const TypeDescriptor *GetReturnType() const { return mReturnType; } std::vector<const TypeDescriptor*> GetParamTypes() const { return mParamTypes; } const TypeDescriptor *GetParamType(size_t index) const { return mParamTypes[index]; } std::size_t GetNumParams() const { return mParamTypes.size(); } protected: MemberFunction(const std::string &name, const TypeDescriptor *parent, const TypeDescriptor *returnType, const std::vector<TypeDescriptor const*> paramTypes) : mName(name), mParent(parent), mReturnType(returnType), mParamTypes(paramTypes) {} const TypeDescriptor *mReturnType; std::vector<TypeDescriptor const *> mParamTypes; private: virtual any InvokeImpl(any object, std::vector<any> &args) const = 0; std::string mName; TypeDescriptor const *mParent; };
As with the DataMember class, the base MemberFunction class is an ordinary (non-template) class, that exposes an interface to query the properties of the reflected function (number and type of parameters, return type), and to invoke the function. A bunch of class templates derive from it, one for each type of function that can be reflected: member functions, const member functions and free functions. For each derived class, a partial template specialization is provided for void return type. I’ll show the implementation for a non const member function, the other implementations can be found along with the complete code on the GitHub repo:
template <typename C, typename Ret, typename... Args> class MemberFunctionImpl : public MemberFunction { private: using MemFunPtr = Ret(C::*)(Args...); public: MemberFunctionImpl(MemFunPtr memFun, const std::string &name) : MemberFunction(name, Resolve<C>(), Resolve<Ret>(), { Resolve<std::remove_cv_t<std::remove_reference_t<Args>>>()... }), mMemFunPtr(memFun) {} private: any InvokeImpl(any object, std::vector<any> &args) const override { return InvokeImpl(object, args, std::make_index_sequence<sizeof...(Args)>()); } template <size_t... indices> any InvokeImpl(any object, std::vector<any> &args, std::index_sequence<indices...> indexSequence) const { std::tuple argsTuple = std::make_tuple(args[indices].TryCast<std::remove_cv_t<std::remove_reference_t<Args>>>()...); std::vector<any> convertedArgs{ (std::get<indices>(argsTuple) ? Handle(*std::get<indices>(argsTuple)) : args[indices].TryConvert<std::remove_cv_t<std::remove_reference_t<Args>>>())... }; argsTuple = std::make_tuple(convertedArgs[indices].TryCast<std::remove_cv_t<std::remove_reference_t<Args>>>()...); if (C *obj = object.TryCast<C>(); (std::get<indices>(argsTuple) && ...) && obj) // object is valid and all arguments are valid return (obj->*mMemFunPtr)(*std::get<indices>(argsTuple)...); else return any(); } MemFunPtr mMemFunPtr; }; template <typename C, typename... Args> class MemberFunctionImpl<C, void, Args...> : public MemberFunction { private: using MemFunPtr = void(C::*)(Args...); public: MemberFunctionImpl(MemFunPtr memFun, const std::string &name) : MemberFunction(name, Resolve<C>(), Resolve<void>(), { Resolve<std::remove_cv_t<std::remove_reference_t<Args>>>()... }), mMemFunPtr(memFun) {} private: any InvokeImpl(any object, std::vector<any> &args) const override { return InvokeImpl(object, args, std::make_index_sequence<sizeof...(Args)>()); } template <size_t... indices> any InvokeImpl(any object, std::vector<any> &args, std::index_sequence<indices...> indexSequence) const { std::tuple argsTuple = std::make_tuple(args[indices].TryCast<std::remove_cv_t<std::remove_reference_t<Args>>>()...); std::vector<any> convertedArgs{ (std::get<indices>(argsTuple) ? Handle(*std::get<indices>(argsTuple)) : args[indices].TryConvert<std::remove_cv_t<std::remove_reference_t<Args>>>())... }; argsTuple = std::make_tuple(convertedArgs[indices].TryCast<std::remove_cv_t<std::remove_reference_t<Args>>>()...); if (C *obj = object.TryCast<C>(); (std::get<indices>(argsTuple) && ...) && obj) // object is valid and all arguments are valid (obj->*mMemFunPtr)(*std::get<indices>(argsTuple)...); return any(); } MemFunPtr mMemFunPtr; };
Base classes and conversions
The C++ language defines a bunch of implicit type conversions, and allows user-defined conversions as well (converting constructors, type conversion operators). Derived-to-base conversions are allowed as well, and a pointer or reference to a derived class can be assigned to a pointer or reference to a base class. The Base and Conversion metaobjects allow these conversions between metatypes.
The Base metaobject has a Cast method that performs a static cast to the base class and returns a void*:
class Base { public: const TypeDescriptor *GetType() const { return mType; } virtual void *Cast(void *object) = 0; protected: Base(TypeDescriptor const *type, const TypeDescriptor *parent): mParent(parent), mType(type) {} private: const TypeDescriptor *mParent; const TypeDescriptor *mType; }; template <typename B, typename D> class BaseImpl : public Base { public: BaseImpl() : Base(Details::Resolve<B>(), Details::Resolve<D>()) {} void *Cast(void *object) override { return static_cast<B*>(object); } };
Analogously he Conversion metaobject has a Convert method that returns an Any containing the converted object:
class Conversion { public: const TypeDescriptor *GetFromType() const { return mFromType; } const TypeDescriptor *GetToType() const { return mToType; } virtual any Convert(const void *object) const = 0; protected: Conversion(const TypeDescriptor *from, const TypeDescriptor *to): mFromType(from), mToType(to) {} private: const TypeDescriptor *mFromType; // type to convert from const TypeDescriptor *mToType; // type to convert to }; template <typename From, typename To> class ConversionImpl : public Conversion { public: ConversionImpl() : Conversion(Resolve<From>(), Resolve<To>()) {} any Convert(const void *object) const override { return static_cast<To>(*static_cast<const From*>(object)); } };
Reflection at work
Let’s see a concrete example of how to use a simple reflection system like this. Let’s define a type called Player and another type Vector3D:
struct Vector3D { float x; float y; float z; }; class Player { public: Player() : id(10), name("Luke") {} Player(int id) : id(id), name("Luke") {} Player(int id, const std::string &name) : id(id), name(name) {} Player(Vector3D const &position) : position(position) {} void SetId(int id) { Player::id = id; } int GetId() const { return id; } void SetName(const std::string &name) { this->name = name; } std::string GetName() const { return name; } void SetPosition(Vector3D position) { this->position = position; } Vector3D GetPosition() const { return position; } void Print(const std::string &s) const { std::cout << s << '\n'; std::cout << "player id: " << id << ", name: " << name << ", health: " << health << '\n'; std::cout << "position: " << position.x << ", y: " << position.y << ", z: " << position.z << std::endl; } std::string SayHello() const { return "hello from " + name + "!"; } float health = 100.0f; private: int id = 11; std::string name;; Vector3D position; };
Now we register these types with the reflection system (we reflect them), and attach to them all the metaobject that we need. We also reflect the type const char* as “cstring” and attach a conversion metaobject to std::string, and the type double as “double”, with a conversion to int. These reflected types are needed to pass a pointer to const char or a double when a std::string or a int are needed:
int main(int argc, char **argv) { Reflect::Reflect<int>("int"); Reflect::Reflect<double>("double") .AddConversion<int>(); Reflect::Reflect<const char*>("cstring") .AddConversion<std::string>(); Reflect::Reflect<Player>("Player") .AddConstructor<>() .AddConstructor<int>() .AddConstructor<int, const std::string&>() .AddConstructor<Vector3D const &>() .AddDataMember<&Player::SetId, &Player::GetId>("id") .AddDataMember<&Player::SetName, &Player::GetName>("name") .AddDataMember(&Player::health, "health") .AddDataMember <&Player::SetPosition, &Player::GetPosition>("position") .AddMemberFunction(&Player::Print, "Print") .AddMemberFunction(&Serialize, "Serialize") .AddMemberFunction(&Deserialize, "Deserialize") .AddMemberFunction(&Player::SayHello, "SayHello"); auto a = Reflect::Resolve("Player")->GetConstructor<int, const std::string &>()->NewInstance(1, "Mario"); Reflect::Resolve("Player")->GetDataMember("position")->Set(a, Vector3D{13.4f, 15.0f, 11.1f}); Reflect::Resolve("Player")->GetDataMember("id")->Set(a, 1.7); Reflect::Resolve("Player")->GetDataMember("name")->Set(a, "Luigi"); Reflect::Resolve("Player")->GetDataMember("health")->Set(a, 20.0f); Reflect::Resolve("Player")->GetMemberFunction("Print")->Invoke(a, Reflect::any("player stats:")); // <--- Invoke with any argument auto b = Reflect::Resolve("Player")->GetConstructor<const Vector3D&>()->NewInstance(Vector3D{ 0.2f, 1.3f, 2.2f }); Reflect::Resolve("Player")->GetMemberFunction("Print")->Invoke(b, Reflect::any("player stats:")); // <--- Invoke with any argument std::cout << "player id: " << *Reflect::Resolve(Player())->GetDataMember("id")->Get(a).TryCast<int>() << std::endl; std::cout << *Reflect::Resolve("Player")->GetMemberFunction("SayHello")->Invoke(a).TryCast<std::string>() << std::endl; std::cout << *Reflect::Resolve("Player")->GetMemberFunction("SayHello")->Invoke(b).TryCast<std::string>() << std::endl; // other code... return 0; }
Now we’re able to construct an instance of Player dynamically at runtime with a string containing the name of the reflected type (“Player”). We can also set and get reflected object fields, and call the object’s member functions with a set of suitable arguments and those functions can return values:
int main(int argc, char **argv) { // reflection code... auto a = Reflect::Resolve("Player")->GetConstructor<int, const std::string &>()->NewInstance(1, "Mario"); Reflect::Resolve("Player")->GetDataMember("position")->Set(a, Vector3D{13.4f, 15.0f, 11.1f}); Reflect::Resolve("Player")->GetDataMember("id")->Set(a, 1.7); Reflect::Resolve("Player")->GetDataMember("name")->Set(a, "Luigi"); Reflect::Resolve("Player")->GetDataMember("health")->Set(a, 20.0f); Reflect::Resolve("Player")->GetMemberFunction("Print")->Invoke(a, Reflect::any("player stats:")); auto b = Reflect::Resolve("Player")->GetConstructor<const Vector3D&>()->NewInstance(Vector3D{ 0.2f, 1.3f, 2.2f }); Reflect::Resolve("Player")->GetMemberFunction("Print")->Invoke(b, Reflect::any("player stats:")); std::cout << *Reflect::Resolve("Player")->GetMemberFunction("SayHello")->Invoke(a).TryCast<std::string>() << std::endl; std::cout << *Reflect::Resolve("Player")->GetMemberFunction("SayHello")->Invoke(b).TryCast<std::string>() << std::endl; return 0; }
The output is:
player stats: player id: 1, name: Luigi, health: 20 position: 13.4, y: 15, z: 11.1 player stats: player id: 11, name: Mario, health: 100 position: 0.2, y: 1.3, z: 2.2 hello from Luigi! hello from Mario!
And that’s it. A simple reflection system that can be used for many useful things, such as serializing/deserializing data, or binding application code to a scripting environment.