C++ Reflection

During game project 7 we had a need to view and edit components from our editor, and since we ideally wanted to keep our editor and game projects separate, which meant no component-specific code, I was tasked with making a reflection system.

What is reflection?

In programming, reflection refers to the ability of a program to examine and modify its own structure and behavior at runtime. Some languages, like C# and Java, support reflection natively. This enables them to support features like automatic serialization and deserialization or creating objects from names.
However, one of the core ideas of C++ is that you only pay for what you use. For reflection this means that any metadata of the program's original structure is lost during compilation, and it's up to the programmer to implement any features needed to support reflection.

Some of my goals with the reflection system, besides providing a way to edit components outside of the editor was:

Registration

With the first requirement in mind, I decided on creating a registry to store the data, a class with static methods where you register a type's members once at the start of the program and can then be later globally used to query type information by using, for example, the registered name or an instance of the object.

The registration of a type is done in the following manner:

                
MetaRegistry::RegisterClass<Foo>("Foo")
    .RegisterMember("bar", &Foo::bar)
    .RegisterMember("baz", &Foo::baz);
                
            

As all registered classes are templated, they inherit from the common class IMetaClass. This class is an interface that allows us to query a given type without knowing the type that is reflected.
Registering members of a class is similar, however, they are stored on each class and are done by utilizing the pointers to members syntax. The implementation of RegisterMember looks like the following:

                    
template <typename MemberType>
MetaClass& RegisterMember(const char* aName, MemberType ClassType::* aMember)
{
    myMembers.emplace_back(
        std::make_unique<MetaMember<ClassType, MemberType>>(aName, aMember)
    );
    return *this;
}
                    
                

Where the ClassType template is the owner of the current member.

How I needed the reflection system

As this system was intended mostly as a way to edit components from an inspector, I needed a way to check if an entity in our entity component system contained any reflectable types. The way I chose to meet this requirement was by iterating all reflectable types, then calling a virtual function that returns true if the selected entity owns a component of the current type.

                
for (IMetaClass& type : MetaRegistry::Classes())
{
    if (type.Contains(aRegistry, aSelectedEntity))
    {
        std::unique_ptr<IMetaReference> reference = type.CreateReference(aRegistry, aSelectedEntity);
        RenderWidget(reference, aRegistry, aSelectedEntity);
    }
}
                
            

Creating references to members

In the above example, I use the method CreateReference. This method is simply a virtual function that gets the object out of our entity component system and stores it as a pointer in the MetaReference class.

The MetaMember class, however, currently just stores a pointer to a member, which looks like MemberType ClassType::*. This is not a normal pointer, we need an instance of ClassType to dereference it. Luckily, since we already store a reference to the ClassType-object and ClassType is a known type in the MetaMember-class, we can cast the type-erased reference back into its real type.

This allows us to create references to members of classes as well.

                
std::unique_ptr<IMetaReference> CreateReference(IMetaReference& aParentClass)
{
    ClassType& parent = *static_cast<MetaReference<ClassType>&>(aParent).GetReference();
    return std::make_unique<MetaReference<MemberType>>(parent.*myMemberPointer, GetName());
}
MemberType ClassType::* myMemberPointer;
                
            

Casting type-erased interface classes back into their templated derived class through virtual functions like this is something I have heavily utilized in this system. And while not very pretty, it's needed as I need to store and operate on them without knowing their types at compile-time.

Iterating and rendering the members

The first approach

My first approach to rendering through a given type's members was done by iterating through all members of the current type with each recursive call, and when encountering a type that's known, for example, fundamental types such as int and float, I called the appropriate function to handle that type.

That method looked similar to the following example:

                    
void RenderWidget(IMetaReference& aReference)
{
    ImGui::Text(aReference.GetName());
    if (HasKnownEditingFunction(aReference))
    {
        EditType(aReference);
        return;
    }

    ImGui::Indent();
    for (IMetaMember& memberType : aReference.GetMembers())
    {
        std::unique_ptr<IMetaReference> memberReference = member.CreateReference(aReference);
        RenderWidget(memberReference);
    }
    ImGui::Unindent();
}
                    
                

However, this method was flawed in that it both required quite a few heap allocations more than might be necessary, but more importantly, it was flawed in the way that it wasn't possible to loop through the members of templated stl-types like vector or, for a more difficult example, tuples. Not without registering each possible instantiation of that type.

The second approach

After scratching my head for a while and trying different solutions to the problems with the first method, I finally tried to utilize the visitor pattern.
The visitor pattern approach allowed me to specialize the way members of a type should be iterated by creating a template specialization of the MetaReference class. While this is not a perfect solution, it allows me to iterate through types that would otherwise be impossible without an explicit registration of that type and its exact template parameters.

The editor now inherits from a Visitor class, and the iteration is updated like the following example:

                
class EditorVisitor : public MetaVisitor
{
    void Accept(IMetaReference& aReference) const override
    {
        ImGui::Text(aReference.GetName());
        if (HasKnownEditingFunction(aReference))
        {
            EditType(aReference);
        }
        else
        {
            ImGui::Indent();
            for (IMetaMember& memberType : aReferece.GetMembers())
            {
                std::unique_ptr<IMetaReference> memberReference = memberType.CreateReference(aReference);
                memberReference->Visit(*this);
            }
            ImGui::Unintent();
        }
            
    }
};

EditorVisitor visitor;
for (IMetaClass& type : MetaRegistry::Classes())
{
    if (type.Contains(aRegistry, aSelectedEntity))
    {
        std::unique_ptr<IMetaReference> reference = type.CreateReference(aRegistry, aSelectedEntity);
        visitor.Visit(*reference);
    }
}
                
            

The default case

                
template <typename T>
class MetaReference : public IMetaReference
{
public:
    void Accept(MetaVisitor& aVisitor) override
    {
        aVisitor.Accept(*this);
    }
private:
    T* myReference;
};
                
            

A template specialization of vector

                
template <typename T>
class MetaReference<std::vector<T>> : public IMetaReference
{
public:
    void Accept(MetaVisitor& aVisitor) override
    {
        for (size_t i = 0; i < myReference->size(); ++i)
        {
            std::string name = std::to_string(i);
            MetaReference<T> elem((*myReference)[i], name.c_str());
            aVisitor.Accept(elem);
        }
    }
private:
    std::vector<T>* myReference;
};
                
            

Results

Utilizing templates like this allowed the reflection system to become very flexible and support almost any type with minimal effort. But it's not without it's shortcomings.

Some things I would like to improve or add include:

That was a lot of details, let's see it in action!

All the boilerplate a user needs to write for a type this complex

registration

The generated inspector GUI

generated gui

Adding custom rendering for a type

custom rendering support