Fundamentals 16 min read

Understanding Property Registration and Runtime Access in a C++ Reflection Framework

The article dissects the Ponder C++ reflection library’s Property subsystem, explaining how registration via ClassBuilder creates property entries, how traits and ValueBinders map members to abstract Property objects, and how runtime Get/Set calls traverse templated binders to provide type‑safe, high‑performance access.

Tencent Cloud Developer
Tencent Cloud Developer
Tencent Cloud Developer
Understanding Property Registration and Runtime Access in a C++ Reflection Framework

This article provides an in‑depth exploration of the Property subsystem within a C++ reflection library (the Ponder library). Compared with simple reflection function implementations, Property involves more complex tag dispatch and intermediate steps, so the article walks through the implementation step by step, using concrete examples.

1. Property example code

//-------------------------------------</code>
<code>//register code</code>
<code>//-------------------------------------</code>
<code>__register_type<Vector3>("Vector3")
    .constructor()
    .constructor<double, double, double>()
    .property("x", &Vector3::x)
    .property("y", &Vector3::y)
    .property("z", &Vector3::z);
</code>
<code></code>
<code>//-------------------------------------</code>
<code>//use code</code>
<code>//-------------------------------------</code>
<code>auto* metaClass = __type_of<framework::math::Vector3>();</code>
<code>ASSERT_TRUE(metaClass != nullptr);</code>
<code></code>
<code>auto obj = runtime::CreateWithArgs(*metaClass, Args{1.0, 2.0, 3.0});</code>
<code>ASSERT_TRUE(obj != UserObject::nothing);</code>
<code></code>
<code>const reflection::Property* fieldX = nullptr;</code>
<code>metaClass->TryProperty("x", fieldX);</code>
<code>ASSERT_TRUE(fieldX != nullptr);</code>
<code>double x = fieldX->Get(obj).To<double>();</code>
<code>ASSERT_DOUBLE_EQ(1.0, x);</code>
<code>fieldX->Set(obj, 2.0);</code>
<code>x = fieldX->Get(obj).to<double>();</code>
<code>ASSERT_DOUBLE_EQ(2.0, x);

The code is divided into two parts: registration (using __register_type) and usage (retrieving the MetaClass, accessing the property via TryProperty, and calling Get / Set).

2. Registration code details

The registration is performed through the ClassBuilder returned by __register_type<T>(). The property(name, accessor) function adds a Property to the class’s property table.

template<typename T>
template<typename F>
ClassBuilder<T>& ClassBuilder<T>::property(IdRef name, F accessor) {
    if (target_->properties_table_.find(name.data()) == target_->properties_table_.end()) {
        return AddProperty(detail::PropertyFactory1<T, F>::Create(name, accessor));
    } else {
        current_type_ = const_cast<Property*>(&(target_->GetProperty(name)));
        return *this;
    }
}

A second overload accepts two accessors (getter and setter) and creates the property via PropertyFactory2.

template<typename T>
template<typename F1, typename F2>
ClassBuilder<T>& ClassBuilder<T>::property(IdRef name, F1 accessor1, F2 accessor2) {
    if (target_->properties_table_.find(name.data()) == target_->properties_table_.end()) {
        return AddProperty(detail::PropertyFactory2<T, F1, F2>::Create(name, accessor1, accessor2));
    } else {
        current_type_ = const_cast<Property*>(&(target_->GetProperty(name)));
        return *this;
    }
}

3. Core runtime mechanism – the Property class

All runtime properties inherit from an abstract Property class that provides a uniform interface:

class Property : public Type {
public:
    IdReturn name() const;
    ValueKind kind() const;
    virtual bool IsReadable() const;
    virtual bool IsWritable() const;
    Value Get(const UserObject& object) const;
    void Set(const UserObject& object, const Value& value) const;
    // ... type erasure helpers ...
protected:
    virtual Value GetValue(const UserObject& object) const = 0;
    virtual void SetValue(const UserObject& object, const Value& value) const = 0;
};

The most used methods are Get() and Set(), which delegate to the concrete implementation.

4. Value binders – linking a property to actual C++ members

The ValueBinder template bridges the abstract Property with a concrete member (field or function). It uses traits to decide whether the property is writable.

template<class C, typename PropTraits>
class ValueBinder {
public:
    using ClassType = C;
    using AccessType = typename std::conditional<PropTraits::kIsWritable,
        typename PropTraits::AccessType&, typename PropTraits::AccessType>::type;
    using SetType = typename std::remove_reference<AccessType>::type;
    using Binding = typename PropTraits::template TBinding<ClassType, AccessType>;
    ValueBinder(const Binding& b) : bound_(b) {}
    AccessType Getter(ClassType& c) const { return bound_.Access(c); }
    bool Setter(ClassType& c, SetType v) const {
        if constexpr (PropTraits::kIsWritable) return (bound_.Access(c) = v), true;
        else return false;
    }
    // ... GetValue / SetValue wrappers ...
protected:
    Binding bound_;
};
ValueBinder2

extends ValueBinder by allowing an external function object to perform the set operation.

template<class C, typename PropTraits>
class ValueBinder2 : public ValueBinder<C, PropTraits> {
    using Base = ValueBinder<C, PropTraits>;
public:
    template<typename S>
    ValueBinder2(const typename Base::Binding& g, S s) : Base(g), set_(s) {}
    bool Setter(typename Base::ClassType& c, typename Base::SetType v) const { set_(c, v); return true; }
    // ... overload for Value ...
protected:
    std::function<void(typename Base::ClassType&, typename Base::AccessType)> set_;
};

Both binders rely on PropTraits (e.g., TMemberTraits or TFunctionTraits) which describe the member type, return type, and provide a TBinding that actually accesses the member.

5. Trait extraction – TFunctionTraits and TMemberTraits

These traits decompose a function or member pointer into:

Parameter types

Return type

Dispatch type for std::function Kind (MemberObject vs Function)

The traits also expose the appropriate ValueBinder (or ValueBinder2) to be used by the property implementation.

6. Property factories – creating concrete property objects PropertyFactory1<T, F>::Create builds a SimplePropertyImpl that wraps a GetSet1 (single accessor) pipeline. PropertyFactory2 does the same with GetSet2 (separate getter and setter).

The creation flow can be visualised as:

Deduce the correct Accessor and PropertyImpl from the class type C and the trait PropTraits using GetSet1 / GetSet2.

Instantiate the appropriate ValueBinder (or ValueBinder2) based on writability.

Construct the concrete implementation ( SimplePropertyImpl, EnumPropertyImpl, etc.).

7. Concrete property implementations

Examples include: SimplePropertyImpl – handles ordinary member objects. EnumPropertyImpl, ArrayPropertyImpl, UserPropertyImpl – similar structure, specialised for enums, arrays, or user‑defined types.

template<typename A>
class SimplePropertyImpl : public SimpleProperty {
public:
    SimplePropertyImpl(IdRef name, A accessor);
protected:
    bool IsReadable() const final { return true; }
    bool IsWritable() const final { return true; }
    Value GetValue(const UserObject& object) const final {
        return Value{accessor_.interface_.Getter(object.get<typename A::ClassType>())};
    }
    void SetValue(const UserObject& object, const Value& value) const final {
        if (!accessor_.interface_.Setter(object.Ref<typename A::ClassType>(), value.to<typename A::DataType>()))
            PONDER_ERROR(ForbiddenWrite(name()));
    }
private:
    A accessor_; // bridges to the actual C++ member
};

8. Runtime get/set process – call‑stack walk‑through

The article walks through a concrete call stack when retrieving Vector3::x:

// level 1
framework::reflection::detail::SimplePropertyImpl<
    framework::reflection::detail::GetSet1<
        framework::math::Vector3,
        framework::reflection::detail::TMemberTraits<double framework::math::Vector3::*>
    >
>::GetValue(const framework::reflection::UserObject& object);

// level 2
framework::reflection::detail::ValueBinder<
    framework::math::Vector3,
    framework::reflection::detail::TMemberTraits<double framework::math::Vector3::*>
>::Getter(framework::math::Vector3& c);

// level 3
framework::reflection::detail::TMemberTraits<double framework::math::Vector3::*>::
    TBinding<framework::math::Vector3, double&>::Access(framework::math::Vector3& c);

Each level corresponds to a template instantiation that ultimately dereferences the member pointer and returns the value.

9. Summary

Through a cascade of template classes— GetSet, AccessTraits, ValueBinder, and concrete PropertyImpl —the framework achieves high‑performance, type‑safe runtime property access. The article also notes that while the current C++17 implementation works, a future rewrite using C++20 concepts could simplify the design.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

RuntimeC++Template MetaprogrammingProperty
Tencent Cloud Developer
Written by

Tencent Cloud Developer

Official Tencent Cloud community account that brings together developers, shares practical tech insights, and fosters an influential tech exchange community.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.