C++反射:全面解读property的实现机制!

2022-03-03 10:41:57 浏览数 (1)

导语 | 本文将深入Property的部分进行介绍,相比较[[reflection function implement]],Property涉及的Tag Dispatch和中间过程更复杂,整体的实现需要一步一步来理清,我们还是从例子入手,从Property的注册和使用来展开整体的实现。

在上篇C 反射:深入浅出剖析ponder库实现机制!中我们对反射实现的整体做了相关的介绍,本篇将深入Property的部分进行介绍。

一、 Property示例代码

代码语言:javascript复制
//-------------------------------------//register code//-------------------------------------__register_type<Vector3>("Vector3")    .constructor()    .constructor<double, double, double>()    .property("x", &Vector3::x)    .property("y", &Vector3::y)    .property("z", &Vector3::z)    );
//-------------------------------------//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);
const reflection::Property* fieldX = nullptr;metaClass->TryProperty("x", fieldX);ASSERT_TRUE(fieldX != 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);

上面的代码分为两部分:

(一)注册的代码

注册的代码通过__register_type<T>()创建的ClassBuilder提供的。property(name,property)函数来完成对属性的注册。

(二)使用的代码

使用的代码,先获取到MetaClass,再从MetaClass中通过TryProperty()通过名字查询到对应的reflection::Property,然后我们可以通过Property的Get(),Set()方法来对对应UserObject对象的属性进行设置和获取。

(三)整体文章的展开思路

我们的讲述会按照下面的顺序逐步展开:

  • 一些基础知识。
  • Property运行时的承载对象Property类。
  • 编译期的注册机制。
  • 不同的Property特化实现。
  • 运行时获取值、设置值的具体过程。

二、基础知识

C 中的Property多以Member Object的方式表达,MemberObject的类型和处理方式比较特殊。以framework::math::Vector3举例:

Vector3的成员变量定义如下:

代码语言:javascript复制
class Vector3 { public:  double x;  double y;  double z;};

以下代码是用来获取成员变量y的:

代码语言:javascript复制
//using MemberType = double(framework::math::Vector3::*);using MemberType = double framework::math::Vector3::*;MemberType tmppy = &framework::math::Vector3::y;framework::math::Vector3 tmpvec(1.0, 2.0, 3.0);auto tmpy = tmpvec.*tmppy;

简单总结如下:

  • 通过T(C::) 或者T C::来表达成员变量的类型,如上例中的 double(Framework::math::Vector3::) 。
  • 通过成员变量取地址的方式获取对应成员的地址,如上例中的&framework::math::Vector3::y。
  • 如上例中,可以通过tmpvec.tmppy这种获取方式来获取对应对象中的成员(获取的是tmpvec.y的值)。
  • 正常如果不是实现反射,很少使用相关的特性。

三、运行时属性的表达-Property类

为了实现运行时Property,所有的Property需要进行类型擦除,以一致的外观进行组织和调用,framework中的Property实现如下(节选):

代码语言:javascript复制
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;
  inline TypeId type_index() const { return type_index_; }
  inline auto implement_type() const { return implement_type_; } protected:  virtual Value GetValue(const UserObject& object) const = 0;    virtual void SetValue(const UserObject& object, const Value& value) const = 0;};

主要使用的是Get(),Set()两个方法,用于从UserObject中获取和设置指定Property的值。

四、依赖的核心机制

虽然Property整体的机制比较复杂,但核心依赖的机制实现比较简洁,主要依赖的是ValueBinder<>和ValueBinder2<>,以及与这两者基本一致的InternetRefBinder<>和InternetRefBinder2<>模板类。

(一)ValueBinder<>和ValueBinder2<>

ValueBinder<>的实现如下图所示:

上图中还有个依赖的Binding对象,具体的信息如下:

相关的Function和Member的Traits我们下文中会具体展开,本部分主要关注ValueBinder和两个Traits内部的TBinding模板类的实现。先从ValueBinder的具体代码说起:

代码语言:javascript复制
代码语言:javascript复制
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 this->bound_.Access(c) = v, true;    else      return false;  }  bool Setter(ClassType& c, Value const& value) const { return Setter(c, value.to<SetType>()); }  Value GetValue(ClassType& c) const {    if constexpr (PropTraits::kIsWritable)      return UserObject::MakeRef(Getter(c));    else      return UserObject::MakeCopy(Getter(c));  } protected:  Binding bound_;};

Getter(),Setter()的实现主要依托Traits内的TBinding::Access()来实现,也就是我们上图中所贴出的TFunctionTraits<T>::TBinding和 TMemberTraits<T>::TBinding实现,这样在模板层面,我们就有了一个获取和设置对象属性的模板类了,当然,真正将ValueBinder用起来,我们还需要其他的模板设施,暂时我们先关注最核心的这部分。

除了ValueBinder外,反射库也提供了ValueBinder2模板类,看实现可以发现,主要是提供了外部额外提供一个函数来做Setter的机制:

代码语言:javascript复制
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 { return set_(c, v), true; }  bool Setter(typename Base::ClassType& c, Value const& value) const {    return Setter(c, value.to<typename Base::SetType>());  } protected:  std::function<void(typename Base::ClassType&, typename Base::AccessType)> set_;};

(二)Internet RefBinder<>与Internet RefBinder2<>实现

与ValueBinder提供的接口完全一致,主要是为UserObject类型的对象服务的,此处不详细赘述了。

(三)反射框架中类名后的数字

Propety部分相关的模板类,不少都有数字,如ValueBinder2<>,InternalRefBinder2<>,GetSet2<>等,都是两个参数版本的property注册使用的,一个参数指定getter,一个参数指定setter,setter。前面介绍ValueBinder2的时候也有说到,ValueBinder2通过额外的function对象重载了Setter()接口。

五、属性的注册

ClassBuilder提供了两个版本的property注册函数,第一个版本对应的是一个accessor的版本:

代码语言:javascript复制
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;  }}

第二个版本对应的是两个accessor的版本:

代码语言:javascript复制
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;  }}

从上述的两段代码可以看到,直接负责创建Property的是模板类的Create函数,PropertyFactory1<T,F>::Create()和PropertyFactory2<T, F1,F2>::Create(),下文中会具体展开相关的实现。

六、PropertyFactory1<T, F>

&PropertyFactory2<T, F1, F2>具体实现

先以PropertyFactory1<T,F>::Create的处理过程为例,来看一下整体Property的创建流程:

整体处理流程如下:

  • 根据C,T推导正确的Accessor和PropertyImpl,主要是利用GetSet1<>模板类。
  • 利用GetSet1<>模板类中定义的Access类型, 关联正确的AccessTraits类型。
  • 利用AccessTraits类型中定义的Impl和ValueBinder类型正确的产生InterfaceType和Property的具体Impl类(如SimplePropertyImpl类)。

整个处理过程比较复杂,下文中将详细展开相关的类。

PropertyFactory2<>的处理流程基本与PropertyFactory1<>的处理流程一致, 主要的区别在于PropertyFactory2创建的Property的Setter是通过F2来指定的, 不详细细述了。

(一)PropertyFactor与GetSet<>模板类

通过上图的关系, 我们也能很容易的看到PropertyFactory处理属性的类别,主要是三类:

  • GetSet1<TFunctionTraits<T>>用来处理以单个Getter函数提供的属性。
  • GetSet1<TMemberTaits<T>>用来处理以第3节中介绍的,直接用Member Object来表达的属性。
  • GetSet2<>用于表达以两个函数分别表达getter,setter的属性。

这里涉及的TFunctionTraits,TMemberTratis的定义如下所示:

  • TFunctionTraits

中间利用了另外一个辅助的模板类TCallableDetails<T>,细节如下:

整个Function Traits主要是对各种不同函数类型的特化表达,最后方便我们获取:

  • ParamTypes: 参数类型列表。
  • ReturnType: 返回值类型。
  • FuncType: 函数类型。
  • DispatchType: 用于构建std::function<>的模板参数。
  • FunctionCallTypes: 同ParamTypes。
  • TMemberTraits

(二)GetSet模板类的实现

GetSet1模板类的定义与GetSet2基本一致,除了GetSet2明确利用函数来表达getter,setter。

(三)AccessTratis<>模板类的实现

如上图所示,AccessTraits的核心信息比较少,主要是以下几项:

  • kind: 属性的类别,主要是两类,MemberObject和Function。
  • using ValueBinder: GetSet1用到的属性绑定类型。
  • using ValueBinder2: GetSet2用到的属性绑定类型。

AccessTraits主要有以下几类:

覆盖了我们反射支持的所有属性类型:

  • SimplePropertyImpl
  • EnumPropertyImpl
  • ArrayPropertyImpl
  • UserPropertyImpl

七、不同的Property特化实现

要实现运行时Property特性,光有上述介绍的GetSet<>,AccesssTraits<>模板类是不够的,我们需要通过具体的PropertyImpl来将相关的功能串联起来。

(一)SimplePropertyImpl

代码语言:javascript复制
template <typename A>class SimplePropertyImpl : public SimpleProperty { public:  SimplePropertyImpl(IdRef name, A accessor); protected:  bool IsReadable() const final;  bool IsWritable() const final;  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_;  // Accessor used to access the actual C   property};

如图所示,以 SimplePropertyImpl<>为桥梁,将GetSet1<>,ValueBinder<>等模板类串联到一起,完成了对一个具体的UserObject某个属性进行设置和获取的功能实现(中间还有GetSet模板与AccessTraits模板的串接,上文中已经交代,这里不再重复。

另外的几个PropertyImpl,如EnumPropertyImpl,ArrayPropertyImpl,UserPropertyImpl与SimplePropertyImpl的实现大同小异,这里不一一展开了。

八、获取值、设置值的具体过程

我们以最前面例子中获取属性值时的调用栈以实际运行的例子来看一下整个运行时获取属性值的过程:

调用栈不太方便分析, 我们适当格式化方便分析, 我们从上图中从外到内的顺序来具体看一下:

(一)Stack Level1

格式化后的调用栈:

代码语言:javascript复制
//code 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);

对应的代码截图:

(二)Stack Level2

格式化后的调用栈:

代码语言:javascript复制
//code 2:framework::reflection::detail::ValueBinder<    framework::math::Vector3,    framework::reflection::detail::TMemberTraits<        double framework::math::Vector3::*    >>::Getter(framework::math::Vector3 & c);

对应的代码截图:

(三)Stack Level3

格式化后的调用栈:

代码语言:javascript复制
//code 3:framework::reflection::detail::TMemberTraits<    double framework::math::Vector3::*>::TBinding<framework::math::Vector3,double &>::Access(framework::math::Vector3 & c)

对应的代码截图:

(四)小结

利用多个模板类的级联和使用,我们最后通过SimplePropertyImpl<>完成了运行时动态获取属性的目的,设置的过程与获取的过程基本一致,这里不重复展开了。

九、总结

通过多层模板的级联,我们完成了运行时动态获取设置属性的功能,另外因为整体代码多利用模板,通过最后一节的分析,我们也能发现,整体的性能其实是比较高的,更多还是依赖模板自身的特性和Tag Dispatch来完成了相关的功能。同时,也能发现,如果仅依托c 17的特性,模板之间的关联会比较弱,整体代码的维护和理解会比较麻烦。后续我们考虑用c 20的concept重构整个反射库,到时再额外输出相关的文章了。

 作者简介

沈芳

腾讯后台开发工程师

IEG研发效能部开发人员,毕业于华中科技大学。目前负责CrossEngine Server的开发工作,对GamePlay技术比较感兴趣。

 推荐阅读

微前端究竟是什么?微前端核心技术揭秘!

C 反射:深入浅出剖析ponder库实现机制!

海量Node.js网关的架构设计与工程实践!

Monorepo——探秘源码管理新姿势!

0 人点赞