Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++11

An Awful, Still Useful C++ Reflection System

4.50/5 (2 votes)
27 Jul 2015CPOL10 min read 16.2K  
Reflection for C++ made easy

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, enums, 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:

C++
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:

C++
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:

C++
XM_DEFINE_CLASS(MyClass)
{
    // Bind a method with automatic name extrapolation
    bindMethod(XM_MNP(myMethod));
  
    // Bind a property from a field
    bindProperty(XM_MNP(myField));

    // Bind a property from get and set methods
    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:

C++
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.

C++
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 Variants. But because Variants 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.

Variants can be casted only to the very same type they were built form, except for reference Variants 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:

C++
MyReflectedClass myRelfectedObject;

// Construct a value Variant from any reflected object
xm::Variant var(myReflectedObject);

// Construct a reference Variant
xm::Variant var2 = xm::ref(myReflectedObject);

// Cast the variant back to the original type
MyReflectedClass myObj2 = var.as<MyReflectedClass>();

// Same here, but the casting is implicit through the casting operator
MyReflectedClass myObj3 = var;

// Throw an exception because of the type mismatch 
int a = var2;

Inheritance

Inheritance and multiple inheritance are supported by eXtendedMirror. Suppose you have a class hierarchy like this:

C++
class MyBase1 {
// Body here
};

class MyBase2 {
// Body here
};

class MyDerived : public MyBase1, public MyBase2 {
// Body here
};

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:

C++
XM_DEFINE_CLASS(MyDerived)
{
    XM_BIND_PBASE(MyBase1);
    XM_BIND_PBASE(MyBase2);

    // Member binding here
}

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:

C++
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:

C++
XM_ASSUME_NON_INSTANTIABLE(MyClass); // MyClass is not instantiable (by default constructor)
XM_ASSUME_NON_DESTRUCTIBLE(MyClass); // MyClass is not destructable
XM_ASSUME_NON_COPYABLE(MyClass); // MyClass is not copyable
XM_DEFINE_CLASS(MyClass);

Since this is always the case for abstract classes, another macro is provided to combine the tree of before:

C++
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:

C++
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, enums and global/static variables (referred to as "free items") are also supported by the system. If you have this code:

C++
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.

C++
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:

C++
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 enums 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:

C++
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:

C++
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:

C++
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:

C++
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:

C++
template<typename T1, typename T2>
XM_DEFINE_CLASS(MyClass<T1, T2>)
{
    // Bind a method with automatic name extrapolation
    bindMethod(XM_MNP(myMethod));
  
    // Bind a property from a field
    bindProperty(XM_MNP(myField));

    // Bind a property from get and set methods
    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:

C++
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:

C++
const xm::CompoundClass& clazz = xm::getCompoundClass("MyClass<int, float>");
std::cout << clazz.getTemplate().getName() << std::endl; // prints "MyClass"
std::cout << clazz.getTemplateArgs()[0].getType().getName() << std::endl; // prints "int"

const xm::CompoundClass& clazz2 = xm::getCompoundClass("MyClass<float, 10>");
std::cout << clazz.getTemplateArgs()[1].getValue().as<int>() << std::endl; // prints "10"

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:

C++
template<typename T, int I>
class MyTemplate2 {
    // Definition here
};

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:

C++
XM_DEFINE_CLASS(MyTemplate2<float, 10>)
{
    // Here we add information about the actual arguments of the template instantiation in the
    // reflection system
    XM_ADD_TEMPL_ARG(xm::getType<float>());
    XM_ADD_TEMPL_ARG(10);

    // Bind other stuff
}

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:

C++
class MyClass {
public:
    int myMethod(int, int);
    int myMethod(float);
};

We can bind its two methods with:

C++
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:

C++
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:

C++
Variant v(MyClass);
v.call("myMethod", 10, 10); // calls myMethod(int, int);
v.call("myMethod", 10.0f);  // calls myMethod(float);
v.call("myMethod", 10);     // fails because it looks for myMethod(int)

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)