Experimenting with C++ Reflection
For a number of projects I've worked on at Canonical have involved
using GObject based libraries from C++. To make using these APIs easier
and safer, we've developed a few helper utilities. One of the more
useful ones was a class (originally by Jussi
Pakkenen) that presents an API similar
to the C++11 smart pointers but specialised for GObject types we called
gobj_ptr
.
This ensures that objects are unrefed when the smart pointer goes out of
scope, and increments the reference count when copying the pointer.
However, even with the use of this class I still end up with a bunch of type cast macros littering my code, so I was wondering if there was anything I could do about that. With the current C++ language the answer seems to be "no", since GObject's convention of subclasses as structures whose first member is a value of the parent structure type is at a higher level but then I found a paper describing an extension for C++ Static Reflection. This looked like it could help, but seems to have missed the boat for C++17. However, there is a sample implementation for Clang written by Matus Chochlik, so I downloaded and compiled that to have a play.
At its heart, there is a new reflexpr()
operation that takes a type as
an argument, and returns a type-like "metaobject" that describes the
type. For example:
using MO = reflexpr(sometype);
These metaobjects can then be manipulated using various helpers in the
std::meta
namespace. For example,
std::meta::reflects_same_v<MO1,MO2>
will tell you whether two
metaobjects represent the same thing. There were a few other useful
operations:
std::meta::Class<MO>
will return true if the metaobject represents a class or struct (which are effectively interchangeable in C++).std::meta::get_data_members_m<MO>
will return a metaobject representing the members of a class/struct.- From a sequence metaobject, we can determine its length with
std::meta::get_size_v<MO>
, and retrieve the metaobject elements in the sequence withstd::meta::get_element_m<MO>
- We can get a metaobject representing the type for a data member
metaobject with
std::meta::get_type_m<MO>
.
Put all this together, and we've got the building blocks to walk the GObject inheritance hierarchy at compile time. Now rather than spread the reflection magic throughout my code, I used it to declare a templated compile time constant:
template <typename Base, typename Derived>
constexpr bool is_base_of = ...
With this, an expression like is_base_of<GObject, GtkWidget>
will
evaluate true, while something like
is_base_of<GtkContainer, GtkLabel>
will evaluate false.
As a simple example, this constant could be used to implement an up-cast helper:
template <typename Target, typename Source>
inline Target* gobj_cast(Source* v)
{
static_assert(is_base_of<Target, Source>,
"Could not verify that target type is a base of source type");
return reinterpret_cast<Target*>(v);
}
If we can verify that this is a correct up-cast, this function will
compile down to a simple type cast. Otherwise, compilation will fail
on the static_assert
, printing a relatively short and readable error
message.
The same primitive could be used for other things, such as
allowing you to construct a gobj_ptr<T>
from an instance of a
subclass, or copying one gobj_ptr
to another one representing a
parent class.
It'd be nice to implement something like dynamic_cast
for
down-casting, but I don't think even static reflection will help
us map from a struct type to the corresponding helper function
that returns the GType
.
If you want to experiment with this, the code used to implement all of the above can be found in the following repository: