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.
-
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.
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;
}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.
}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).
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 examplesRTLBenchmarkApp– Performance benchmarks
Additional resources:
RTLTestRunApp/src– Detailed test casesRTLTestRunApp/src/MyReflectionTests/– Tutorial exampleRTLBenchmarkApp/src– Benchmark implementationsrun_benchmarks.sh– Automated benchmark runs
-
✅ 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
HeaporStackallocation. - Automatic move semantics for ownership transfers.
- Scope-based destruction for
Heapallocated instances.
- Choose between
-
✅ 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::errordiagnostics for all reflection operations (no exceptions, no silent failures). -
✅ Smart Pointer Reflection – Reflect
std::shared_ptrandstd::unique_ptr, transparently access the underlying type, with full sharing and cloning semantics. -
🟨 Conservative Conversions – Safely reinterpret reflected values. For example: treat an
intas achar, or astd::stringas astd::string_view/const char*(In Progress) -
🚧 STL Wrapper Support – support for wrappers like
std::optionalandstd::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.
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.
C++ joins the reflection party! – why should Java have all the fun?