Skip to content

ReflectCxx/ReflectionTemplateLibrary-CPP

Repository files navigation

Reflection Template Library (RTL) – A Run-Time Reflection System for C++.

License: MIT   CMake   C++20   Build   Codecov   Try RTL Online

RTL provides type-safe run-time reflection for C++, combining compile-time guarantees with run-time flexibility.

It enables name-based discovery and invocation of functions, constructors, and object members through a non-intrusive, type-safe reflection system that follows modern C++ idioms. For example, consider the following function –

std::string complexToStr(float real, float img);

Using RTL, you can discover this function by name and invoke it dynamically –

rtl::function<std::string(float, float)> cToStr = cxx::mirror().getFunction("complexToStr")
                                                               ->argsT<float, float>()
                                                               .returnT<std::string>();
if(cToStr) {   // Function materialized?
    std::string result = cToStr(61, 35);  // Works!
}
// cxx::mirror() returns an instance of 'rtl::CxxMirror' (explained in Quick-Preview section)

No compile-time coupling to target symbols. No unsafe casting. No guesswork. Just run-time lookup and type-safe invocation.

Performance

RTL’s reflective calls are comparable to std::function for fully type-erased dispatch, and achieve lower call overhead (just a function-pointer hop) when argument and return types are known.

Design Highlights

  • Single Source of Truth – All reflection metadata can be centralized in a single immutable rtl::CxxMirror, providing a consistent, thread-safe, duplication-free, and deterministic view of reflected state.

  • Non-Intrusive & Macro-Free – Reflection metadata is registered externally via a builder-style API, with no macros, base classes, or intrusive annotations required on user types.

  • Zero-Overhead by Design – Metadata can be registered and resolved lazily. Reflection introduces no runtime cost beyond the features explicitly exercised by the user.

  • Cross-Compiler Consistency – Implemented entirely in standard C++20, with no compiler extensions or compiler-specific conditional behavior.

  • Tooling-Friendly Architecture – Reflection data is encapsulated in a single immutable, lazily-initialized structure that can be shared with external tools and frameworks without compile-time type knowledge – suitable for serializers, debuggers, test frameworks, scripting engines, and editors.

Design Features RTL Syntax & Semantics

A Quick Preview: Reflection That Looks and Feels Like C++

First, create an instance of CxxMirror

auto cxx_mirror = rtl::CxxMirror({ /* ...register all types here... */ });

The cxx_mirror object provides access to the runtime reflection system. It enables querying, introspection, and instantiation of registered types without requiring compile-time type knowledge at the call site. It can reside in any translation unit. To make it globally accessible and ensure it is initialized only when needed, a singleton interface can be used –

// MyReflection.h
namespace rtl { class CxxMirror; }	// Forward declaration, no includes here!
struct cxx { static rtl::CxxMirror& mirror(); };	// The Singleton.

define and register everything in an isolated translation unit –

// MyReflection.cpp
#include <rtl_builder.h> 	// Reflection builder interface.

rtl::CxxMirror& cxx::mirror() {
    static auto cxx_mirror = rtl::CxxMirror({   // Inherently thread safe.
        // Register free(C-Style) function -
	    rtl::type().function("complexToStr").build(complexToStr),
	    // Register class 'Person' ('record' is general term used for 'struct/class') -
	    rtl::type().record<Person>("Person").build(), // Registers default/copy ctor as well.
	    // Register user defined ctor -
	    rtl::type().member<Person>().constructor<std::string, int>().build(),
        // Register method -
	    rtl::type().member<Person>().method("getName").build(&Person::getName)
    });
    return cxx_mirror;
}

RTL in action:

Explore the demo code

Lookup the Person class by its registered name –

std::optional<rtl::Record> classPerson = cxx::mirror().getRecord("Person");
if (!classPerson) { /* Class not registered. */ }

rtl::CxxMirror provides two lookup APIs that return reflection metadata objects: rtl::Record for any registered type (class, struct or pod) and rtl::Function for non-member functions.

From rtl::Record, registered member functions can be queried as rtl::Method. These are metadata descriptors (not callables) and are returned as std::optional, which will be empty if the requested entity is not found.

Callables are materialized by explicitly providing the argument types we intend to pass. If the signature is valid, the resulting callable can be invoked safely. For example, the overloaded constructor Person(std::string, int)

rtl::constructor<std::string, int> personCtor = classPerson->ctor<std::string, int>();
if (!personCtor) { /* Constructor with expected signature not found. */ }

Or the default constructor –

rtl::constructor<> personCtor = classPerson->ctor();

Instances can be created on the Heap or Stack with automatic lifetime management –

auto [err, robj] = personCtor(rtl::alloc::Stack, "John", 42);
if (err != rtl::error::None) { std::cerr << rtl::to_string(err); } // Construction failed.

The constructed object is returned wrapped in rtl::RObject. Heap-allocated objects are internally managed via std::unique_ptr, while stack-allocated objects are stored directly in std::any.

Now, Lookup a member-function by name –

std::optional<rtl::Method> oGetName = classPerson->getMethod("getName");
if (!oGetName) { /* Member function not registered */ }

And materialize a complete type-aware caller –

rtl::method<Person, std::string()> getName = oGetName->targetT<Person>()
                                                     .argsT().returnT<std::string>();
if (!getName) { 
    std::cerr << rtl::to_string(getName.get_init_err()); 
}
else {
    Person person("Alex", 23);
    std::string nameStr = getName(person)(); // Returns string 'Alex'.
}

The above getName invocation is effectively a native function-pointer hop, since all types are known at compile time.

If the concrete type Person is not accessible at the call site, its member functions can still be invoked by erasing the target type and using rtl::RObject instead. The previously constructed instance (robj) is passed as the target –

rtl::method<rtl::RObject, std::string()> getName = oGetName->targetT()
                                                            .argsT().returnT<std::string>();
auto [err, ret] = getName(robj)();	// Invoke and receive return as std::optional<std::string>.
if (err == rtl::error::None && ret.has_value()) {
    std::cout << ret.value();
}

If the return type is also not known at compile time,rtl::Return can be used –

rtl::method<rtl::RObject, rtl::Return()> getName = oGetName->targetT()
                                                            .argsT().returnT();
auto [err, ret] = getName(robj)();	// Invoke and receive rtl::RObject as return, wrapping std::string underneath.
if (err == rtl::error::None && ret.canViewAs<std::string>()) {
    const std::string& name = ret.view<std::string>()->get();
    std::cout << name;	// Safely view the returned std::string.
}

How RTL Fits Together

At a high level, every registered C++ type is encapsulated as an rtl::Record. Callable entities (functions, member functions and constructors) are materialized through rtl::Function, rtl::Method and rtl::Record, all of which are discoverable via rtl::CxxMirror.

RTL provides the following callable entities, designed to be as lightweight and performant as std::function (and in many micro-benchmarks, faster when fully type-aware):

rtl::function<> – Free (non-member) functions

rtl::constructor<> – Constructors

rtl::method<> – Non-const member functions

rtl::const_method<> – Const-qualified member functions

rtl::static_method<> – Static member functions

These callable types are regular value types: they can be copied, moved, stored in standard containers, and passed around like any other lightweight object.

When invoked, only type-erased callables return an rtl::error, with results provided as rtl::RObject (when both the return and target types are erased) or as std::optional<T> (when only the target type is erased), while fully type-aware callables return T directly with no error (by design).

How to Build (Windows / Linux)

mkdir build && cd build
cmake ../ -G "<Generator>"    # Use a C++20-compatible compiler
cmake --build .

Run the generated binaries from bin/:

  • RTLTestRunApp – Reflection tests and examples
  • RTLBenchmarkApp – Performance benchmarks

Additional resources:

  • RTLTestRunApp/src – Detailed test cases
  • RTLTestRunApp/src/MyReflectionTests/ – Tutorial example
  • RTLBenchmarkApp/src – Benchmark implementations
  • run_benchmarks.sh – Automated benchmark runs

Reflection Features

  • Function Reflection – Register and invoke C-style functions, supporting all kinds of overloads.

  • Class and Struct Reflection – Register and dynamically reflect their methods, constructors, and destructors.

  • Complete Constructor Support :

    • Default construction.
    • Copy/Move construction.
    • Any overloaded constructor.
  • Allocation Strategies & Ownership :

    • Choose between Heap or Stack allocation.
    • Automatic move semantics for ownership transfers.
    • Scope-based destruction for Heap allocated instances.
  • Member Function Invocation :

    • Static methods.
    • Const/Non-const methods.
    • Any overloaded method, Const/Non-Const based as well.
  • Perfect Forwarding – Binds LValue/RValue to correct overload.

  • Zero Overhead Forwarding – No temporaries or copies during dispatch and arguments forwarding.

  • Failure Semantics – Explicit rtl::error diagnostics for all reflection operations (no exceptions, no silent failures).

  • Smart Pointer Reflection – Reflect std::shared_ptr and std::unique_ptr, transparently access the underlying type, with full sharing and cloning semantics.

  • 🟨 Conservative Conversions – Safely reinterpret reflected values. For example: treat an int as a char, or a std::string as a std::string_view / const char* (In Progress)

  • 🚧 STL Wrapper Support – support for wrappers like std::optional and std::reference_wrapper. Return them, forward them as parameters, and access wrapped entities transparently. (In Progress)

  • 🚧 Relaxed Argument Matching – Flexible parameter matching for reflective calls, enabling safe conversions (ex- base/derived) and overload resolution. (In Progress)

  • Inheritance Support: Next in line.

  • Composition Support: Planned.

  • Property Reflection: Planned.

  • Enum Reflection: Planned.

  • Metadata iterators: Planned.

💚 Support RTL’s Development

RTL is an actively maintained, production-oriented C++ runtime reflection system focused on performance, type safety, and real-world usability.

Sponsorship supports continued improvement of RTL’s core reflection capabilities, along with:

  • Production-ready examples
  • Tooling and documentation
  • Cross-platform CI and testing

If you’re interested in advancing practical runtime reflection in C++ and supporting the continued evolution of RTL’s core capabilities, consider sponsoring the project.

Sponsor RTL

C++ joins the reflection party! – why should Java have all the fun?

Sponsor this project

 

Packages

No packages published

Contributors 2

  •  
  •