Qt信号与槽——原理分析

知其然,知其所以然

今天面试的时候问到了Qt信号槽底层原理的实现,然后发现自己不懂,很尴尬,现在需要过来恶补一下相关的知识,同时也给自己一个教训,以后学习新内容不能只知道怎么用,还要尽可能掌握底层原理的实现。

信号——槽机制复习

我们首先看一下官方文档是如何使用信号与槽的,我们定义一个计数器,其头文件如下:

1
2
3
4
5
6
7
8
9
10
11
class Counter : public QObject
{
Q_OBJECT
int m_value;
public:
int value() const { return m_value; }
public slots:
void setValue(int value);
signals:
void valueChanged(int newValue);
};

在cpp文件中,我们定义setValue函数如下:

1
2
3
4
5
6
7
void Counter::setValue(int value)
{
if (value != m_value) {
m_value = value;
emit valueChanged(value); //发射信号
}
}

我们可以这样使用我们的计数器:

1
2
3
4
5
Counter a, b;
QObject::connect(&a, SIGNAL(valueChanged(int)),
&b, SLOT(setValue(int)));

a.setValue(12); // a.value() == 12, b.value() == 12

从Qt诞生之初,我们就延续着这样的使用方式,但是其具体实现经历了几次变革,我们来看一下现在的Qt是如何实现信号与槽机制的。

MOC——元对象编译

Qt的信号与槽基于运行期对对象的解析,这个解析的意思是能够在运行期列出对象的方法和属性,同时获得这些方法及属性的全部信息,例如变量的类型等等。C++采用RTTI(Runtime Type Information)来获取运行期信息。而Qt自己提供了一套工具实现内省功能,即MOC,一个代码生成器(不是预处理器),MOC会解析头文件,生成一个额外的C++文件,在Qt中我们看到的是文件名中包含moc的文件,该文件包含了需要进行运行期内省(introspection)的代码。

内省(Introspection)是面向对象语言和环境的一个强大特性,内省是对象揭示自己作为一个运行时对象的详细信息的一种能力。这些详细信息包括对象在继承树上的位置,对象是否遵循特定的协议,以及是否可以响应特定的消息

一些宏定义

我们在使用Qt的过程中,经常会用到一些非C++本身的关键字,例如 signals, slots, Q_OBJECT, emit, SIGNAL, SLOT,这些是Qt对C++的一个扩展,实际上是一些简单的宏定义,在qobjectdefs.h文件中给出

1
2
3
#define signals public
#define slots /* nothing */
#define emit /* nothing */

我们可以看到,实际上public和slots根本什么也没有给出,更多的是对开发人员的一种提示,同理还有emit,即使你不写emit,直接写信号函数,也会触发相应的connect,说明信号本身就是一种函数。然后是著名的Q_Object宏,定义如下:

1
2
3
4
5
6
7
8
9
#define Q_OBJECT \
public: \
static const QMetaObject staticMetaObject; \
virtual const QMetaObject *metaObject() const; \
virtual void *qt_metacast(const char *); \
virtual int qt_metacall(QMetaObject::Call, int, void **); \
QT_TR_FUNCTIONS /* translations helper */ \
private: \
Q_DECL_HIDDEN static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **);

我们可以看到,该宏定义了一系列的function和一个静态的QMetaObject,这些函数在哪里实现呢?就在我们的MOC文件中。

对于SLOT和SIGNAL,其定义如下:

1
2
3
4
5
6
7
8
9
Q_CORE_EXPORT const char *qFlagLocation(const char *method);
#ifndef QT_NO_DEBUG
# define QLOCATION "\0" __FILE__ ":" QTOSTRING(__LINE__)
# define SLOT(a) qFlagLocation("1"#a QLOCATION)
# define SIGNAL(a) qFlagLocation("2"#a QLOCATION)
#else
# define SLOT(a) "1"#a
# define SIGNAL(a) "2"#a
#endif

这两个宏定义就是简单地把函数名转换为字符串,然后添加到代码中。

MOC生成了哪些代码

接下来我们分析MOC具体生成的代码有什么作用。

The QMetaObject

1
2
3
4
5
6
7
8
9
10
11
12
//static const QMetaObject staticMetaObject; 

const QMetaObject Counter::staticMetaObject = {
{ &QObject::staticMetaObject, qt_meta_stringdata_Counter.data,
qt_meta_data_Counter, qt_static_metacall, Q_NULLPTR, Q_NULLPTR}
};

//virtual const QMetaObject *metaObject() const;
const QMetaObject *Counter::metaObject() const
{
return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() : &staticMetaObject; //QObject::d_ptr->metaObject 被用于动态元对象,如果是静态的,直接返回staticMetaObject
}

定义在qobjectdefs.h的元对象如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct QMetaObject
{
/* ... Skiped all the public functions ... */

enum Call { InvokeMetaMethod, ReadProperty, WriteProperty, /*...*/ };

struct { // private data
const QMetaObject *superdata;
const QByteArrayData *stringdata;
const uint *data;
typedef void (*StaticMetacallFunction)(QObject *, QMetaObject::Call, int, void **);
StaticMetacallFunction static_metacall;
const QMetaObject **relatedMetaObjects;
void *extradata; //reserved for future use
} d;
};

此处的间接d表示这些对象应当为私有的,但实际并不是私有的,是为了能够静态初始化以及让其作为POD类

POD类类型就是指class、struct、union,且不具有用户定义的构造函数、析构函数、拷贝算子、赋值算子;不具有继承关系,因此没有基类;不具有虚函数,所以就没有虚表;非静态数据成员没有私有或保护属性的、没有引用类型的、没有非POD类类型的(即嵌套类都必须是POD)、没有指针到成员类型的(因为这个类型内含了this指针)。

QMetaObject *superdata和父类的元对象一起初始化(此处为QObject::staticMetaObject,因为Counter继承自QObject),stringdata and data稍后解释。

Introspection Tables——内省表

我们首先分析一下元对象集成的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static const uint qt_meta_data_Counter[] = {

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

// signals: name, argc, parameters, tag, flags
1, 1, 24, 2, 0x06 /* Public */,

// slots: name, argc, parameters, tag, flags
4, 1, 27, 2, 0x0a /* Public */,

// signals: parameters
QMetaType::Void, QMetaType::Int, 3,

// slots: parameters
QMetaType::Void, QMetaType::Int, 5,

0 // eod
};

前13个整数如果包含两列,第一列为计数,第二列表示方法描述在该array中的index,在本例中,我们有2个方法,同时方法的描述从14位开始。方法的描述由5个整数组成,具体如下:

  • name:一个string table中的索引
  • argc:参数个数
  • parameters:参数位置索引
  • tag和flags暂略

对于每个函数,moc还会保存每个parameter的返回类型,以及他们对于name的类型和索引位置。

参考文献

0%