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 and GCC.

Prerequisites:

  • MSVC: /std:c++20 /Zc:preprocessor. => This will tell MSVC 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