元对象和元数据
P.S.(该系列文章是个人学习总结,拿出来和大家讨论,水平有限,如有错误,特别、非常、极其欢迎批评和指正!)
开始之前,先放一个链接,这个网站可以查看不同版本 Qt 相关的源码,不调试的话用这个就很方便。Qt源码浏览
1 疑问
Qt 作为跨平台的GUI框架,在实际项目中应用广泛,在日常的使用中,随手使用的一些机制(如著名的信号槽机制),属性(如Property系统),以及重载各种事件函数来完成定制化,有时不禁好奇这些内容是怎么实现的。该系列文章不适合作为 Qt 的入门文章,适合有一定 Qt 使用经验,想了解 Qt 内部核心机制的朋友们。
是否好奇过,为什么在 Qt 的框架下,我们只需要通过简单的信号槽宏连接两个对象的方法,就可以实现类似观察者的通信方式——甚至当前类并没有存另一个类的任何信息。
带着好奇,我查看了经典的SINGAL()
和SLOT()
宏定义,我发现这个宏就做了一个事情,把我们的信号和槽的方法包装为一个字符串!那个qFlagLocation
可以看到,就是进去转了一圈。
# define SLOT(a) qFlagLocation("1"#a QLOCATION)
# define SIGNAL(a) qFlagLocation("2"#a QLOCATION)
const char *qFlagLocation(const char *method)
{
QThreadData *currentThreadData = QThreadData::current(false);
if (currentThreadData != nullptr)
currentThreadData->flaggedSignatures.store(method);
return method;
}
这里没有发现猫腻,那么猫腻是不是在connect
方法中呢?
static QMetaObject::Connection connect(const QObject *sender, const char *signal,
const QObject *receiver, const char *member, Qt::ConnectionType = Qt::AutoConnection);
可以看到,这里面确实只利用了前面包装的字符串——即函数名,问题是,你见过 C++ 中有如下的调用吗?
pMyclass->"method1";
//或者
myClass."method2";
那么,Qt 只是拿两个方法名就能完成调用,是怎么做到的呢?素朴的想法是,一定是根据某种方法把字符串转换为对应对象方法,在通过方法调用来完成,但是 C++ 本身显然不提供这个能力,Java 中有类似反射的概念可以完成这个任务。
所以推测,Qt 大概率是采用某种方法拿到了方法和函数名的映射数据,从而完成转换,这部分数据我们暂且称为元数据。
2 元数据和元对象
什么是元数据?
元数据是描述数据的数据,试想一下,我们会怎么描述一个类 MyClass:
class MyClass : public Object
{
public:
MyClass();
~MyClass();
enum Type
{
//...
};
public:
virtual void fool() override;
void bar();
//...
};
- 这个类的类名为MyClass
- 继承了一个基类 Object
- 有一个无参的构造函数和一个析构函数
- 实现了继承来的一个虚方法
- 自己有一个名为bar的public方法
- 内定义了一个枚举类型
- …
上述描述内容就是元数据,用来描述我们声明的一个class,如果我们把以上数据封装为一个类,我们简单的认为这个类就是元对象。
3 额外的话题:为什么需要元对象系统
3.1 场景和问题
1)类型转换
面型对象的应用场景中我们经常操作一个指向派生类的基类指针,利用面向对象的多态特性,可以大大简化我们的编码方式,也是各种代码设计,设计模式中的基础。但是不可避免的,我们会遇到需要知道一个对象具体类型的时候(比如在一段处理 Object 的逻辑里面,如果这个类型是 MyClass,我们需要做一些特殊处理),这时候该怎么办呢?
2)对象间通信
Qt 中最有特点的便是对象间的通讯机制-信号槽系统,这点在GUI程序尤为重要,使用起来很方便,绑定对象的信号和槽,当信号发送时,槽函数得到响应。如果使用 C++ 的能力,我们要怎么做呢?
3)运行时增加属性
如果,我想在运行时根据当前的上下文为一个对象增加或者删除属性,并且要做到在其他地方使用的时候无感——就像这个属性原来就声明在类中一样,在原生的 C++ 中,怎么办?
4)…
3.2 C++的解决方案
针对场景1),我们当然可以使用 dynamic_cast 去尝试,但我想对于所有 C++ 的开发者来讲,我们都会有意避免使用动态类型转换,尤其是继承深度不断增长时,大量而频繁的 dynamic_cast 不可避免的使程序变慢。
对于场景2),我们可以使用回调函数或者函数对象,但是类型安全检查让人头秃,各种typedef也不好看;我们也可以使用观察者模式,当一个对象的行为发生变化时,更新另一个对象的状态,但是发现了吗,这个地方是紧耦合(一定要知道具体的类型),而且对于函数签名限制死了,更通用的说法是,对于 RTTI(运行时类型信息), C++ 并没有提供很好的支持,没有一种反射机制,可以让我们运行时得知一个类的描述(继承关系,成员函数…), C++ 是静态语言,这些信息在编译器存在,但是运行期是没有的。
对于场景3),无解,最起码以我有限的开发经验没想到办法。
…
那么该如何解决这个问题呢?Qt 给出的答案是基于 Qt 元对象系统的一系列机制。
4 朴素的元对象系统
Qt 的元对象系统发展这么久,完善是真的完善,代码多也是真的多!在迷失于复杂繁琐的源代码中之前,不妨先来设计一个简单的元对象系统来帮助我们理解思想。
4.1 元对象声明
联系前面的元数据的说明,朴素的想法是我们可以用另一个对象来描述这些信息,即元对象,在运行时通过这个对象来获取相关的具体类型等。
根据我们的需要,元对象应该具有以下信息
- 类型名
- 继承的父类信息
- 成员函数的信息
- 内部定义的枚举变量可能也是需要的
- …
看起来像是这样
class MetaObject
{
public:
// 其他成员函数
// ...
private:
// 简单起见,直接用对象了
ClassInfo m_info;
ClassInfo m_superClass;
ClassMethod m_methods;
ClassEnums m_enums;
};
4.2 对C++扩展
为了使我们能在软件系统中有效的管理,我们需要对MyClass做一些拓展,现在MyClass看上去像这样
// MyClass.h
class MyClass : public Object
{
// ... 和之前一样
// 重写一个来自Object的虚方法
virtual const MetaObject *metaObject() const override;
static const MetaObject staticMetaObject; // 一个静态成员
};
现在,只要这个数据能够正确初始化,如果我们需要,我们就可以借助多态的特性,通过接口来获得这个类的相关信息了。
4.3 初始化元对象
那么问题来了,怎么初始化这个变量呢,C++ 作为静态语言,想要获取这些编译期有关的信息,我们只能选择在编译时或者编译前来做这件事,直觉告诉我们,我们要做编译器之前来做这件事,有两个显而易见的原因
- 不要妄图修改编译器,成本巨大且危险
- 直接修改编译器显示不是用户能接受的方式
当然可以手动编写这个文件,把类的信息一个个提炼出来,但是那样太不程序员了,我们需要写一段程序,在编译器之前来做这个事情(你可以把它当成一段生成代码的脚本),我们可以这样做:
- 在我们写的类里面加上一个标记,来表示该类使用了元对象,需要处理并正确初始化 MetaObejct,我们这里假设就用 DEBUG_OBJ 来表示
- 运行我们的程序,如果在某个文件里面发现了标记,解析这个文件,获取他的类型信息(ClassInfo),方法信息(ClassMethod),继承信息等
- 脚本生成了一个 moc_MyClass.cpp 文件,用上述信息初始化 MetaObject,类似于下面这样
// 由脚本生成的文件
// moc_MyClass.cpp
#include "MyClass.h"
// 这里是脚本解析原来头文件生成的数据
// 解析了类的名称,成员,继承关系等等
// ...
const MetaObject MyClass::staticMetaObject = {
// 用解析来的数据来初始化元对象内容
};
const MetaObject *MyClass::metaObject() const
{
return &staticMetaObject;
}
Done!
然后把这个文件也为做源文件一起编译就行了。
4.4 使用元对象
现在再回头来看前面的问题
1)现在直接通过虚函数多态性质拿到 MetaObject,再拿到元数据,比较两个类名是不是一致即可,如果我们采用静态的字符串数组来存类名,甚至我们不需要比较字符串是否一致,只需要比较字符串指针是否相同就可以了。
2)现在直接绑定两个对象的方法字符串即可,我们可以在 MetaObject 提供两各方法
- 检查这两个字符串是否是类的方法(ClassMethod中有没有这个字符串以及参数检查),以判断绑定是否能成功
- 一个统一的调用形式,内部根据字符串来调用相关方法
3)现在你可添加属性,实际添加到元数据中,而存取就像你调用get,set方法一样自然
大功告成,至此,一个丑陋的、不周全的乞丐版元对象系统就设计好了!
5 Qt的解决方案
以下关于元数据部分的内容参考了下面两篇博客,可以作为延伸阅读。
RunningSnail:深入了解Qt(二)之元对象系统(Meta-Object System)
天山老妖S:Qt高级——Qt信号槽机制源码解析
来看一下成熟的解决方案——Qt的元对象系统。
Qt官方文档 的描述是: Qt’s meta-object system provides the signals and slots mechanism for inter-object communication, run-time type information, and the dynamic property system. 即qt元对象系统主要提供了三个能力
- 对象间通信(信号槽机制)
- 运行时信息(类似反射机制)
- 动态的属性系统
根据我们之前分析的乞丐版元对象系统的思想,下面来看以下 Qt 元对象系统是如何构建的,这里笔者环境:win平台vs2017,Qt 版本 5.6.3