C++ reflection - The simplest way

> Coding, hacking, computer graphics, game dev, and such...
User avatar
fips
Site Admin
Posts: 166
Joined: Wed Nov 12, 2008 9:49 pm
Location: Prague
Contact:

C++ reflection - The simplest way

Post by fips »

First, I’m not going to show anything super-sophisticated here. Have a look at Boost Fusion if you are looking for a robust industrial solution for reflection in C++. I’d rather talk about something simple and efficient, for those who appreciate keeping their code-base clean and flexible, without external dependencies, for those like me ;) who like fast iteration cycles and easy debugging.

So, I’ve been recently in need of a simple reflection system that would allow me to pass my POD structures through a generic interface as BLOBs, without loosing the type information about the POD attributes on the way.

After a few iterations, I’ve come up with quite a simple system, similar to this:

VIEW THE CODE BELOW IN FULL-SCREEN (reflection_sample.cpp)

Code: Select all

/*
(c) 2012 +++ Filip Stoklas, aka FipS, http://www.4FipS.com +++
THIS CODE IS FREE - LICENSED UNDER THE MIT LICENSE
ARTICLE URL: http://forums.4fips.com/viewtopic.php?f=3&t=724
*/

#include <array>
#include <vector>
#include <string>
#include <algorithm>
#include <cstdio> // printf
#include <cstdint> // uint16_t, ...
#include <cstddef> // offsetof
#include <cassert>

const uint16_t c_invalid_index = uint16_t(-1);
struct Vector4 { float x, y, z, w; };

/// Describes various type characteristics.
struct Type
{
    std::string name;
    uint16_t byte_size;
    //...

    Type(const std::string &name, uint16_t byte_size):
    name(name), byte_size(byte_size)
    {}
};

/// Manages a table of types, indexed by uint16_t.
class Type_table
{
 public:

    /// Defines a basic set of types, more can be added later using add().
    Type_table()
    {
        add("float", sizeof(float));
        add("Vector4", sizeof(Vector4));
    }

    /// Adds a new type and returns its index.
    uint16_t add(const char *type_name, uint16_t byte_size)
    {
        assert(type_name && byte_size > 0);
        _types.emplace_back(Type(type_name, byte_size));
        return static_cast<uint16_t>(_types.size() - 1);
    }

    /// A direct read-only access to the internal 'Type' can't harm ;)
    const Type & get(uint16_t type_index) const
    {
        assert(type_index < _types.size());
        return _types[type_index];
    }

    /// Finds a type by name, returns 'c_invalid_index' if not found.
    uint16_t find(const char *type_name) const
    {
        assert(type_name);

        const auto it = std::find_if(
         _types.begin(), _types.end(),
         [&type_name](const Type &type) { return type.name == type_name; }
        );

        return
         it != _types.end() ?
         static_cast<uint16_t>(std::distance(_types.begin(), it)) :
         c_invalid_index;
    }

 private:

    std::vector<Type> _types;
};

/// Describes a single struct attribute.
struct Struct_attribute
{
    uint16_t type_index;
    uint16_t byte_offset;

    Struct_attribute():
    type_index(c_invalid_index), byte_offset(0)
    {}

    Struct_attribute(uint16_t type_index, uint16_t byte_offset):
    type_index(type_index), byte_offset(byte_offset)
    {}
};

/// Describes a whole struct (made of attributes) in a generic way.
struct Struct_descriptor
{
    const void *data;
    size_t byte_size;
    const Struct_attribute *attribs_begin;

    Struct_descriptor(const void *data, size_t byte_size, const Struct_attribute *attribs_begin):
    data(data), byte_size(byte_size), attribs_begin(attribs_begin)
    {}
};

/// A sample class with a 'Data' block that supports reflection.
class Material
{
 public:

    Material(const Type_table &type_table)
    {
        const Attribute_array data_attribs =
        {
            Struct_attribute(type_table.find("Vector4"), offsetof(Data, inner_color)),
            Struct_attribute(type_table.find("Vector4"), offsetof(Data, outer_color)),
            Struct_attribute(type_table.find("float"), offsetof(Data, point_size)),
            Struct_attribute() // terminator
        };
        // one unnecessary copy of 'data_attribs' here is worth the initialization through {}
        _data_attribs = data_attribs;
    }

    Struct_descriptor data_descriptor() const
    { return Struct_descriptor(&_data, sizeof(_data), _data_attribs.data()); }

    const Vector4 & inner_color() { return _data.inner_color; }
    void set_inner_color(const Vector4 &color) { _data.inner_color = color; }
    //...

 private:

    /// The actual reflected structure.
    struct Data
    {
        Vector4 inner_color;
        Vector4 outer_color;
        float point_size;

    } _data;

    typedef std::array<Struct_attribute, 4> Attribute_array;
    Attribute_array _data_attribs;
};

/// A function that demonstrates how to work with a 'reflected' structure in a generic way.
void dump(const Type_table &type_table, const Struct_descriptor &desc)
{
    assert(desc.data);
    assert(desc.byte_size > 0);
    assert(desc.attribs_begin);

    printf("struct_byte_size=%d\n", desc.byte_size);
    for(const Struct_attribute *attrib = desc.attribs_begin; attrib->type_index != c_invalid_index; ++attrib)
    {
        const Type &type = type_table.get(attrib->type_index);
        printf(
         "type_index=%d, byte_offset=%d, type_size='%s', type_size=%d\n",
         attrib->type_index, attrib->byte_offset, type.name.c_str(), type.byte_size
        );
    }
}

int main()
{
    const Type_table type_table;

    const Material material(type_table);
    dump(type_table, material.data_descriptor());

    const Material material2 = material; // note that copying is almost for free, with no heap allocations!
    dump(type_table, material2.data_descriptor());

    return 0;
}

// output:
// struct_byte_size=36
// type_index=1, byte_offset=0, type_name='Vector4', type_size=16
// type_index=1, byte_offset=16, type_name='Vector4', type_size=16
// type_index=0, byte_offset=32, type_name='float', type_size=4
// struct_byte_size=36
// type_index=1, byte_offset=0, type_name='Vector4', type_size=16
// type_index=1, byte_offset=16, type_name='Vector4', type_size=16
// type_index=0, byte_offset=32, type_name='float', type_size=4
It’s worth mentioning that the reflection info itself is being kept in std::array, so it’s directly embedded in the host class and there’re no heap allocations involved = big win, cheap to copy, no sharing needed!