Recently I have been experimenting with a simple reflection library for C++. In this blog post series, I will share the details on how I implemented it. It is inspired by golang’s reflect package, although not feature rich like it unfortunately.
If you want to take a look at the full implementation up ahead; you can find it here.
What is reflection?
It is the ability for a process to examine, introspect and modify its own structure and behaviour; more on Wikipedia.
Examples of usage:
Reflection is very useful when it comes to generic programming; here are some examples
Serialization: with the type info generated by reflection, we can read data in memory in a generic way and serialize/deserialize it; avoiding the need to write a specific serialize/deserialize function for each serializable object.
Logging: the same with serialization, log function can query the type info of the data to be printed and format it accordingly, this alleviates the need to overload a format function for each object required.
Debugging tools: in game engines that rely on immediate mode UI systems, we can write a generic inspection tool that works with any data using the type info.
Stringification: for example; generation of strings from enumerators.
Hashing.
We will discuss the implementation details of some of these examples later in this series.
Library features:
- Single file header only library.
- No external dependencies.
- No allocations, type info is stored statically.
- Primitive types, pointers, arrays and enums are supported out of the box.
- Type info for user defined types are generated with minimal writing overhead.
- User defined custom annotations for struct fields stored statically with the type info.
- Works with
MSVC
andGCC
.
Prerequisites:
- MSVC:
/std:c++20 /Zc:preprocessor
. => This will tellMSVC
to conform to the preprocessor standard. - GCC:
-std=c++2b
.
A brief introduction to the API:
// name_of<Foo>(); => "Foo".
auto n = name_of<T>();
// (TYPE_KIND_POINTER, TYPE_KIND_STRUCT, ..etc).
auto k = kind_of<T>(); // By type.
auto k = kind_of(T &&); // By rvalue.
// Get type info.
auto t = type_of<T>(); // By type.
auto t = type_of(T{}); // By instance.
// Get value (pointer to the data, accompanied by the type info).
auto v = value_of(T&&);
// User defined types example.
struct Foo
{
f32 x;
void *internal;
};
// Member fields can be tagged with optional custom user defined annotations.
TYPE_OF(Foo, x, (internal, "NoSerialize"))
void serialize(Value v)
{
...
if (v.type->kind == TYPE_KIND_STRUCT)
{
for (u64 i = 0; i < type->as_struct.field_count; ++i)
{
// Skip serialization if the field is tagged as "NoSerialize".
auto f = type->as_struct.fields[i];
if (f.tag == "NoSerialize")
continue;
// Do serialization.
...
}
}
}
/*
{
.name = "Foo",
.kind = TYPE_KIND_STRUCT,
.size = sizeof(Foo),
.align = alignof(Foo),
.as_struct.fields = {
{"x", offsetof(Foo, x), type_of<f32>(), ""},
{"internal", offsetof(Foo, internal), type_of<void*>(), "NoSerialize"}
},
.as_struct.field_count = 2
}
*/
auto foo_type = type_of<Foo>();
// Works on template types also.
template <typename T>
struct Bar
{
T t;
};
template <typename T>
TYPE_OF(Bar<T>, t)
/*
{
.name = "Bar<Foo>",
.kind = TYPE_KIND_STRUCT,
.size = sizeof(Bar<Foo>),
.align = alignof(Bar<Foo>),
.as_struct.fields = {
{"t", offsetof(Bar<Foo>, t), type_of<Foo>(), ""},
},
.as_struct.field_count = 1
}
*/
auto bar_type = type_of<Bar<Foo>>();
Limitations and things to improve:
- Make it fully compile time.
- Support functions reflection.
- Support Clang.
Let’s get started:
I will break down each function implementation in its own article
- Static reflection system for C++ - name_of - part 1
- Static reflection system for C++ - name_of - part 2
- Static reflection system for C++ - kind_of
- [Static reflection system for C++ - type_of] (WIP)
- [Static reflection system for C++ - value_of] (WIP)