Reflection

Sven van Huessen | Nov 4, 2025

Reflection

The way ImGui works is pretty simple. Want to inspect an int? Use ImGui::InputInt.

  • float use ImGui::InputFloat
  • bool use ImGui::Checkbox
  • etc.

So normally when you have a custom object, you would have to manually write an inspector function. For example:

struct GameSettings {
    int volume = 50;
    float sensitivity = 1.0f;
    bool fullscreen = false;
};

void inspect_settings(GameSettings& settings) {
    ImGui::InputInt(settings.volume);
    ImGui::InputFloat(settings.sensitivity);
    ImGui::Checkbox(settings.fullscreen);
}

Now this works fine, pretty easy to implemented. But what if you make more objects? You would need to write a custom inspect function for every object. This can become quite cumbersome and time consuming. Here is where Reflection comes into play.

Type reflection allows you as a programmer to loop through member variables and access the value, name, and type for each member. So in a perfect world we could do something like this:

struct GameSettings {
    int volume = 50;
    float sensitivity = 1.0f;
    bool fullscreen = false;
};

void inspect(object){
    // Doesn't actuall work...
    for (auto& member : object.members()){
        if(member.type == int){
            ImGui::InputInt(member.value);
        } else if(member.type == float){
            ImGui::InputFloat(member.value);
        } else if(member.type == bool){
            ImGui::Checkbox(member.value);
        }
    }
}

Unlike some other languages, C++ does not offer this type built-in reflection (Until C++ 26). Luckily there are some libraries that offer reflection of types. Below I will go over some of the libraries I considered using with their pros, cons, and example code.

Feature RTTR CTTI Visit Struct Boost Describe
Registration Manual None Macro Macro
Compile Time
Member Iteration
Method Reflection
Metadata/Attributes Limited
Enum Support
Header-Only
C++ Version C++11 C++14 C++14 C++14
Private Members

The most important aspects for ImReflect were compile time reflection and member iteration. Additionally, I want it to be as simple as possible to “register” which members need to be inspected. So looking at the table, RTTR already falls of because it’s runtime, and CTTI can’t work because you can’t loop through members.

Code Samples

Now that we only have Visit Struct and Boost Describe left, let’s compare them. I asked Claude AI to generate an example implementation of both.

Examples were generated by Claude AI

Visit Struct

struct GameSettings {
    int volume = 50;
    float sensitivity = 1.0f;
    bool fullscreen = false;
};
VISITABLE_STRUCT(GameSettings, volume, sensitivity, fullscreen);

int main() {
    GameSettings settings;

    visit_struct::for_each(settings, [](const char* name, auto& value) {
        std::cout << name << " = " << value << "\n";
    });
}

Boost Describe

struct GameSettings {
    int volume = 50;
    float sensitivity = 1.0f;
    bool fullscreen = false;
};
BOOST_DESCRIBE_STRUCT(GameSettings, (), (volume, sensitivity, fullscreen))

int main() {
    GameSettings settings;
    
    using Desc = boost::describe::describe_members<GameSettings,
        boost::describe::mod_any_access>;
    
    boost::mp11::mp_for_each<Desc>([&](auto D) {
        std::cout << D.name << " = " << settings.*D.pointer << "\n";
    });
}

Looking at the code snippets, they’re pretty much identical. Simple registration using a macro, and easily loop through members. For ImReflect I decided to go with Visit Struct. There are 2 main reasons for this decision:

  • Experience - In previous projects I also used Visit Struct and I know how it works.
  • Visitation contexts - Visit struct allows you to write multiple macros with different contexts and member variables.

Now that we can loop through members, we need a way of calling the correct ImGui widget for each type. Simplest way would be something like this, with visit struct:

template<typename T>
void inspect(T& object){
    visit_struct::for_each(object, [](const char* name, auto& value) {
        using ValueType = std::decay_t<decltype(value)>;
        if constexpr (std::is_same_v<ValueType, int>) {
            ImGui::InputInt(name, &value);
        } else if constexpr (std::is_same_v<ValueType, float>) {
            ImGui::InputFloat(name, &value);
        } else if constexpr (std::is_same_v<ValueType, bool>) {
            ImGui::Checkbox(name, &value);
        }
    });
}

This function takes any object and loops through it’s reflected members. It checks the type of each member and if it encounters a bool, float, or int, it knows how to inspect it. Nice, this works, but this is not scalable at all. This function would get enormous if we kept adding more types. Additionally, I wanted the library to be expandable, where users could implement their own custom implementations.

Here is where Tag Invoke comes into play.