Qt核心:元对象系统(1)- 元对象和元数据

本文深入探讨了Qt的元对象系统,解释了元数据和元对象的概念,以及为何需要元对象系统来解决类型转换、对象间通信等问题。通过一个简单的元对象系统设计,展示了如何在运行时获取类的描述信息。Qt的解决方案是通过宏Q_OBJECT扩展C++,使用moc工具生成元对象数据,并集成到Qt的信号槽机制和动态属性系统中。
摘要由CSDN通过智能技术生成


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++ 作为静态语言,想要获取这些编译期有关的信息,我们只能选择在编译时或者编译前来做这件事,直觉告诉我们,我们要做编译器之前来做这件事,有两个显而易见的原因

  1. 不要妄图修改编译器,成本巨大且危险
  2. 直接修改编译器显示不是用户能接受的方式

当然可以手动编写这个文件,把类的信息一个个提炼出来,但是那样太不程序员了,我们需要写一段程序,在编译器之前来做这个事情(你可以把它当成一段生成代码的脚本),我们可以这样做:

  1. 在我们写的类里面加上一个标记,来表示该类使用了元对象,需要处理并正确初始化 MetaObejct,我们这里假设就用 DEBUG_OBJ 来表示
  2. 运行我们的程序,如果在某个文件里面发现了标记,解析这个文件,获取他的类型信息(ClassInfo),方法信息(ClassMethod),继承信息等
  3. 脚本生成了一个 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࿰

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值