logo
Kurashizu Blogwhere ideas flow
  • Home
  • News
  • Blog
  • About
  • Home
  • News
  • Blog
  • About

© 2026 Kurashizu. All rights reserved.

Admin·New Post·Service Status
Powered by Cloudflare·01.11.26
← Back to news

Beautiful Type Erasure with C++26 Reflection

Original article
ryanjk5.github.io|83 points|33 comments|by RyanJK5|Jul 14, 2026

Elegant Type Erasure via C++26 Reflection

By Ryan Keane

If you have ever attempted to implement type erasure for anything more complex than the basic std::any or std::function, you likely encountered one of two paths: writing hundreds of lines of fragile, manual boilerplate or relying on heavy-duty libraries like Folly.Poly or Boost.TypeErasure.

rjk::duck leverages the power of C++26 reflection to eliminate these hurdles, providing a seamless experience without sacrificing performance or flexibility.

A Practical Example

Consider a scenario where we want a generic container interface:

#include <rjk/duck.hpp>
#include <vector>
#include <string>
#include <map>

// Define the interface using a trait annotation
struct [[ = rjk::trait ]] Container { 
    auto size() const - std::size_t; 
    auto empty() const - bool; 
    auto clear() - void; 
};

int main() {
    // The duck can hold any type that satisfies the Container trait
    rjk::duck<Container> c { std::vector<int>{1, 2, 3} };
    c.size(); // Returns 3

    c = std::string{"hello"}; // Runtime type swap
    c.size(); // Returns 5

    c = std::map<int, int>{{1, 2}, {3, 4}};
    c.empty(); // Returns false
}

Instead of writing complex wrappers, you simply define the interface once, and the library handles the underlying implementation.

Key Capabilities of rjk::duck:

  • Owning and non-owning semantics
  • Support for operators
  • Interface composition
  • Adapters for legacy interfaces
  • Extension methods for external types

Warning: This is bleeding-edge technology. Currently, this is only supported on GCC using the flags -std=c++26 -freflection.


Demystifying the Magic

The library uses reflection in ways that go far beyond simple JSON serialization. To understand how rjk::duck works, we need to look at four primary mechanisms: tag generation, vtable codegen, overload resolution, and interconvertibility.

1. C++26 Reflection Basics

In the example above, you saw [[=rjk::trait]]. This is a C++26 annotation, which functions similarly to an attribute but is accessible via reflection.

The trait identifier is defined simply as: constexpr inline struct {} trait {};

To detect if a type possesses this annotation, the library uses the ^^ (reflection) operator:

return std::ranges::any_of(
    annotations_of( ^^ MyType ), 
    [](std::meta::info annotation) { 
        return type_of(annotation) == type_of( ^^ trait ); 
    }
);

Key Concepts:

  • ^^: The reflection operator. It turns a type or variable into a std::meta::info object.
  • std::meta::info: The universal type for all reflections.
  • annotations_of / type_of: Meta-functions used to query the properties of a reflection.

2. The Tag Generation Process

The first step is converting the members of a trait struct into internal tags. For instance, if we have:

struct [[ = rjk::trait ]] MyTrait { 
    auto foo() - void; 
    auto bar() const - int; 
};

The goal is to produce tags like has_fn "foo", auto() - void and has_fn "bar", auto() const - int.

The Transformation Pipeline:

The implementation looks like this:

consteval auto members_to_tags(std::meta::info trait) - std::vector<std::meta::info> {
    const auto ctx = std::meta::access_context::unprivileged(); 
    
    return members_of(trait, ctx) 
        | std::views::filter(std::meta::is_user_declared) // No constructors
        | std::views::filter(std::meta::is_function)      // No data members
        | std::views::filter(std::meta::has_identifier)   // No operators
        | std::views::transform([](std::meta::info member) {
            const fixed_string name { identifier_of(member) }; 
            const auto signature = type_of(member);
            
            // Create the tag: substitute( ^^ has_fn, { name, signature })
            return substitute( ^^ has_fn, { reflect_constant(name), signature });
        }) 
        | std::ranges::to<std::vector>();
}

3. Generating the VTable

C++26 provides a limited but powerful way to generate code. The vtable is constructed using a consteval block and the define_aggregate function.

The Logic Flow:

  1. Iterate through all provided Traits using template for.
  2. Convert each trait to tags via members_to_tags.
  3. Transform the function signature to include a void* (the object pointer).
  4. Define the final vtable structure.

Mathematical Representation of the Signature Shift: If a trait method is defined as f(Args)→Retf(\text{Args}) \rightarrow \text{Ret}f(Args)→Ret, the vtable entry is transformed into: vtable_fn(void∗self,Args)→Ret\text{vtable\_fn}(\text{void}^* \text{self}, \text{Args}) \rightarrow \text{Ret}vtable_fn(void∗self,Args)→Ret

Implementation Snippet:

template <is_trait... Traits>
consteval {
    std::vector<std::meta::info> members { /* basic ops: copy, move, destroy */ };
    constexpr static std::array<std::meta::info, sizeof...(Traits)> traits { ^^ Traits... };

    template for (constexpr auto trait : traits) {
        for (const auto tag : members_to_tags(trait)) {
            const auto args = template_arguments_of(tag);
            const auto name = extract_fixed_string(args[0]);
            const auto func_type = remove_fn_qualifiers(args[1]);
            
            // Prepend void* to the function signature
            const auto signature = add_pointer(prepend_arg(func_type, ^^ void*));
            const auto member = data_member_spec(signature, { name });
            
            members.push_back(member);
        }
    }
    define_aggregate( ^^ vtable, members );
}

Summary Comparison

FeatureTraditional Type Erasurerjk::duck (C++26)
BoilerplateHigh\text{High}High (Manual wrappers)Low\text{Low}Low (Trait-based)
MaintenanceDifficult (Manual updates)Automatic (Reflection-based)
PerformanceVTable overheadVTable overhead (Optimized)
FlexibilityRigidHighly Composable