Qt的元对象系统

541 阅读10分钟

前言

C++的RTTI机制只能提供有限的类型信息。 即利用dynamic_cast(将基类类型的指针或引用安全地转换为其派生类类型的指针或引用)为typeid(返回指针和引用所指的实际类型)。而Qt构建的元对象系统,使用其基类QObject所创建的派生类对象,可以在运行期间获取该对象的类名成员变量成员函数等信息。Qt也因此实现了强大的信号与槽

RTTI

作用

RTTI(Run-Time Type Information),运行期类型信息。我们知道C++是一种静态类型语言,有关类的信息只在编译期被使用,编译后并未保留,所以在执行期间,无法得知对象的类资料。例如,Dog继承Animal类,若有如下代码:

Animal *p;
p = new Dog();
Animal &q = p;

执行时,pq所指向或参考的类对象资料就无法得知。因此利用RTTI的dynamic_cast和typeid能够保存对象类型信息。

dynamic_cast

指向基类对象的指针转换为指向派生类对象的指针,如果转换失败则返回NULL。因此,dynamic_cast唯一的功能就是判断一个对象具有哪些类型。例如有4个类GrandFather <- Father <- Son <- GrandSon,右边的类分别是左边类的派生类。此时有一个类型为GrandFather* 的指针p,为了判断p所指对象是否具有Son的类型,可以使用语句Son son = dynamic_cast<Son>(p)。如果返回的son不为NULL,则p所指的对象具有Son类型。同理我们也可以得到该对象是否具有Father、GrandSon类型。要对其进行操作,我们需要知道其实际类型。

typeid

等同于sizeof这类的操作符。typeid操作符的返回结果是对Type_info的标准库类型的对象的引用(在头文件typeinfo中定义)。如果表达式的类型是类类型且至少包含有一个虚函数,则typeid操作符返回表达式的动态类型,需要在运行时计算;否则,typeid操作符返回表达式的静态类型,在编译时就可以计算

小结

完整的描述一个类型需要很多信息,例如类的名字、有哪些父类、有哪些成员变量、有哪些成员函数、哪些是public的、哪些是private的、哪些是protected的等等。有时候一个工程项目可能包含成千上万个类,完整的保存这些信息将会消耗大量的内存资源。为了节省内存,C++标准约定typeid只能返回类名。因此,仅靠dynamic_cast和typeid两个关键字提供的类型信息实在有限。 即使仅提供有限的类型信息,RTTI的实现仍然耗费了很大的时间和存储空间,这就会降低程序的性能。另一方面,虽然C++定义了dynamic_cast和typeid两个关键字,但并没有说明如何实现这两个关键字。这就造成了不同的编译器的实现不同,更别说提供RTTI功能的库千差万别。由此导致的最大问题就是程序的可移植性差,项目之间无法完美兼容。

元对象系统

Qt元对象系统的强大在于“即使编译器不支持RTTI,我们也能动态获取类型信息”。例如在任何时候调用QMetaObject::className()函数都会返回类的名称。由于程序运行时保留了类型信息,那么自然就可以进行父子类之间的动态转换。qobject_cast()相比dynamic_cast()强制转换安全得多,而且速度更快。

概念

Qt实现的动态属性系统,一方面是在编译期间声明的属性可以有动态行为,如发出信号、在脚本系统中使用等;另一方面允许应用程序在运行时根据需要向某个对象添加属性。为数据传递、保存、获取提供了极大的灵活性。

例如实现一个多任务下载软件,每个QNetworkReply对象代表了一个网络连接,我们可以把它需要的输入信息(URL)、我们需要的输出信息(保存路径、文件指针)、我们所建立的状态控制逻辑(如超时控制)等等都通过动态属性捆绑到这个对象上而不必另辟空间保存也不需要担心诸如多个任务之间的状态混淆与紊乱等等问题。它的实现基于如下:

  1. QObject类提供了原对象系统的框架实现,使用相关方法都必须是QObject的派生类。
  2. 在类声明的开始,私有访问区,要放置Q_OBJECT宏。这个宏定义会启用元对象系统特性,如信号与槽、动态属性等。
  3. 元对象编译器(the Meta-Object Compiler,简写为moc)为QObject的派生类提供必须的代码来实现元对象系统的特性。 前两个很容易理解,那么moc又是怎样调用的呢。

元对象编译器(moc)

代码

我们以牡丹花的例子,手动添加peony.h、peony.cpp、peonyLover.h三个文件,来弄清Qt Creator编译背后的原理。它会定期开花,广播bloom()信号,开花后会凋谢,广播wizen()信号。如果A游客想去看牡丹,就提供一个槽,连接到这个信号,如果花开,A就可以去看花。

penoy.h
#include <QObject>
#include <QTimer>
#include <QDateTime>
class Peony : public QObject {
	Q_OBJECT	/*可以像任何其他QObject派生的类一样使用信号和槽*/
public:
	Peony();
	void setBloomDate(const QDateTime &date);
	protected slots:
		void onBloomTime();
signals:
		void bloom();
		void wizen();
private:
	QTimer *m_timer;
};

Penoy类定义了一个槽onBloomTime(),两个信号bloom()和wizen()。我们可以给peony类的实例设定一个开花日期,它内部使用QTimer启动一个定时器来实现开花、凋谢的逻辑。

Peony类实现
#include "peony.h"
Peony::Peony() : m_timer(0){
}

void Peony::setBloomDate(const QDateTime &date){
    m_timer = new QTimer(this);
    connect(m_timer, SIGNAL(timeout()), this, SLOT(onBloomTime()));
    m_timer -> setSingleShot(true);
    m_timer -> start(QDateTime::currentDateTime().msecsTo(date));
}//申请了QTimer实例。把QTimer的信号timeout()和Peony的槽onBloomTime()连接起来。设置定时器为单次触发后,计算给定日期距离当前日期的毫秒数,启动定时器。

void Peony::onBloomTime(){
    QDateTime currentDate = QDateTime::currentDateTime();
    m_timer -> disconnect(SIGNAL(timeout()), this);
    connect(m_timer, SIGNAL(timeout()), this, SIGNAL(wizen()));
    m_timer -> start(currentDate.msecsTo(currentDate.addDays(10)));
    emit bloom();
}//先断开timeout()信号与自己的连接,然后把timeout()信号和自己的wizen()信号连接起来,最后设定10天后凋谢,启动定时器。 

tip:信号也可以与信号连接。

PenoyLover.h
#include <QObject>
#include <QDebug>

class PeonyLover : public QObject{
	Q_OBJECT
public:
	PeonyLover(QString name) : m_strName(name) {
	}
	public slots:
		void onPeonyBloom() {
			qDebug() << "peony bloom, let's go ";
		} /*该槽仅仅打印一条调试信息。*/
private:
	QString m_strName;
}; 

main()函数

#include <QCoreApplication>
#include "peony.h"
#include "peonyLover.h"
int main(int argc, char *argv[]){
	QCoreApplication a(argc, argv);
	Peony *peony = new Peony();
	peony -> setBloomDate(QDateTime::currentDateTime().addDays(30));
	PeonyLover * jack = new PeonyLover("Jack");
	QObject::connect(peony, SIGNAL(bloom()), jack, SLOT(onPeonyBloom()));
	PeonyLover * bob = new PeonyLover("Bob");
	QObject::connect(peony, SIGNAL(bloom()), bob, SLOT(onPeonyBloom()));
	return a.exec();
}; 

main()函数创建了一个Peony对象,两个PeonyLover对象,设定peony对象30天后开花,然后衔接了peony与jack及bob。

编译过程

Peony项目构建步骤分为两步,qmake和make.

  1. qmake 首先调用qmake.exe生成Makefile、Makefile.Debug、Makefile.Release,建立了调用moc工具生成附加源文件的依赖规则(如moc_penoy.cpp: moc.exe peony.h -o moc_peony.cpp),然后将这些源文件添加到项目的源文件列表中,便于make工具自动根据这个规则生成moc_peony.cpp和moc_peonyLover.cpp。命令是“qmake peony.pro -r -spec win32-g++”。

  2. make 执行make过程中,make工具默认使用Makefile文件进行构建。而qmake生成的Makefile文件会根据我们选择的构建版本决定调用Makefile.Debug还是Makefile.Release。 make工具会根据Makefile语法规则,执行Makefile中的语句,发现最终的目标文件依赖moc_peony.o而moc_peony.o依赖moc_peony.cpp依赖“moc.exe peony.h -o moc_peony.cpp”,解决依赖后,make会先调用moc.exe生成moc_peony.cpp,再调用g++编译出moc_peony.o,这个过程会针对OBJECTS变量中的.o文件一一执行,当所有.o文件生成后,再把它们链接为目标文件。那么moc_peony.cpp原文件内的内容是什么呢?先来看看Q_OBJECT宏做了什么。

Q_OBJECT宏

#define Q_OBJECT \
public: \
    Q_OBJECT_CHECK \
    static const QMetaObject staticMetaObject; \
    Q_OBJECT_GETSTATICMETAOBJECT \
    virtual const QMetaObject *metaObject() const; \
    virtual void *qt_metacast(const char *); \
    QT_TR_FUNCTIONS \
    virtual int qt_metacall(QMetaObject::Call, int, void **); \
private: \
    Q_DECL_HIDDEN static const QMetaObjectExtraData staticMetaObjectExtraData; \
    Q_DECL_HIDDEN static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **);

Q_OBJECT宏做了这几件事:

  1. 定义一个静态的元对象staticMetaObject
  2. 重载QObject类定义的虚函数qt_metacast()
  3. 国际化支持,QT_TR_FUNCTIONS会被展开为tr()和trUtf8()两个函数
  4. 重载QObject类定义的虚函数qt_metacall()
  5. 定义静态函数qt_static_metacall

QMetaObject

当我们继承QObject类并使用Q_OBJECT宏后,类里就有了一个静态的QMetaObject实例staticMetaObject。这个staticMetaObject会保存定义的类的所有元信息,如类名、信号的名字与索引、槽的名字与索引等等。QMetaObject类的方法:

  1. className()返回类的名字
  2. superClass()返回父类的元对象
  3. method()和methodCount()提供类的信号与槽及其他可以通过名字调用的成员方法的信息
  4. enumerator()和enumeratorCount()提供一个类定义的枚举类型的信息
  5. propertyCount()和property()提供一个类的属性信息
  6. constructor()和constructorCount()提供一个类的构造函数的信息

关键字signals、slots、emit、SIGNALS、SLOTS

signals
signals:
    void bloom();
    void wizen();

关键字宏定义“define signals protected”,在声明类时使用signals,会在预编译阶段展开成protected

emit
emit bloom();

宏定义"define emit",是个空宏,所以"emit bloom()"等同于"bloom()"。

slots
protected slots;
    void onBloomTime();

宏定义"define slots",同样为空宏。

SIGNALS

宏定义"2"#a
使用C语言中的字符串拼接符"#"把传入的信号名字转换成一个数字2开始的字符串。如SIGNAL(clicked())的真身是"2clicked()".

SLOTS

宏定义"1"#a
当使用SLOTS(onBloomTime()),实际上得到以字符串"1onBloomTime()"。Qt会解析这个字符串,根据类型索引1和名字onBloomTime在元数据中查找。\

这几个关键字的定义,一方面是作为助记符方便moc工具扫描头文件提取信号与槽函数,另一方面方便开发人员使用、阅读代码。

moc工作清单

moc_peony.cpp 部分源码,Qt版本为4.8

QT_BEGIN_MOC_NAMESPACE
static const uint qt_meta_data_Peony[] = {

 // content:
       6,       // revision
       0,       // classname
       0,    0, // classinfo
       3,   14, // methods
       0,    0, // properties
       0,    0, // enums/sets
       0,    0, // constructors
       0,       // flags
       2,       // signalCount

 // signals: signature, parameters, type, tag, flags
       7,    6,    6,    6, 0x05,
      15,    6,    6,    6, 0x05,

 // slots: signature, parameters, type, tag, flags
      23,    6,    6,    6, 0x09,

       0        // eod
};

static const char qt_meta_stringdata_Peony[] = {
    "Peony\0\0bloom()\0wizen()\0onBloomTime()\0"
};

void Peony::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
    if (_c == QMetaObject::InvokeMetaMethod) {
        Q_ASSERT(staticMetaObject.cast(_o));
        Peony *_t = static_cast<Peony *>(_o);
        switch (_id) {
        case 0: _t->bloom(); break;
        case 1: _t->wizen(); break;
        case 2: _t->onBloomTime(); break;
        default: ;
        }
    }
    Q_UNUSED(_a);
}

const QMetaObjectExtraData Peony::staticMetaObjectExtraData = {
    0,  qt_static_metacall 
};

const QMetaObject Peony::staticMetaObject = {
    { &QObject::staticMetaObject, qt_meta_stringdata_Peony,
      qt_meta_data_Peony, &staticMetaObjectExtraData }
};

int Peony::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
    _id = QObject::qt_metacall(_c, _id, _a);
    if (_id < 0)
        return _id;
    if (_c == QMetaObject::InvokeMetaMethod) {
        if (_id < 3)
            qt_static_metacall(this, _c, _id, _a);
        _id -= 3;
    }
    return _id;
}

// SIGNAL 0
void Peony::bloom()
{
    QMetaObject::activate(this, &staticMetaObject, 0, 0);
}

// SIGNAL 1
void Peony::wizen()
{
    QMetaObject::activate(this, &staticMetaObject, 1, 0);
}
  1. moc扫描头文件,根据signals和slots提取对应索引信息,保存到全局变量qt_meta_stringdata_Peony中。
  2. 为类的方法、信号、槽、属性、枚举类型等建立各种元信息,保存到全局数组qt_meta_data_Peony中。
  3. 实现Q_OBJECT宏定义的函数,初始化Q_OBJECT宏定义的类型为QMetaObject的静态实例staticMetaObject,把前面提取的各种元信息以及实现的函数都关联到staticMetaObject实例上。
  4. 为Peony类的信号bloom()和wizen()生成模板代码。
信号的真容

以Peony::bloom()函数为例:

void Peony::bloom() {
    QMetaObject::activate(this, &staticMetaObject, 0, 0); 
}

信号就是函数

信号与槽

QMetaObject::activate()是QMetaObject类的内部方法。源码如下:

void QMetaObject::activate(QObject *sender, cosnt QMetaObject *m, int local_signal_index, void **argv);

QMetaObject类的定义(数据成员部分):

struct Q_CORE_EXPORT QMetaObject
{
    /* many methods ... */
    struct { // private data
        const QMetaObject *superdata;
        const char *stringdata;
        const uint *data;
        const void *extradata;
    } d;
}

内部嵌套了一个匿名结构体。传值情况如下: image.png
我们知道Q_OBJECT宏定义了静态实例staticMetaObject,moc_peony.cpp文件,初始化了静态实例,这段代码如下:

const QMetaObject Peony::staticMetaObject = {
    { &QObject::staticMetaObject, qt_meta_stringdata_Peony,
      qt_meta_data_Peony, &staticMetaObjectExtraData }
};

qt_meta_stringdata_Peony,里面保存了构造函数、信号、槽等信息。

static const char qt_meta_stringdata_Peony[] = {
    "Peony\0\0bloom()\0wizen()\0onBloomTime()\0"
};

qt_meta_data_Peony,它包括方法数量、信号个数等信息。以第一个信号bloom()为例,它在数组中的描述是“7,6,6,6,0x05,”依次为函数签名,参数个数,种类······

static const uint qt_meta_data_Peony[] = {

 // content:
       6,       // revision
       0,       // classname
       0,    0, // classinfo
       3,   14, // methods
       0,    0, // properties
       0,    0, // enums/sets
       0,    0, // constructors
       0,       // flags
       2,       // signalCount

 // signals: signature, parameters, type, tag, flags
       7,    6,    6,    6, 0x05,
      15,    6,    6,    6, 0x05,

 // slots: signature, parameters, type, tag, flags
      23,    6,    6,    6, 0x09,

       0        // eod
};

qt_static_metacall函数。
它的第一个参数是QObject,真身是Peony的实例指针*;
第二个参数_c是QMetaObject:Call类型的变量,实际是个枚举值
第三个参数_id,只有在_c取值为QMetaObject::InvokeMetaMethod(调用元方法)时有用,表示信号或槽的序号
第四个参数_avoid* 数组,函数指针的指针只有当_c取值为QMetaObject::IndexOfMethod时才用到

void Peony::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
    if (_c == QMetaObject::InvokeMetaMethod) {
        Q_ASSERT(staticMetaObject.cast(_o));
        Peony *_t = static_cast<Peony *>(_o);
        switch (_id) {
        case 0: _t->bloom(); break;  // 0对应bloom()
        case 1: _t->wizen(); break;  // 1对应wizen()
        case 2: _t->onBloomTime(); break;
        default: ;
        }
    }
    Q_UNUSED(_a);
}
activate

QMetaObject::activate()函数。bloom()函数调用的activate()函数版本,在内部使用元数据对象计算了信号的偏移量后又调用了另一个同名重载函数,原型如下:

static void activate(QObject *sender, const QMetaObject *, int local_signal_index, void **argv);

activate()会调用metacall()或者callFunction()(指向Connection实例的callFunction)来实际调用接收者的槽或信号

int QMetaObject::metacall(QObject *object, Call cl, int idx, void **argv) {
    if (object -> d_ptr -> metaObject) {
        return object -> d_ptr ->metaObject -> metaCall(object, cl, idx, argv);
    }
    else
        return object -> qt_metacall(cl, idx, argv);
}

这段代码会先测试实现槽并接收信号的实例的d_ptr(类型为QObjectPrivate)的metaObject成员(类型为QDynamicMetaObjectData),如果不为空,则调用它的metaCall()的函数如果为空,则调用接受方实例的qt_metacall()虚函数,只要接收方的类是QObject的派生类,这个函数总是存在的。而代码正是从qt_metacall()虚函数跳转到开发者实现的类中的。以moc_peony.cpp代码为例,qt_metacall()代码为:

int Peony::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
    _id = QObject::qt_metacall(_c, _id, _a);
    if (_id < 0)
        return _id;
    if (_c == QMetaObject::InvokeMetaMethod) {
        if (_id < 3)
            qt_static_metacall(this, _c, _id, _a);
        _id -= 3;
    }
    return _id;
}

它先调用父类的qt_metacall()方法,检测返回值,如果小于0则说明父类已处理此次调用;如果大于等于0,则调用前面实现的qt_static_metacall()函数,然后_id减3(即被调用,因为最大才2)。

信号与槽id

qt_metacall()和qt_static_metacall()方法中的id,是利用Connection类的相关函数访问QMetaObject的数据成员来检索类的信号、槽等信息。而QMetaObject的数据成员d中的data、superdata等变量正是从moc_peony.cpp中传递来的,是moc工具完成的接头工具。

参考

《Qt on Android核心编程》
Qt中的元对象系统