Introduction
One of the key missing functionalities of C++ is, you know that, reflection. Usually, someone who wants to use reflection in one of her projects, has to develop her own system, and those systems are usually application specific, or not shared with the open source community, leading to people reinventing the wheel again and again.
Here I propose a C++ reflection library, simple to use, and powerful enough to support almost everthing you may need. The library is released under a permissive open source license (BSD-2-Clause).
How It Started
I started that way too, because, as every C++ and videogame passionate, the first serious application I wanted to code was a game framework. So I started coding my reflection system alongside the framework.
But since I hadn’t enough time and I found a lot of powerful and complete open source game frameworks/engines online, I abandoned the project. However I never found a satisfying reflection system for C++, one that only uses C++, thus being portable, simple and flexible enough at the same time. I wanted to learn C++ at best, and so I accepted the challenge!
I’ve started working on this project, called eXtendedMirror
, years ago and it went through a lot of redesigns, as my knowledge of the language improved. I feel this project is finally usable, even if it is not feature-complete yet, so I want to tell the world about it and share some details about its usage and the design choices I made.
What's Awful?
Even though the system tries to be as more general, clean and powerful as possible, it results in what I think is an awful workaround for a missing functionality that should be built in the language itself, and hopefully will be in the future. In the mean time, however, I think it could be very useful, so if you are looking to something like that, you can give it a try, or even contribute instead of writing your own.
You can get the code here. The documentation, albeit not completed yet, can be generated through Doxygen.
What's Supported
You may ask what "extended" stands for. It means that, not only almost every construct of the language is supported, but it adds some constructs too, like properties. Properties are an abstraction that can represent many things within a class instance. It can be a field, a couple of get
and set
methods or only a get
method.
All the properties are accessed in the same way, through the system API.
You can reflect classes with methods and properties, non member function, constants, enum
s, global variables, namespaces and templates. Inheritance and multiple inheritance is supported too.
Code Usage
We can start by looking at how to reflect a class. Note that by default, only the public
interface can be reflected. This enforces information hiding and allows the reflection mechanism to be non intrusive. If you want to reflect a private
member, you must make your class friend of the xm::DefineClass
template.
Suppose we want to reflect the following class along with its members:
class MyClass {
public:
int myMethod(int a, int b);
int myField;
int getMyField2();
void setMyField2(int val);
private:
int myField2;
};
To register this class, we first have to put somewhere in a header (after the class definition) the following macro. It will specialize some templates that are needed by the reflection system:
XM_DECLARE_CLASS(MyClass);
Then in some compilation unit (maybe in MyClass.cpp, but not necessarily), you have to put the XM_DEFINE_CLASS
macro. This macro hides the signature of a function. You have to write its body, and within its body, you can bind properties and methods of your class, to the Class
object that will represent the reflected class.
For the previous definition, you should write something like this:
XM_DEFINE_CLASS(MyClass)
{
bindMethod(XM_MNP(myMethod));
bindProperty(XM_MNP(myField));
bindProperty("myField2", &MyClass::getMyField2, &MyClass::setMyField2);
}
XM_REGISTER_TYPE(MyClass);
The last macro can be omitted if you don't want the type to be automatically registered at program startup.
As simple as that. The template deduction mechanism is used to extract all the type information.
XM_MNP (Member Name and Pointer) lets you avoid to write the pointer and the name of the member as parameters of bindProperty
, the preprocessor is used to stringize the field name.
Now, you can access the Class
object in your main function, and call the method by using objects of class Method
, like this:
MyClass myInstance;
const xm::Class& clazz = xm::getClass("MyClass");
const xm::Method& method = clazz.getMethod("myMethod");
int res = method.call(xm::ref(myInstance), 1, 2);
I will explain what the xm::ref
does in the next paragraph.
Instead, if you want to access a property, you can retrieve the corresponding Property
object and use its methods getData
and setData
.
MyClass myInstance;
const xm::Class& clazz = xm::getClass("MyClass");
const xm::Property& property = clazz.getProperty("myField");
property.setData(xm::ref(myInstance), 1);
You may wonder how can Method::call
and Property::getData/Property::setData
accept any kind of argument. In reality, these functions only accepts Variant
s. But because Variant
s have a templated constructor, the passed arguments are converted to Variants
automatically. Of course, there is a limit on the number of parameters the function may take, since you can't use type conversion with a variadic parameter list. This number held in the XM_FUNCTION_PARAM_MAX
macro.
Variants
This type can store any reflected type. It can work in two modes: as a value Variant
or a as a reference Variant
.
In the first case, it automatically manages the memory of the object it holds. In the second case, it just refers to an object managed by someone else.
You can create a reference Variant
with the utility method xm::ref()
like in the example before, where it has been used as the "this
" parameter to call the method on or access the property of.
Variant
s can be casted only to the very same type they were built form, except for reference Variant
s of a class. You can cast such variants to a base class or to a derived class as long as a dynamic_cast
to the expected type will succeed. Otherwise an exception will be raised.
You can also pass to Method::call
or to Property::getData
/Property::setData
a Variant
constructed from a base class or a derived class of the expected parameter class, and the casting will be performed.
Let's look at some examples:
MyReflectedClass myRelfectedObject;
xm::Variant var(myReflectedObject);
xm::Variant var2 = xm::ref(myReflectedObject);
MyReflectedClass myObj2 = var.as<MyReflectedClass>();
MyReflectedClass myObj3 = var;
int a = var2;
Inheritance
Inheritance and multiple inheritance are supported by eXtendedMirror
. Suppose you have a class hierarchy like this:
class MyBase1 {
};
class MyBase2 {
};
class MyDerived : public MyBase1, public MyBase2 {
};
To register MyDerived
as a subclass of MyBase1
and MyBase2
, you just use the XM_BIND_BASE
or XM_BIND_PBASE
macro inside the XM_DEFINE_CLASS
body of the derived class.
The difference between the two is that XM_BIND_PBASE
assumes your class is polymorphic (has a vtable
and can be safely dowcasted) while XM_BIND_BASE
should be used with non polymorphic classes:
XM_DEFINE_CLASS(MyDerived)
{
XM_BIND_PBASE(MyBase1);
XM_BIND_PBASE(MyBase2);
}
And that's it!
Namespaces
Namespaces are supported. To register a class as part of a namespace, just register it specifying the full qualified name as XM_DECLARE_CLASS
argument:
XM_DECLARE_CLASS(myNS::MyClass);
Simple as that.
Special Methods
When you register a class, by default the system assumes it can be instantiated through a public default constructor, copied through a public copy constructor and destroyed through a public destructor.
If this is not the case, you can use these self explanatory macros, prior to the XM_DEFINE_CLASS
:
XM_ASSUME_NON_INSTANTIABLE(MyClass); XM_ASSUME_NON_DESTRUCTIBLE(MyClass); XM_ASSUME_NON_COPYABLE(MyClass); XM_DEFINE_CLASS(MyClass);
Since this is always the case for abstract
classes, another macro is provided to combine the tree of before:
XM_ASSUME_ABSTRACT(MyAbstractClass);
XM_DEFINE_CLASS(MyAbstractClass);
If your class do not have a public
default constructor, but you have non-default public
constructor and you want the system to be able to generate objects of this class without changing the class definition, you can specialize the template ConstructorImpl
to manually provide the system with a factory function that will be used to call the non-default constructor. Suppose that MyClass
have a constructor MyClass(int, int, const char*)
, then you write:
template<>
class ConstructorImpl<MyClass> : public Constructor {
public:
ConstructorImpl(const Class& owner) : Constructor(owner) {};
void init(Variant& var) const
{
new (&var.as<MyClass>()) MyClass(1, 2, "test");
}
};
Non-member Items
Free functions, constants, enum
s and global
/static
variables (referred to as "free items") are also supported by the system. If you have this code:
namespace myNS {
int myFunction(int arg1, int arg2);
const int MyConst = 4;
enum MyEnum {
Val0,
Val1,
Val2
}
int myGlobal;
}
We can register them all by using the XM_BIND_FREE_ITEMS
macro in a compile unit. Like XM_DEFINE_CLASS
, this marco hides among other things, a function signature and you have to write its body.
The macros used in the body are self explanatory and I won't bother much to explain them.
XM_BIND_FREE_ITEMS
{
XM_BIND_FUNCTION(myNs::myFunction);
XM_BIND_COSTANT(myNs::MyConst);
XM_BIND_ENUM(myNs::MyEnum)
.XM_ADD_ENUM_VAL(Val0)
.XM_ADD_ENUM_VAL(Val1)
.XM_ADD_ENUM_VAL(Val2);
XM_BIND_VARIABLE(myNs::myGlobal);
}
The items will be registered automatically at the program startup and you can access them in your code like this:
const xm::Function& func = xm::getFunction("myNs::myFunction");
int res = func.call(10, 20);
const xm::Constant& constant = xm::getConstant("myNs::MyConst");
int val = constant.getValue();
const xm::Enum& enumerator = xm::getEnum("myNs::MyEnum");
int val2 = enumerator.getValue("Val2");
const xm::Variable& variable = xm::getVariable("myNs::myGlobal");
int val2 = variable.getReference();
variable.getReference().as<int>() = 5;
This is self explanatory. Just note that enum
s are converted to plain integers, so Enum::getValue()
returns a Variant
object of type int
, while Variable::getReference()
returns a reference Variant
object referencing the variable. Because, when you cast a Variant
with as()
, or with the casting operator, you cast it by reference, you can write something like the last line and it will actually set the value of the referenced variable.
Static Members
There is not a dedicated way to reflect static
members, because they are basically non member items inside a namespace with the same name of the class they belong. Since a Class
object is also a Namespace
, to reflect static
members, you just have to reflect them as non member items specifying the full qualified name. For clarity, here is an example header:
class MyClass {
static int myStaticFunction(int arg1, int arg2);
static const int MyConst = 4;
enum MyEnum {
Val0,
Val1,
Val2
}
static int myStatic;
};
And the corresponding registration macro to put in a compile unit:
XM_BIND_FREE_ITEMS
{
XM_BIND_FUNCTION(MyClass::myStaticFunction);
XM_BIND_COSTANT(MyClass::MyConst);
XM_BIND_ENUM(MyClass::MyEnum)
.XM_ADD_ENUM_VAL(Val0)
.XM_ADD_ENUM_VAL(Val1)
.XM_ADD_ENUM_VAL(Val2);
XM_BIND_VARIABLE(MyClass::myStatic);
}
Templates
There are two ways to reflect templates: For templates having only types (i.e. no constant values) as template parameter, you can use this first and more powerful approach.
Suppose you have the following class template:
template<typename T1, typename T2>
class MyClass {
public:
T1 myMethod(T2 a, T2 b);
T1 myField;
T2 getMyField2();
void setMyField2(T2 val);
private:
T2 myField2;
};
The process is very similar to that of reflecting a normal class. You first "declare" the template class in a header using the provided XM_DECLARE_TEMPLATE_N
function of the right arity N
, for example:
XM_DECLARE_TEMPLATE_2(MyClass);
Then you "define" the class. This time however, you have to put this code in an header accessible to all the compile units that will register a type instance of this template:
template<typename T1, typename T2>
XM_DEFINE_CLASS(MyClass<T1, T2>)
{
bindMethod(XM_MNP(myMethod));
bindProperty(XM_MNP(myField));
bindProperty("myField2", &MyClass::getMyField2, &MyClass::setMyField2);
}
Remember that you can specialize, or partial specialize your class, and everything will still be fine, as long as you keep the same part of the interface that has been reflected. It is not much useful to specialize changing the interface, so this should not be a problem.
Now you can explicitly register a class instantiated from this template like this:
XM_REGISTER_TYPE(MyClass<int, float>);
The power of this approach is that you don't even need to register explicitly every possible instantiation of the template. Every time a function, method, property, constant or variable that uses a type instantiated from this template is registered, the instantiated type gets registered too. You can access the Template
object of a CompoundClass
and retrieve its name.
This may be useful, for instance, if you want to treat uniformly all the containers instantiated from the same template. You can also access the parameters used to instantiate the template.
Look at this example:
const xm::CompoundClass& clazz = xm::getCompoundClass("MyClass<int, float>");
std::cout << clazz.getTemplate().getName() << std::endl; std::cout << clazz.getTemplateArgs()[0].getType().getName() << std::endl;
const xm::CompoundClass& clazz2 = xm::getCompoundClass("MyClass<float, 10>");
std::cout << clazz.getTemplateArgs()[1].getValue().as<int>() << std::endl;
Reflecting Templates with Value Parameters
As previously said, the above method doesn’t work If you have value parameters in you template parameter list. An alternative, although less convenient method is provided for this case:
For every possible instantiation of your template, you have to put a XM_DECLARE_CLASS
in a header:
template<typename T, int I>
class MyTemplate2 {
};
XM_DECLARE_CLASS(MyTemplate2<float, 10>);
And in a compile unit, for every possible instantiation of your template, you have to put a XM_DEFINE_CLASS
.
The type is automatically recognized as an instantiation of the MyTemplate2
template, but the actual template arguments aren't recognized automatically. You have instead to specify them manually inside the XM_DEFINE_CLASS
macro:
XM_DEFINE_CLASS(MyTemplate2<float, 10>)
{
XM_ADD_TEMPL_ARG(xm::getType<float>());
XM_ADD_TEMPL_ARG(10);
}
Overloads
To register an overloaded method or function, we have to help the deduction mechanisms to pick the one we want by explicitly specifying the template parameters of the bindMethod
or bindFunction
functions.
Suppose we have this class:
class MyClass {
public:
int myMethod(int, int);
int myMethod(float);
};
We can bind its two methods with:
XM_DEFINE_CLASS(MyClass)
{
bindMethod<ClassT, int, int, int>(XM_MNP(myMethod));
bindMethod<ClassT, float>(XM_MNP(myMethod));
}
Now, you may wonder how can we now refer to the different overloads when we retrieve the Method
object. Simple, we can use a Method
object as key and by constructing it with the utility function template methodSign
.
Here is an example:
const xm::Class& clazz = xm::getClass("MyClass");
const xm::Method& method = clazz.getMethod(xm::methodSign<MyClass, float>());
I won't say much on function overloads, since the process is quite the same of that for methods: The function bindFunction
and functionSign
are just like bindMethod
and MethodSign
, except they don’t take the reference to the class as first parameter
XM also provides a more direct way of calling an overloaded method, that is via the call()
method of a Variant
object.
Look at this example:
Variant v(MyClass);
v.call("myMethod", 10, 10); v.call("myMethod", 10.0f); v.call("myMethod", 10);
It automatically and dynamically resolves the overload to call the appropriate method according to the passed arguments. Note however that the type of the parameters and that of the arguments must match exactly to the overload to be found!
There's More!
I've covered the key functionalities of the API but this is not all. If you want, you can delve into the source code, or read the documentation.
Further improvements are possible, like allowing multiple constructors, allowing casting variants to compatible types, and implementing a dynamic overload resolution that matches the C++ static
one. But the essential for supporting your project should be here.
I hope you enjoyed the reading, and maybe you can find this library useful for your next projects. Any feedback (or contribution) is also greatly appreciated.