C + + reflection: analyze the implementation mechanism of ponder Library in simple terms!

Introduction | adding dynamic features to static languages seems to be a thing that everyone in the C + + community is happy to see, and there are many wheels. We won't list the wheels of various genres made by our predecessors one by one. It is mainly combined with the C + + reflection implementation used in our framework and the new features of C + + to systematically disassemble the reflection implementation in the current framework. In addition, the code originated from ponder, and the overall processing flow is basically consistent with the original, so the relevant source code can be directly referred to [ponder's original code]( https://github.com/billyquith/ponder) 

1, Simple example

//-------------------------------------//register code//-------------------------------------  using namespace framework;  using namespace framework::reflection;  using namespace framework::math;  __register_type<Vector3>("Vector3")      .constructor()      .constructor<Real, Real, Real>()      .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<framework::math::Vector3>();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<double>();ASSERT_DOUBLE_EQ(1.0, x);fieldX->Set(obj, 2.0);x = fieldX->Get(obj).to<double>();ASSERT_DOUBLE_EQ(2.0, x);fieldX->Set(obj, 1.0);x = fieldX->Get(obj).to<double>();
double y = fieldY->Get(obj).to<double>();double z = fieldZ->Get(obj).to<double>();
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 testsauto& tmpVec3 = obj.Ref<Vector3>();const reflection::Function* overFunc = nullptr;metaClass->TryGetFunction("operator*", overFunc);Value obj3 = runtime::CallStatic(*overFunc, Args{obj, 3.0});auto& tmpVec4 = obj3.ConstRef<UserObject>().Ref<Vector3>();
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<UserObject>().Ref<Vector3>();ASSERT_DOUBLE_EQ(2.0, tmpVec5.x);ASSERT_DOUBLE_EQ(4.0, tmpVec5.y);ASSERT_DOUBLE_EQ(6.0, tmpVec5.z);

The above code demonstrates the registration of C + + properties and methods in the framework, and how to dynamically set and obtain the properties of the object and call related methods. Finally, it also demonstrates how to call the overload method of the object. Let's not disassemble the specific code too carefully. Let's have a general impression and pay attention to the following points:

  • For the construction of reflective objects, we use the following code:
 runtime::createWithArgs(*metaClass, Args{ 1.0, 2.0, 3.0 }); ASSERT_TRUE(obj != UserObject::nothing);
  • For the acquisition of attributes, we use the following code:
const reflection::Property* fieldX = nullptr;  metaClass->tryProperty("x", fieldX);  ASSERT_TRUE(fieldX != nullptr);  Real x = fieldX->get(obj).to<Real>();
  • For function calls, we use the following code:
const reflection::Function* overFunc = nullptr;  metaClass->tryFunction("operator*", overFunc);  Value obj3 = runtime::callStatic(*overFunc, Args{obj, 3.0 });

There are several "magical" classes, Args, Value and UserObject. If you notice this, congratulations on paying attention to the core point of dynamic reflection implementation. To achieve runtime reflection, we must first achieve complete type "unification".

2, Implementation overview

The above figure is the engineering view of relation. We focus on the parts marked with blue numbers in the figure, and briefly describe their functions first:

  • meta: part of the package presentation of reflected information.
  • traits: used for type extraction of compiler time. Through the relevant code, we can easily analyze the members and functions of the class at compile time, such as obtaining the return value type and parameter type of the function.
  • type_erasure: in order to have a unified interface for the use of reflection, we must provide a basic type erasure container, so that we can dynamically call the interface of an object or obtain the properties of an object at runtime.
  • builder: meta information is not automatic. We need a means to build relevant meta information from the original class (non intrusive).
  • runtime: provides more friendly interfaces than meta, such as the runtime::createWithArgs() method seen above.

The general relationship between them is shown in the figure above:

  • traits´╝îtype_erasure: provides the most core functional support.
  • Builder: complete the analysis of the original class by Compiler time and the output of meta information. In fact, it is the bridge of Compiler time - > runtime, which is also equivalent to the whole reflection scaffold. We complete the composition of reflected meta information through the implementation of builder.
  • meta, Runtime: complete the expression of runtime information and provide relevant use interfaces together.

Let's take a specific look at the implementation of each part.

3, Meta implementation - expression of class C#

When talking about the runtime system, we first think of the rtti of the weak chicken. Even if the function is weak, many places are disabled, and the information and related facilities that can be obtained at runtime are very simple.

Another point to mention is that C + + uses the design method with complete compiler time types, and the runtime types will be collapsed. The slang in the circle is "good at streaking".

Therefore, if we want to add the ability of dynamic reflection to C + +, the first thing we need to do is to find a way to supplement it with enough runtime information, that is, various meta data similar to C #, and then we can happily use the dynamic features to develop some more universal functions of serialization, deserialization and cross language support.

So what information is needed to create an operation object at runtime? We give the answer directly:

In fact, the design of the whole reflection meta part is to meet the above-mentioned requirements and supplement various meta information related to the types required at runtime.

After understanding the part of meta information, let's take a look at the specific implementation combined with the code.

(1) reflection::Class

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;             // Table of meta functions indexed by name
    FunctionIdTable          m_functions_by_id;   // Table for meta functions indexed by number id    PropertyTable                   m_properties;            // Table of meta properties indexed by ID    //Here use function to implement read only static properties, to get a simple implement    FunctionTable                   m_static_properties;     // Table of meta static properties indexed by ID
    BaseList                        m_bases;                 // List of base metaclasses    ConstructorList                 m_constructors;          // List of metaconstructors    Destructor                      m_destructor;            // Destructor (function able to delete an abstract object)    UserObjectCreator               m_userObjectCreator;     // Convert pointer of class instance to UserObject
    ClassUserdata                   m_userdata;    public: // reflection    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 specific implementation is basically one-to-one corresponding to the previous figure, which will not be described in detail here.

(2) reflection::Function

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:    // FunctionImpl inherits from this and constructs.    Function(IdRef name);        Id m_name;                          // Name of the function    uint64_t m_id;            // Id of the function    FunctionKind m_funcType;            // Kind of function    ValueKind m_returnType;             // Runtime return type    policy::ReturnKind m_returnPolicy;  // Return policy    const void* m_usesData;};

Note that the Function here is a virtual implementation. Later, we will see that the real Function is built by the builder.

Another point is that meta function does not directly give the Invoke method as c# does. This is because the function of type erasure is different for different applications. For example, for lua, the prototype of type erasure function is lua_CFunction. For C + +, it is:


The advantage of different unified types on different occasions is that there is no need for wrappers and no additional performance overhead, but it will also cause trouble in the use of peripherals. Here, some adjustments may need to be made according to the actual situation of the project.

(3) reflection::Property

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;protected:    virtual Value getValue(const UserObject& object) const = 0;  virtual void setValue(const UserObject& object, const Value& value) const = 0;protected:    Id m_name; // Name of the property    ValueKind m_type; // Type of the property    TypeId m_typeIndex;
    PropertyImplementType m_implement_type = PropertyImplementType::Unknow;};

Like reflection::Function, this class is also a virtual base class. The builder will be responsible for the implementation of specific properties. The real specialization is mainly the two virtual interfaces getValue(), setValue(), and the external directly uses the get(), set() interface to obtain and set the corresponding properties.

(4) static property

The implementation of static property is relatively simple. At present, it is implemented indirectly by Function, and only static variables of read only type are provided. This place will not be expanded in detail.

4, traits implementation - basic tools

This part of the implementation is also the part with obvious "subtraction" effect after the introduction of new C + + features, and it is also the basis of type erasure. There are two core parts, TypeTraits and FunctionTraits. We mainly focus on understanding their functions and uses, ignoring the implementation part. With the advent of c++20 and the introduction of concept, this part can be further simplified. At present, it is mainly through the SIFINEA feature of C + + to complete the relevant derivation and implementation. It is more about dealing with the relevant code in detail. It feels limited to understand the relevant specific implementation value, so we don't do too detailed development.

(1) TypeTraits

TypeTraits is mainly used to remove modifiers such as Pointer and Reference of a type and obtain the original type. In addition, we can also quickly obtain the expected type of data through its get(), getPointer() function.

template <typename T>struct TypeTraits<T*>{static constexpr ReferenceKind kind = ReferenceKind::Pointer;using Type = T*;using ReferenceType = T*;using PointerType = T*;using DereferencedType = T;using DataType = typename DataType<T>::Type;static constexpr bool isWritable = !std::is_const<DereferencedType>::value;static constexpr bool isRef = true;
static inline ReferenceType get(void* pointer) { return static_cast<T*>(pointer); }static inline PointerType getPointer(T& value) { return &value; }static inline PointerType getPointer(T* value) { return value; }};
  • DataType<>

In TypeTraits, the DataType of type T is indirectly obtained through DataType < > (remove *, & modified types). The implementation of DataType is as follows:

template <typename T, typename E = void>struct DataType{    using Type = T;};
// consttemplate <typename T>struct DataType<const T> : public DataType<T> {};
template <typename T>struct DataType<T&> : public DataType<T> {};
template <typename T>struct DataType<T*> : public DataType<T> {};
template <typename T, size_t N>struct DataType<T[N]> : public DataType<T> {};
// smart pointertemplate <template <typename> class T, typename U>    struct DataType<T<U>, typename std::enable_if<IsSmartPointer<T<U>, U>::value >::type>    {        using Type = typename DataType<U>::Type;    };
  • Examples
using rstudio::reflection::detail::TypeTraits;using rstudio::reflection::ReferenceKind;
ASSERT_TRUE(TypeTraits<int>::kind == ReferenceKind::Instance);ASSERT_TRUE(TypeTraits<float>::kind == ReferenceKind::Instance);
ASSERT_TRUE(TypeTraits<int*>::kind == ReferenceKind::Pointer);ASSERT_TRUE(TypeTraits<float*>::kind == ReferenceKind::Pointer);
ASSERT_TRUE(TypeTraits<float&>::kind == ReferenceKind::Reference);ASSERT_TRUE(TypeTraits<Methods&>::kind == ReferenceKind::Reference);
ASSERT_TRUE(TypeTraits<std::shared_ptr<Methods>>::kind == ReferenceKind::SmartPointer);

(2) FunctionTraits

FunctionTraits is mainly used to complete the derivation of function types during compilation and obtain its return value types, parameter types and other information. Common in various cross language middle tier implementations, such as Lua's various bridge s, are also the starting point of function type erasure. I need to know the information of the function itself before I can further make type erasure. Let's start with an example:

/* * Specialization for native callable types (function and function pointer types) *  - We cannot derive a ClassType from these as they may not have one. e.g. int get() */template <typename T>struct FunctionTraits<T,typename std::enable_if<std::is_function<typename std::remove_pointer<T>::type>::value>::type>{    static constexpr FunctionKind kind = FunctionKind::Function;    using Details = typename function::FunctionDetails<typename std::remove_pointer<T>::type>;    using BoundType = typename Details::FuncType;    using ExposedType = typename Details::ReturnType;    using ExposedTraits = TypeTraits<ExposedType>;    static constexpr bool isWritable = std::is_lvalue_reference<ExposedType>::value        && !std::is_const<typename ExposedTraits::DereferencedType>::value;    using AccessType = typename function::ReturnType<typename ExposedTraits::DereferencedType, isWritable>::Type;    using DataType = typename ExposedTraits::DataType;    using DispatchType = typename Details::DispatchType;
    template <typename C, typename A>    class Binding    {        public:        using ClassType = C;        using AccessType = A;
        Binding(BoundType d) : data(d) {}
        AccessType access(ClassType& c) const { return (*data)(c); }        private:        BoundType data;    };};

This is the fucntiontraits < > specialization of function and function pointer. The specialization of other function types provides basically the same capabilities as this specialization, mainly including the following information:

  • kind: the type of function is actually the specialized implementation of FunctionTraits. See the following for details.
  • Details: the specific information of the function, such as return value type, parameter table tuple < >, are stored in it.
  • BoundType: function type.
  • ExposedType: return value type.
  • Exposedtraits: TypeTraits of exposedtype.
  • Datatype: data type of exposedtype.
  • DispatchType: used in conjunction with std::function < > as a template parameter of std::function, so that an std::function < > object matching the original Function type can be constructed.

Several key attributes are described in detail below:

  • FunctionTraits<>::kind

The FunctionKind enumeration mainly has the following values:

/** * \brief Enumeration of the kinds of function recognised * * \sa Function */enum class FunctionKind{    None,               ///< not a function    Function,           ///< a function    MemberFunction,     ///< function in a class or struct    FunctionWrapper,    ///< `std::function<>`    BindExpression,     ///< `std::bind()`    Lambda              ///< lambda function `[](){}`};
  • FunctionTraits<>::Detatils

Details itself is also a template type, including two implementations: functiondetails < > and methoddetails < >. Their main members are basically the same. Methoddetails < > is for the member function of the class, so there will be an additional ClassType. The difference between the two is very small. We only list the code of functiondetails < >:

template <typename T>struct FunctionDetails {};
template <typename R, typename... A>struct FunctionDetails<R(*)(A...)>{    using ParamTypes = std::tuple<A...>;    using ReturnType = R;    using FuncType = ReturnType(*)(A...);    using DispatchType = ReturnType(A...);    using FunctionCallTypes = std::tuple<A...>;};
template <typename R, typename... A>struct FunctionDetails<R(A...)>{    using ParamTypes = std::tuple<A...>;    using ReturnType = R;    using FuncType = ReturnType(*)(A...);    using DispatchType = ReturnType(A...);    using FunctionCallTypes = std::tuple<A...>;};

The implementation is relatively simple. The main members are:

  • ParamTypes: tuple containing all parameters of the function < >.
  • ReturnType: the return value type of the function.
  • FuncType: function pointer type.
  • DispatchType: used together with std::function < > as the template parameter of std::function, so that a Function object matching the original Function type can be constructed.
  • FunctionCallTypes: the same as ParamTypes. Note that there is a difference between MethodDetails < >. ParamTypes does not contain the class itself, and the first parameter of MethodDetails is the class itself.

Use example:

using rstudio::reflection::detail::FunctionTraits;using rstudio::reflection::PropertyKind;using rstudio::reflection::FunctionKind;
ASSERT_TRUE(FunctionTraits<void(void)>::kind == FunctionKind::Function);
ASSERT_TRUE(FunctionTraits<void(int)>::kind == FunctionKind::Function);
ASSERT_TRUE(FunctionTraits<int(void)>::kind == FunctionKind::Function);
ASSERT_TRUE(FunctionTraits<int(char*)>::kind == FunctionKind::Function);
// non-class void(void)ASSERT_TRUE(FunctionTraits<decltype(func)>::kind == FunctionKind::Function);
// non-class R(...)ASSERT_TRUE(FunctionTraits<decltype(funcArgReturn)>::kind == FunctionKind::Function);
// class static R(void)ASSERT_TRUE(FunctionTraits<decltype(&Class::staticFunc)>::kind == FunctionKind::Function);

The unit test code of FunctionTraits and the trails of various types of functions.

(3) ArrayTraits

The original array traits of res ponder are mainly completed through ArrayMapper. ArrayMapper is used in ArrayPropertyImpl. Like other Traits, let's take a look at ArrayMapper with the specialized implementation of STD:: vector < >:

template <typename T>struct ArrayMapper<std::vector<T> >{    static constexpr bool isArray = true;    using ElementType = T;
    static constexpr bool dynamic(){        return true;    }
    static size_t size(const std::vector<T>& arr){        return arr.size();    }
    static const T& get(const std::vector<T>& arr, size_t index){        return arr[index];    }
    static void set(std::vector<T>& arr, size_t index, const T& value){        arr[index] = value;    }
    static void insert(std::vector<T>& arr, size_t before, const T& value){        arr.insert(arr.begin() + before, value);    }
    static void remove(std::vector<T>& arr, size_t index){        arr.erase(arr.begin() + index);    }
    static void resize(std::vector<T>& arr, size_t totalsize){        arr.resize(totalsize);    }
    static constexpr bool support_raw_pointer(){        return true;    }
    static void* ptr(std::vector<T>& arr){        return arr.data();    }};

Array mapper provides a unified interface to operate various types of arrays through various versions of specialized implementations, such as:

  • size()
  • get()
  • set()
  • insert()
  • Resize, etc

In addition, for a type, we can also simply judge whether it is an array through arraymapper < T >:: isarray.

(4) Other Traits

  • IsSmartPointer<>
template <typename T, typename U>struct IsSmartPointer{    static constexpr bool value = false;};
template <typename T, typename U>struct IsSmartPointer<std::unique_ptr<T>, U>{    static constexpr bool value = true;};
template <typename T, typename U>struct IsSmartPointer<std::shared_ptr<T>, U>{    static constexpr bool value = true;};

Judge whether a type is smart pointer (you can consider using concept directly).

  • get_pointer()
template<class T>    T* get_pointer(T* p){    return p;}
template<class T>    T* get_pointer(std::unique_ptr<T> const& p){    return p.get();}
template<class T>    T* get_pointer(std::shared_ptr<T> const& p){    return p.get();}

Provide a unified interface for smart pointer and raw pointer to obtain raw pointer.

5, type_erasure implementation -- unified appearance

Let's review the two interfaces of Property introduced in the Meta section:

virtual Value getValue(const UserObject& object) const = 0;virtual void setValue(const UserObject& object, const Value& value) const = 0;

It's easy to see the UserObject and Value, which are basically the whole reflection type_erasure implements the two core objects. UserObject is used to uniformly express objects created through MetaClass, and Value is similar to variables, which is used to uniformly express all types of values supported by reflection. Through the introduction of these two types, we have well completed the unified expression of all types by getValue() and setValue(). Let's introduce the implementation of these two in detail.

(1) UserObject

Unlike reflection::Class, which is used to express Meta information, UserObject is actually the actual data part. When the two are combined, we can obtain the complete runtime type expression. We can dynamically construct an object according to ID or name, and obtain and set its properties or call its interface.

class UserObject{public:    template <typename T>    static UserObject makeRef(T& object);
    template <typename T>    static UserObject makeRef(T* object);
    template <typename T>    static UserObject makeCopy(const T& object);
    template <typename T>    static UserObject makeOwned(T&& object);
    UserObject(const UserObject& other);
    UserObject(UserObject&& other) noexcept;        template <typename T>    UserObject(const T& object);
    template <typename T>    UserObject(T* object);    UserObject& operator = (const UserObject& other);    UserObject& operator = (UserObject&& other) noexcept;        template <typename T>    typename detail::TypeTraits<T>::ReferenceType get() const;
    void* pointer() const;
    template <typename T>    const T& cref() const;
    template <typename T>    T& ref() const;
    const Class& getClass() const;
    Value get(IdRef property) const;
    Value get(size_t index) const;
    void set(IdRef property, const Value& value) const;
    void set(size_t index, const Value& value) const;
    bool operator == (const UserObject& other) const;
    bool operator != (const UserObject& other) const { return !(*this == other); }
    bool operator < (const UserObject& other) const;
    static const UserObject nothing;private:    void set(const Property& property, const Value& value) const;    UserObject(const Class* cls, detail::AbstractObjectHolder* h)        : m_class(cls)            , m_holder(h)        {}
    // Metaclass of the stored object    const Class* m_class;    // Optional abstract holder storing the object    std::shared_ptr<detail::AbstractObjectHolder> m_holder;};

It is estimated that it is different from the complex implementation of full screen that most people think. In fact, the code of UserObject is relatively simple. As mentioned earlier, UserObject mainly completes the holding and management of object data. The original implementation of data Holder is relatively simple, and STD:: shared is directly used_ PTR < >, through several different purpose holders inherited from AbstractObjectHolder, to complete the data holding and life cycle management.

There are several types of UserObject interfaces:

  • Static construction method
template <typename T>static UserObject makeRef(T& object);
template <typename T>static UserObject makeRef(T* object);
template <typename T>static UserObject makeCopy(const T& object);
template <typename T>static UserObject makeOwned(T&& object);

A UserObject is constructed statically. The main difference between them lies in the difference of object life cycle management:

  • makeRef(): does not create an object, but holds it indirectly. Therefore, when an object is held by UserObject, it may be released by an external error, resulting in an exception.
  • makeCopy(): different from makeRef, the Holder will directly create and hold a copy of related objects, which is the implementation of life cycle security.
  • makeOwned(): similar to makeCopy(), the difference is that it transfers the control of external objects to UserObject, which is also the implementation of life cycle security.
  • Generic constructor
template <typename T>UserObject(const T& object);
template <typename T>UserObject(T* object);

For the construction and implementation of type T, note that the internal call is the makeCopy() static method described above, so the UserObject constructed in this way is life-cycle safe.

  • Conversion to the original C + + type
template <typename T>typename detail::TypeTraits<T>::ReferenceType get() const;
template <typename T>const T& cref() const;
template <typename T>T& ref() const;

Note that if the conversion fails, C + + exceptions will be thrown directly.

  • Convenient interface related to Property
Value get(IdRef property) const;Value get(size_t index) const;void set(IdRef property, const Value& value) const;void set(size_t index, const Value& value) const;

A shortcut interface for accessing Property.

  • other
void* pointer() const;const Class& getClass() const;static const UserObject nothing;

Two special interfaces are commonly used, including:

  • pointer(): the raw pointer used to obtain the internal storage object of UserObject.
  • **getClass(): used to get the MetaClass corresponding to UserObject.
  • Nothing: the null value of UserObject. It determines whether a UserObject is null. It can be directly compared with the static variable.

(2) Value

The implementation of Value is also very simple. The core code is as follows:

using Variant = std::variant<        NoType,        bool,        int64_t,        double,        reflection::String,        EnumObject,        UserObject,        ArrayObject,        detail::BuildInValueRef      >;Variant m_value; // Stored valueValueKind m_type; // Ponder type of the value

In fact, through STD:: variant < >, it defines a containing:

  • NoType
  • bool
  • int64_t
  • double
  • string
  • EnumObject
  • UserObject
  • ArrayObject
  • BuildInValueRef

One of the above types and types, and then use std::visit() to access the internal std::variant to complete an implementation of various operations. The implementation idea is relatively simple. In this way, we can uniformly express the types supported by reflection with Value.

Common interfaces are as follows:

ValueKind kind() const;
template <typename T>T to() const;
template <typename T>typename std::enable_if_t<detail::IsUserType<T>::value, T&> ref();template <typename T>typename std::enable_if_t<!detail::IsUserType<T>::value, T&> ref();
template <typename T>typename std::enable_if_t<detail::IsUserType<T>::value, const T&> cref() const;template <typename T>typename std::enable_if_t<!detail::IsUserType<T>::value, const T&> cref() const;template <typename T>bool isCompatible() const;
static const Value nothing;

6, builder implementation -- together

The main purpose of builder is to use the compiler time feature to complete the generation of Meta information from original types through type derivation.

(1) Implementation of function

For the implementation of function, please refer to [[reflection function implement ation]].

(2) Implementation of property

For the implementation of property, please refer to [[reference property implementation]].

(3) Summary

Since the compiler time type of C + + itself is relatively complete, it provides us with a purpose to complete the generation of Meta information through the compiler time feature. The implementation of Property is similar to that of function. Finally, the information of the whole meta class is filled by using trails and some other facilities, Then we can use the registered types at runtime.

7, Runtime implementation -- convenient runtime interface

(1) Auxiliary methods for object creation and deletion

template <typename... A>static inline UserObject create(const Class& cls, A... args);
static inline UserObject createWithArgs(const Class& cls, Args&& args);
using UniquePtr = std::unique_ptr<UserObject>;inline UniquePtr makeUniquePtr(UserObject* obj);
template <typename... A>static inline UniquePtr createUnique(const Class& cls, A... args)
static inline void destroy(const UserObject& obj)

Through these methods, we can quickly create and destroy reflective objects.

(2) Function call

template <typename... A>static inline Value call(const Function& fn, const UserObject& obj, A&&... args);
static inline Value callWithArgs(const Function& fn, const UserObject& obj, Args&& args);
template <typename... A>static inline Value callStatic(const Function& fn, A&&... args);
static inline Value callStaticWithArgs(const Function& fn, Args&& args);

When introducing the Function object, we didn't find the above invoke method. The original implementation of res ponder is indirectly completed through the auxiliary call(), callStatic() method to execute the corresponding meta function. In this place, we should consider providing the corresponding implementation directly on the Function instead of the current mode.

8, Future -- thinking questions

After reflection? further more:

// Define the interface of something that can be drawnstruct Drawable : decltype(dyno::requires_(  "draw"_s = dyno::method<void (std::ostream&) const>)) { };
// Define an object that can hold anything that can be drawn.struct drawable {  template <typename T>  drawable(T x) : poly_{x} { }
  void draw(std::ostream& out) const{ poly_.virtual_("draw"_s)(out); }
private:  dyno::poly<Drawable> poly_;};
void f(drawable const& d) {  d.draw(std::cout);}
struct Square {  void draw(std::ostream& out) const { out << "Square"; }};
struct Circle {  void draw(std::ostream& out) const { out << "Circle"; }};
int main() {  f(Square{}); // prints Square  f(Circle{}); // prints Circle}

We need to implement t rust traits, which is a polymorphic class with the same compile time and run time. What else do we need to do? What features do we already have? What other features are needed?

9, Summary

In fact, after understanding the system, we will find that with the iteration of C + + itself and the wheel of reflection, the development difficulty becomes simpler and simpler. Compared with luabind in the 1990s of C + +, the reflection implementation code in CPP framework has been very simplified, and we can also find that the function is more powerful, more occasions are adapted, and the code complexity has decreased significantly. From this point of view, You can see that the iteration of the new features of C + + is actually subtraction, which can give you a more concise and understandable way to express what needs to be done by a more complex implementation. It's a good time to learn and understand Meta Programming from the 14 / 17 node.

reference material:

1.[github ponder library]


Added by nelietis on Tue, 22 Feb 2022 14:43:22 +0200