Tag Invoke

Sven van Huessen | Nov 4, 2025

Tag Invoke / Library Design

I designed ImReflect in such a way where the user has full control over how widgets get drawn. I do this by using the tag invoke design pattern.

ImReflect Library design showcase

It works like this; Whenever the single entry point gets called with a type, it will find, at compile time, a compatible overloaded function using tag invoke. It checks this in a specific order:

  1. User defined overloads
  2. Library defined overloads
  3. Reflected overload (only if the type is reflected)

This way, if the user doesn’t like how I, the library maker, implemented a widget, they can just provide their own overload and it will be used instead.

In code this looks like this:

template<typename T>
void input_impl(const char* label, T& value, ImSettings& settings, ImResponse& response) {
    /* Priority 1: User-defined implementations */
    if constexpr (svh::is_tag_invocable_v<ImInput_t, const char*, T&, ImSettings&, ImResponse&>) {
        tag_invoke(input, label, value, type_settings, type_response);
    }
    /* Priority 2: Library-provided implementations */
    else if constexpr (svh::is_tag_invocable_v<ImInputLib_t, const char*, T&, ImSettings&, ImResponse&>) {
        tag_invoke(input_lib, label, value, type_settings, type_response);
    }
    /* Priority 3: Reflection-based automatic generation */
    else if constexpr (visit_struct::traits::is_visitable<T, ImContext>::value) {
        imgui_input_visit_field(label, value, type_settings, type_response);
    } 
    /* Compile error if no implementation found */
    else {
        static_assert(svh::always_false<T>::value, 
        "ImReflect Error: No suitable Input implementation found for type T");
    }
}

This hierarchy ensures that user customizations always takes priority, followed by library defaults, and finally automatic reflection for registered structs.

Library defined overloads

This is how I, as the library maker, define an overload/specialization for an int type.

I define a tag_invoke function that takes in a ImInputLib_t tag (to specify it’s a library defined overload), the label, the value (int), the settings and the response.

template<typename T>
void tag_invoke(Detail::ImInputLib_t, const char* label, int& value, ImSettings& settings, ImResponse& response) {
    //....
}

User defined overloads

This is how a user can define their custom implementation for a Transform type. The compiler will find this overload automatically. So even if Transform is a reflected type, this overload will still get priority over the reflected one.

void tag_invoke(ImReflect::ImInput_t, const char* label, Transform& value, ImSettings& settings, ImResponse& response) {
    //....
}

Users can also overwrite library defined implementations by just defining an overload with the ImInput_t tag.

void tag_invoke(ImReflect::ImInput_t, const char* label, int& value, ImSettings& settings, ImResponse& response) {
    //....
}

Now whenever the user calls the single entry point with an int, this overload will be picked instead of the implementation the library provided.

This gives users full control over how widgets get drawn without ever having to modify the library code and is completely optional.

Reflected overloads

If the type is reflected, I can use Visit Struct to iterate over all fields and call the single entry point for each field.

template<typename T>
void imgui_input_visit_field(const char* label, T& value, ImSettings& settings, ImResponse& response) {
    ImGui::SeparatorText(label);
    visit_struct::context<ImContext>::for_each(value,
        [&](const char* name, auto& field) {
            auto& member_settings = settings.get_member(value, field);
            auto& member_response = response.get_member(value, field);
            input_impl(name, field, member_settings, member_response); // recurse
        });
}

This way, users can just reflect their types and get a fully functional UI for free. It recurses into nested reflected types.