C++ Runtime Reflection Implementation Overview with Code Examples
The article explains how to implement a runtime reflection system for C++ using the Ponder library, detailing type registration, property and function metadata, builder-generated meta‑objects, type‑erasure wrappers like UserObject and Value, and runtime APIs for dynamic object creation and invocation.
This article introduces how to add dynamic features to the static C++ language by using a reflection system based on the Ponder library. It explains the registration of types, properties and functions, and shows how the framework builds a runtime reflection layer on top of modern C++ features.
1. Simple Example
//-------------------------------------
//register code
//-------------------------------------
using namespace framework;
using namespace framework::reflection;
using namespace framework::math;
__register_type
("Vector3")
.constructor()
.constructor
()
.property("x", &Vector3::x)
.property("y", &Vector3::y)
.property("z", &Vector3::z)
.function("Length", &Vector3::Length)
.function("Normalise", &Vector3::Normalise)
.overload(
"operator*", [](Vector3* caller, Real val) { return caller->operator*(val); },
[](Vector3* caller, const Vector3& val) { return caller->operator*(val); });
//-------------------------------------
//use code
//-------------------------------------
auto* metaClass = __type_of
();
ASSERT_TRUE(metaClass != nullptr);
auto obj = runtime::CreateWithArgs(*metaClass, Args{1.0, 2.0, 3.0});
ASSERT_TRUE(obj != UserObject::nothing);
auto obj2 = runtime::CreateWithArgs(*metaClass, Args{1.0});
ASSERT_TRUE(obj2 == UserObject::nothing);
const reflection::Property* fieldX = nullptr;
metaClass->TryGetProperty("x", fieldX);
ASSERT_TRUE(fieldX != nullptr);
const reflection::Property* fieldY = nullptr;
metaClass->TryGetProperty("y", fieldY);
ASSERT_TRUE(fieldY != nullptr);
const reflection::Property* fieldZ = nullptr;
metaClass->TryGetProperty("z", fieldZ);
ASSERT_TRUE(fieldZ != nullptr);
double x = fieldX->Get(obj).to
();
ASSERT_DOUBLE_EQ(1.0, x);
fieldX->Set(obj, 2.0);
x = fieldX->Get(obj).to
();
ASSERT_DOUBLE_EQ(2.0, x);
fieldX->Set(obj, 1.0);
x = fieldX->Get(obj).to
();
double y = fieldY->Get(obj).to
();
double z = fieldZ->Get(obj).to
();
ASSERT_DOUBLE_EQ(1.0, x);
ASSERT_DOUBLE_EQ(2.0, y);
ASSERT_DOUBLE_EQ(3.0, z);
const reflection::Function* lenfunc = nullptr;
metaClass->TryGetFunction("Length", lenfunc);
ASSERT_TRUE(lenfunc != nullptr);
const reflection::Function* normalizeFunc = nullptr;
metaClass->TryGetFunction("Normalise", normalizeFunc);
ASSERT_TRUE(normalizeFunc != nullptr);
// Overload tests
auto& tmpVec3 = obj.Ref
();
const reflection::Function* overFunc = nullptr;
metaClass->TryGetFunction("operator*", overFunc);
Value obj3 = runtime::CallStatic(*overFunc, Args{obj, 3.0});
auto& tmpVec4 = obj3.ConstRef
().Ref
();
ASSERT_DOUBLE_EQ(3.0, tmpVec4.x);
ASSERT_DOUBLE_EQ(6.0, tmpVec4.y);
ASSERT_DOUBLE_EQ(9.0, tmpVec4.z);
Value obj4 = runtime::CallStatic(*overFunc, Args{obj, Vector3(2.0, 2.0, 2.0)});
auto& tmpVec5 = obj4.ConstRef
().Ref
();
ASSERT_DOUBLE_EQ(2.0, tmpVec5.x);
ASSERT_DOUBLE_EQ(4.0, tmpVec5.y);
ASSERT_DOUBLE_EQ(6.0, tmpVec5.z);The code demonstrates how the framework registers a C++ class ( Vector3 ) with its properties ( x , y , z ) and member functions ( Length , Normalise , and an overloaded operator* ). It also shows how to create objects at runtime, access and modify properties, and invoke functions dynamically.
2. Implementation Overview
The reflection system consists of several key modules (highlighted in the diagram):
meta : Packages the reflection information.
traits : Performs compile‑time type introspection (e.g., extracting return types and parameter types of functions).
type_erasure : Provides a uniform runtime interface so that any object can be accessed via a generic Value or UserObject .
builder : Generates the meta information from the original class in a non‑intrusive way (compiler‑time → runtime bridge).
runtime : Offers user‑friendly APIs such as runtime::createWithArgs() for object construction.
These components work together to supply the missing runtime type information that C++ normally discards, enabling features like serialization, scripting bindings, and generic algorithms.
3. Meta Implementation – Class (C#‑like Representation)
class Class : public Type {
size_t m_sizeof; // Size of the class in bytes.
TypeId m_id; // Unique type id of the metaclass.
Id m_name; // Name of the metaclass
FunctionTable m_functions; // Meta functions indexed by name
FunctionIdTable m_functions_by_id; // Functions indexed by numeric id
PropertyTable m_properties; // Meta properties indexed by ID
FunctionTable m_static_properties; // Read‑only static properties
BaseList m_bases; // List of base metaclasses
ConstructorList m_constructors; // List of constructors
Destructor m_destructor; // Destructor for abstract objects
UserObjectCreator m_userObjectCreator; // Convert pointer to UserObject
// ... public reflection API ...
const Class& base(size_t index) const;
size_t constructorCount() const;
const Constructor* constructor(size_t index) const;
void destruct(const UserObject& uobj, bool destruct) const;
const Function& function(size_t index) const;
const Function& function(IdRef name) const;
const Function* function_by_id(uint64_t funcId) const;
bool tryFunction(const IdRef name, const Function*& funcRet) const;
bool tryStaticProperty(const IdRef name, const Function*& funcRet) const;
size_t propertyCount() const;
bool hasProperty(IdRef name) const;
const Property& property(size_t index) const;
const Property& property(IdRef name) const;
bool tryProperty(const IdRef name, const Property*& propRet) const;
size_t sizeOf() const;
UserObject getUserObjectFromPointer(void* ptr) const;
bool has_meta_attribute(const IdRef attrName) const noexcept;
const Value* get_meta_attribute(const IdRef attrName) const noexcept;
std::string get_meta_attribute_as_string(const std::string_view attrName) const noexcept;
};The Class object stores all meta‑information about a C++ type, including its constructors, properties, functions, and inheritance hierarchy.
4. Function Implementation
class Function : public Type {
public:
IdReturn name() const;
uint64_t id() const;
FunctionKind kind() const { return m_funcType; }
ValueKind returnType() const;
policy::ReturnKind returnPolicy() const { return m_returnPolicy; }
virtual size_t paramCount() const = 0;
virtual ValueKind paramType(size_t index) const = 0;
virtual std::string_view paramTypeName(size_t index) const = 0;
virtual TypeId paramTypeIndex(size_t index) const = 0;
virtual TypeId returnTypeIndex() const = 0;
virtual bool argsMatch(const Args& arg) const = 0;
protected:
Function(IdRef name);
Id m_name; // Function name
uint64_t m_id; // Unique id
FunctionKind m_funcType;
ValueKind m_returnType;
policy::ReturnKind m_returnPolicy;
const void* m_usesData;
};Functions are abstract; concrete implementations are generated by the builder based on the original C++ callable.
5. Property Implementation
class Property : public Type {
public:
IdReturn name() const;
ValueKind kind() const;
virtual bool isReadable() const = 0;
virtual bool isWritable() const = 0;
Value get(const UserObject& object) const;
void set(const UserObject& object, const Value& value) const;
protected:
virtual Value getValue(const UserObject& object) const = 0;
virtual void setValue(const UserObject& object, const Value& value) const = 0;
Id m_name; // Property name
ValueKind m_type; // Property type
TypeId m_typeIndex;
PropertyImplementType m_implement_type = PropertyImplementType::Unknow;
};Like Function , Property is a virtual base; the builder creates concrete subclasses that know how to read/write the underlying C++ member.
6. Traits – Core Compile‑Time Tools
Traits such as TypeTraits , FunctionTraits , ArrayTraits , and IsSmartPointer provide compile‑time information about pointers, references, arrays, and smart pointers. They are heavily used by the builder to generate the meta structures.
template
struct TypeTraits
{
static constexpr ReferenceKind kind = ReferenceKind::Pointer;
using Type = T*;
using ReferenceType = T*;
using PointerType = T*;
using DereferencedType = T;
static constexpr bool isWritable = !std::is_const
::value;
static constexpr bool isRef = true;
static inline ReferenceType get(void* pointer) { return static_cast
(pointer); }
static inline PointerType getPointer(T& value) { return &value }
static inline PointerType getPointer(T* value) { return value; }
};These traits enable the framework to treat any type uniformly at runtime.
7. UserObject – Runtime Wrapper for Instances
class UserObject {
public:
template
static UserObject makeRef(T& object);
template
static UserObject makeRef(T* object);
template
static UserObject makeCopy(const T& object);
template
static UserObject makeOwned(T&& object);
// ... constructors, assignment, conversion helpers ...
Value get(IdRef property) const;
void set(IdRef property, const Value& value) const;
const Class& getClass() const;
void* pointer() const;
static const UserObject nothing;
};UserObject holds an instance of any registered type and provides generic get / set operations through the meta information.
8. Value – Variant for All Supported Types
using Variant = std::variant<
NoType,
bool,
int64_t,
double,
reflection::String,
EnumObject,
UserObject,
ArrayObject,
detail::BuildInValueRef
>;
class Value {
Variant m_value; // Stored value
ValueKind m_type; // Ponder type tag
public:
ValueKind kind() const;
template
T to() const;
// ... reference/const‑reference access, compatibility checks ...
static const Value nothing;
};The Value class is a thin wrapper around std::variant that can hold any type supported by the reflection system.
9. Runtime Helper Functions
// Object creation
template
static inline UserObject create(const Class& cls, A... args);
static inline UserObject createWithArgs(const Class& cls, Args&& args);
// Function invocation
template
static inline Value call(const Function& fn, const UserObject& obj, A&&... args);
static inline Value callStatic(const Function& fn, A&&... args);These helpers make it easy to construct objects and invoke functions without dealing with the low‑level meta API.
10. Future Thoughts
The article ends with a discussion about extending the system to support compile‑time polymorphism similar to Rust traits, mentioning concepts like dyno::poly and the need for a unified static‑and‑dynamic polymorphic model.
Conclusion
By leveraging modern C++ features (templates, constexpr, concepts) and the Ponder library, the reflection framework provides a concise yet powerful way to add runtime introspection and dynamic behavior to C++ applications. The implementation demonstrates how compile‑time type analysis (traits) can be combined with type erasure to achieve a flexible, generic runtime API.
Tencent Cloud Developer
Official Tencent Cloud community account that brings together developers, shares practical tech insights, and fosters an influential tech exchange community.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.