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:
- No extra data stored on components, copying and moving components had to remain fast.
- No need to inherit from any classes to support reflection for that type.
- Support for common stl-containers like vector and unordered_map
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:- Refactorization: the systems has gone through multiple iterations and could use some cleanup
- Support for member functions, this is theoretically not that much more work and would be a nice addition.
- Adding support for base classes, currently each registration needs to include it's base classes members as well.
- Inspector formatting: The formatting for complex types in the inspector is not ideal, this is something i would like to improve.