RTL provides type-safe run-time reflection for modern C++ – combining compile-time guarantees with controlled run-time flexibility.
It enables name-based discovery and invocation of functions, constructors, and objects through a non-intrusive, type-safe reflection system that remains close to native execution.
For example, imagine you’ve written a simple function,
std::string complexToStr(float real, float img);Using RTL, discover it by name and call 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 includes. No compile-time linking. No argument type-casting. No guesswork. Just run-time lookup and type-safe invocation.
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 is registered and resolved lazily. Reflection introduces no runtime cost beyond the features explicitly exercised by the user.
-
Deterministic Lifetimes – Automatic ownership tracking of
HeapandStackinstances with zero hidden deep copies. -
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, passing all type information directly to its constructor –
auto cxx_mirror = rtl::CxxMirror({
// 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 methods -
rtl::type().member<Person>().method("setAge").build(&Person::setAge),
rtl::type().member<Person>().method("getName").build(&Person::getName)
});The cxx_mirror object is your gateway to runtime reflection – it lets you query, introspect, and even instantiate types without any compile-time knowledge. It can live anywhere – in any translation unit, quietly resting in a corner of your codebase, remaining dormant until first access. All you need is to expose the cxx_mirror wherever reflection is required.
And what better way to do that than a Singleton,
(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 all types here... */
});
return cxx_mirror;
}Singleton ensures one central registry, initialized once, accessible everywhere. No static coupling, no multiple instances, just clean runtime reflection.
RTL in action:
#include <rtl_access.h> // Reflection access interface.
#include "MyReflection.h"
int main()
{
// lookup class `Person` by name (given at registration time).
std::optional<rtl::Record> classPerson = cxx::mirror().getRecord("Person");
if (!classPerson) { return 0; } // Class not registered.rtl::CxxMirror provides two lookup APIs that return reflection metadata objects: rtl::Record for class/struct, 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 default constructor:
rtl::constructor<> personCtor = classPerson->ctor();Or the overloaded constructor Person(std::string, int) -
rtl::constructor<std::string, int> personCtor = classPerson->ctor<std::string, int>();
if (!personCtor) { return 0; } // Constructor with expected signature not found.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) { return 0; } // 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.
Similarly, member-function callers can be materialized:
std::optional<rtl::Method> oSetAge = classPerson->getMethod("setAge");
if (!oSetAge) { return 0; } // Method not found.
rtl::method<Person, void(int)> setAge = oSetAge->targetT<Person>()
.argsT<int>().returnT<void>();
if (setAge) {
Person person;
setAge(person)(47);
}The above setAgeinvocation 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.
// Lookup reflected method `getName`.
std::optional<rtl::Method> oGetName = classPerson->getMethod("getName");
if (!oGetName) { return 0; } // Method not found.
// Materialize erased method: std::string Person::getName().
rtl::method<rtl::RObject, std::string()> getName = oGetName->targetT()
.argsT().returnT<std::string>();
if (getName) {
auto [err, opt_ret] = getName(robj)(); // Invoke and receive return as std::optional<std::string>.
if (err == rtl::error::None && opt_ret.has_value()) {
std::string name = opt_ret->get();
std::cout << name;
}
}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();
if (getName) {
auto [err, ret] = getName(robj)(); // Invoke and receive return value std::string wrapped in rtl::RObject.
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.
}
}
return 0;
}At a high level, every registered C++ type is encapsulated as an rtl::Record. Callable entities (constructors, free functions, and member functions) are materialized through rtl::Function and rtl::Method, all of which are discoverable via rtl::CxxMirror.
RTL provides the following callable wrappers, 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, each callable returns an rtl::error along with the result, which is wrapped either in rtl::RObject (for type-erased returns) or in std::optional<T> when the return type is known at compile time.
-
Heap (
alloc::Heap) – objects are owned by an internalstd::unique_ptrand destroyed when theirrtl::RObjectwrapper goes out of scope. -
Stack (
alloc::Stack) – independent copies behave like normal stack values and clean up at scope exit. -
Move semantics –
Heapobjects followstd::unique_ptrrules (move transfers ownership, copy/assign disabled).Stackobjects move like regular values. -
Return values – All returns are propagated back wrapped in
rtl::RObject, cleaned up automatically at scope exit.
RTL doesn’t invent a new paradigm – it extends C++ itself. You create objects, call methods, and work with types as usual, but now safely at run-time.
-
✅ 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 method 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 without hidden costs. For example: treat an
intas achar, or astd::stringas astd::string_view/const char*— with no hidden copies and only safe, non-widening POD conversions. (In Progress) -
🟨 Materialize New Types – Convert a reflected type
Ainto typeBif they are implicitly convertible. Define custom conversions at registration to make them available automatically. (In Progress) -
🚧 STL Wrapper Support – Extended 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 intuitive conversions and overload resolution. (In Progress)
-
❌ Property Reflection: Planned.
-
❌ Enum Reflection: Planned.
-
❌ Composite Type Reflection: Planned.
-
❌ Inheritance Support: Planned.
Create a build directory in the project root folder:
mkdir build && cd buildGenerate a build system using Unix Makefiles or Visual Studio in CMake (use a compiler with C++20):
cmake -G "<Generator>"To build, use any IDE applicable to the generator, or build straight from CMake:
cmake --build .Run the RTLTestRunApp or RTLBenchmarkApp binaries generated in the bin/ directory. (Tested with MSVC 19, GCC 14, and Clang 19)
- See
CxxTestRegistration/src/MyReflectionTests/for introductory examples of type registration and reflective programming. - See
RTLTestRunApp/srcfor detailed test cases. - See
RTLBenchmarkApp/srcfor benchmarking implementations. - Run
run_benchmarks.shto perform automated benchmarking, from micro-level tests to scaled workloads.
Contributions welcome! Report bugs, request features, or submit PRs on GitHub.
GitHub issues or email at [email protected].
C++ joins the reflection party! — why should Java & .NET have all the fun?