前言:
C++发展到如今已经是一种支持多种范式编程的语言,主要包含过程式、面向对象、函数式、泛型、模板元等。本文写作的目的之一是借助Ponder源码向大家展示模板元编程&泛型范式的编程方式和C++模板元编程是如何与C++普通运行时程序完成无缝衔接的。以及向你展示C++另一种编程方式的乐趣,同时也能让大家熟悉了解底层反射的实现原理。我更加相信程序员是更容易理解代码的,所以文章从源码入手用代码的方式来跟大家进行交流相信会比读文档来的更加有趣味。
读完本文你能收获什么 ? 首先它不会让你突然成为C++编程专家,但它能让你能熟悉很多编程技术实现原理, 让你能读懂与修改你曾不熟悉的技术。
1. 锻炼自己数据抽象与建模能力。在面向对象编程领域相信大家无时无刻不在与class打交道,该能力的培养与锻炼相信能让你的程序更加具有扩展性、可读性、健壮性。
2. 带你深入了解一些元编程技术在实际库中的应用。当然ponder库中所展示的技术也许并不是最新的但相信会有令你足够兴奋的。本文仅仅介绍Ponder库中所使用到的技术。
3. 熟悉反射概念,并对其实现细节有深入了解。能够让你零成本的适应支持反射的其他语言。
4. 一些常用设计模式的使用。
Ponder介绍:
Ponder 是一个为类型提供运行时反射的 C++ 库。它为 C++ 的大多数高级概念(如类、枚举、属性、函数和对象)提供了抽象。通过将所有这些概念包装到抽象结构中,Ponder 为程序提供了额外的灵活性,并允许它们在运行时公开和操作它们的数据结构。
ponder库使用参考文档: ponder: Ponder C++ library documentation
文章结合ponder源码与反射概念,将其对C++主要相关概念(类、类成员属性、类成员函数、类构造函数)的抽象类分别称之为元类类,元属性类,元函数类以及元构造类并主要从这几个抽象类来对ponder源码展开分析。各个抽象类记录着相关概念中的元数据和定义了元数据所具备的行为,当然这里的元数据/信息并非大家所理解的传统意义上的元数据,这里所记录着的元数据是运行时数据且是能够被修改的这点与传统意义上的元信息还是有所区别的。
阅读建议:
由于内容原因,文章篇幅偏长。可以先收藏后在业余时间在持续阅读。
目录
-
Meta Class
MetaClass(元类类)与其他普通类本质上并无区别,普通类定义对象的行为而元类是定义某些类的行为。基于对反射概念的表示,实际上这里的Meta Class本质上还是一个普通类。我们可以先看看 ponder为实现meta class功能所抽象出的UML类图

从UML类图可以看出Ponder针对C++ class概念/关键字抽象出的Meta Class类以到达完全具有class概念的类行为,Meta Class类关联了/记录着C++ class中具有的成员属性、成员函数、构造函数、析构函数、基类等概念元信息。同时Meta Class抽象类还关联了ClassManager&ClassBuilder用来管理&构造Meta Class 实例。这里就通过Meta Class完成对C++ class概念/关键字的数据建模。
由于下文中有涉及concept-check,在开始Meta Class源码分析前这里先解释下在泛型/元编程范式中concept-check(概念检查) 一词:
在泛型编程中我们通常要求某些类型在语义上具有某种traits(特征)或者满足某种条件,如类型是否声明了必要的成员函数,是否声明特定的类型等等。 通常将这些要求称之为concept(概念)。concept-check(概念检查)即是检查特定类型是否符合相关特定条件。
meta class源码分析
Meta Class类头文件源码(注: 为优化篇幅以下代码片段并非Class类的全部声明,代码段中仅仅列出了笔者认为能够讲清楚工作流程的主要代码片段)
class PONDER_API Class : public Type
{
PONDER__NON_COPYABLE(Class);
// Structure holding informations about a base metaclass
struct BaseInfo
{
const Class* base;
int offset;
};
typedef std::shared_ptr<Constructor> ConstructorPtr;
typedef std::shared_ptr<Property> PropertyPtr;
typedef std::shared_ptr<Function> FunctionPtr;
typedef std::vector<BaseInfo> BaseList;
typedef std::vector<ConstructorPtr> ConstructorList;
typedef detail::Dictionary<Id, IdRef, PropertyPtr> PropertyTable;
typedef detail::Dictionary<Id, IdRef, FunctionPtr> FunctionTable;
typedef void (*Destructor)(const UserObject&, bool);
typedef UserObject (*UserObjectCreator)(void*);
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 metafunctions indexed by ID
PropertyTable m_properties; // Table of metaproperties 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
public: // declaration
template <typename T>
static ClassBuilder<T> declare(IdRef id = ponder::IdRef());
template <typename T>
static void undeclare();
pulic: // reflection
IdReturn name() const;
size_t baseCount() const;
const Class& base(size_t index) const;
const Constructor* constructor(size_t index) const;
void destruct(const UserObject &uobj, bool destruct) const;
bool hasFunction(IdRef name) const;
const Function& function(size_t index) const;
const Function& function(IdRef name) const;
FunctionView functions() const;
bool tryFunction(const IdRef name, const Function*& funcRet) const;
bool hasProperty(IdRef name) const;
const Property& property(size_t index) const;
const Property& property(IdRef name) const;
PropertyView properties() const;
bool tryProperty(const IdRef name, const Property*& propRet) const;
private:
template <typename T> friend class ClassBuilder;
friend class detail::ClassManager;
Class(TypeId const& id, IdRef name);
int baseOffset(const Class& base) const;
};
这里我们具体来看看Ponder Meta Class中的具体功能,从Class头文件的接口及其成员属性声明我们可以知道Class类的功能主要有以下几点:
- 为简化Meta Class类的使用成本Meta Class类提供了私有的构造函数不允许客户代码在外部实例化Meta Class对象,同时基于建造者模式提供ClassBuilder模板类,需通过静态模板方法declare来声明一个Meta Class T并通过ClassBuilder实例来进行Meta Class的元信息填充,使得客户代码能够使用链式调用简化Class对象的实例化过程及T类元信息注册过程。
- 记录存储某个类T(T的实例化来自于外部代码调用declare方法时的模板参数)的成员属性、成员函数、构造函数、析构函数、基类、名称等meta信息的集合,从UML类图可以看出这些元信息都是通过ponder库中Property、Function等元类进行了封装来间接表示的。
- 结合下文中的ClassBuilder模板类共同提供对类的meta信息的增删改查等接口。
到这里Meta Class的数据与行为基本就声明完成, 本质上来说并不复杂。
注:空基类Type做类型擦除用,无实际功能。
Type Define
class PONDER_API Type
{
public:
virtual ~Type() {}
};
基于建造者&观察者模式提供ClassBuilder模板类&ClassManager类来管理MetaClass集合与实例化。
ClassBuilder类头文件源码,它通过建造者模式提供了Meta Class的构造过程。
template <typename T>
class ClassBuilder
{
public:
ClassBuilder(Class& target);
template <typename U>
ClassBuilder<T>& base();
template <typename F>
ClassBuilder<T>& property(IdRef name, F accessor);
template <typename F1, typename F2>
ClassBuilder<T>& property(IdRef name, F1 accessor1, F2 accessor2);
template <typename F, typename... P>
ClassBuilder<T>& function(IdRef name, F function, P... policies);
template <typename... A>
ClassBuilder<T>& constructor();
template <template <typename> class U>
ClassBuilder<T>& external();
template <typename... U>
ClassBuilder<T>& operator () (U&&... uds)
{
const std::initializer_list<UserData> il = {uds...};
for (UserData const& ud : il)
userDataStore()->setValue(*m_currentType, ud.getName(), ud.getValue());
return *this;
}
private:
ClassBuilder<T>& addProperty(Property* property);
ClassBuilder<T>& addFunction(Function* function);
Class* m_target;
Type* m_currentType;
};
相信熟悉建造者模式的朋友结合上面ClassBuilder类的声明完全可以看出该类的具体功能了。只不过ponder中提供的ClassBuilder类加上模板与提供的模板方法后让类看起来更加复杂了。由于ponder库将要实现功能原因这里的成员方法形参用来表示T类将要注册的元信息。而元信息与具体类型无关所以必须要用模板参数来表示。这里同样介绍下ClassBuilder的主要实现功能:
- 通过ClassBuilder模板类的声明我们可以看出此类所有接口均为返回自身引用以达到链式访问的目的(基本符合建造者模式的规则),构造函数形参为ClassBuilder类操作的代表类T的MetaClass实例。 base、property、 function、constructor 等模板方法分别提供了类T 需要注册到当前表示自己的MetaClass对象中的元信息。
- base 模板方法为类T提供基类元信息的注册。
- constructor 模板方法的模板参数为模板参数包,模板参数列表为类T的构造函数形参类型列表。最终会将模板参数列表打包进ConstructorImpl Class中。
- property&function 方法中的模板类型形参均会被封装成ponder库中的Property&Function等类实例(即通过Property&Function类型擦除模板通用类型),具体封装过程可以在下文MetaProperty&MetaFunction&MetaConstructor中查阅(会涉及到较多的C++ 模板元编程的内容)。
- external 提供了从类型U(模板类)且U的模板参数为当前T的类型中添加属性或方法。
- operator() 模板方法提供了任意参数数量、类型的形参列表用于将用户数据存储到最后一次注册到Meta Class实例中的元信息对象中。在需要时可以通过元信息实例从TypeUserDataStore对象中获取用户数据。这里笔者有个疑问,通过operator()方法的实现可以看出,参数列表的每个实参必须为可以转换成UserData类型的数据,这样就大大限制了该方法声明模板参数包的威力以及增加了使用者对于模板参数包的理解程度。 是否进行下列方式优化会更好?
// PONDER userdata类的声明&实现 class UserData { public: UserData(IdRef name, Value&& value) : m_name(name), m_value(value) {} IdReturn getName() const { return m_name; } const Value& getValue() const { return m_value; } private: Id m_name; Value m_value; }; // 优化方式1, 取消方法模板参数列表。降低使用者理解程度. 直接要求为形参为UserData类型列表。 ClassBuilder<T>& operator () (const std::vector<UserData*>& uds) { for (auto const& ud : uds) userDataStore()->setValue(*m_currentType, ud->getName(), ud->getValue()); return *this; } // 优化方式2, 对UserData做概念检查。给出更加明确的数据类型要求和出错信息。 // 这里不要求参数类型必须为UserData, 只需要存在getName&getValue方法且返回值分别为Id&Value即可。 // 最大限度的发挥出模板参数包的威力同时给使用者足够的灵活度。 // 注意这里的概念检查并没有对类型限定符做处理。 template <typename... U> struct is_user_data; template <typename UF, typename... U> struct is_user_data<UF, U...> { template<typename UD, typename = void> struct is_user_data_inner { static constexpr bool value = false; }; template<typename UD> struct is_user_data_inner<UD, std::void_t<decltype(&UD::getName), decltype(&UD::getValue)>> { static constexpr bool value = std::is_same_v<decltype(std::declval<UD>().getName(), IdReturn> && std::is_same_v<decltype(std::declval<UD>().getValue(), Value>>; }; static constexpr bool value = is_user_data_inner<UF>::value && is_user_data<U...>::value; }; template <> struct is_user_data<> { static constexpr bool value = true; }; template <typename... U> ClassBuilder<T>& operator () (U&&... uds) { static_assert(is_user_data<U...>::value, "存在不符合规范的参数类型"); setUserData(std::forward<U>(uds)...); return *this; } void setUserData() {} template<typename UF, typename ...U> void setUserData(UF&& ud, U&& ... uds) { userDataStore()->setValue(*m_currentType, ud.getName(), ud.getValue()); setUserData(std::forward<U>(uds)...); }
笔者认为ClassBuilder是个非常重要的类,它将类的具体元信息这里特指成员属性地址、属性get/set函数地址、成员函数地址、基类类型、构造函数形参列表等信息转换为在ponder库中抽象出的具体元信息类进行表示并将这些元信息类实例存储到Meta Class中。这里即完成了C++ class类的表示和信息注册,通过Meta Class实例即可在运行时完成类的信息获取和行为调用即达到了运行时反射的功能。
ClassManager类声明, 它管理着Meta Class的集合并通过观察者模式让外部客户代码能够监听Meta Class实例的添加、删除等行为。
class PONDER_API ClassManager : public ObserverNotifier
{
typedef std::map<TypeId, Class*> ClassTable;
typedef std::map<Id, Class*> NameTable;
public:
typedef View<const Class&, ClassTable::const_iterator> ClassView;
static ClassManager& instance();
Class& addClass(TypeId const& id, IdRef name);
void removeClass(TypeId const& id);
size_t count() const;
const Class& getById(TypeId const& id) const;
const Class* getByIdSafe(TypeId const& id) const;
const Class& getByName(const IdRef name) const;
const Class* getByNameSafe(const IdRef name) const;
bool classExists(TypeId const& id) const;
ClassManager();
~ClassManager();
ClassView getClasses() const;
private:
ClassTable m_classes;
NameTable m_names;
};
通过ClassManager类的声明可以看出此类主要提供如下功能:
- 是一个Meta Class实例的容器,用于存储Meta Class类的所有实例。成员属性m_classes&m_names代表着可通过注册类型的typeid&name操作相对应的 Meta Class实例。
- 提供对Meta Class实例的增删查接口。
- 继承自ObserverNotifier观察者接口, 用于客户代码观察meta class实例的变化行为。
到这里Ponder 库中对于C++ Class 概念的封装就完成了,现在来看看Meta Class的declare静态方法实现&使用示例:
Meta Class declare function impl:
template <typename T>
inline ClassBuilder<T> Class::declare(IdRef name)
{
typedef detail::StaticTypeDecl<T> typeDecl;
Class& newClass =
detail::ClassManager::instance()
.addClass(typeDecl::id(false), name.empty() ? typeDecl::name(false) : name);
newClass.m_sizeof = sizeof(T);
newClass.m_destructor = &detail::destroy<T>;
newClass.m_userObjectCreator = &detail::userObjectCreator<T>;
return ClassBuilder<T>(newClass);
}
通过declare的方法的实现代码typedef detail::StaticTypeDecl<T> typeDecl; 可以看出要声明获取类T的Meta Class对象需特化StaticTypeDecl模板类。Ponder库为我们声明了特化StaticTypeDecl模板类的PONDER_TYPE与PONDER_AUTO_TYPE宏:
template <typename T>
struct StaticTypeDecl
{
static constexpr bool defined = false, copyable = true;
typedef T type;
static TypeId id(bool = true)
{
return T::PONDER_TYPE_NOT_REGISTERED();
}
static const char* name(bool = true)
{
return T::PONDER_TYPE_NOT_REGISTERED();
}
};
#define PONDER_TYPE(...) \
namespace ponder { namespace detail { \
template<> struct StaticTypeDecl<__VA_ARGS__> \
{ \
static TypeId id(bool = true) {return calcTypeId<__VA_ARGS__>();} \
static constexpr const char* name(bool = true) {return #__VA_ARGS__;} \
static constexpr bool defined = true, copyable &#