原文链接 https://woboq.com/blog/how-qt-signals-slots-work.html Olivier Goffart 于 2012年12月02日
Qt 因其 信号槽机制 而闻名。但它们究竟是如何工作的呢?
在这篇博客文章中,我们将深入探索 QObject 和 QMetaObject 的内部实现,看看 signals 和 slots 在底层是如何运作的。
在本文中,我会展示 Qt5 的部分代码,有时为了排版和简洁性进行了适当修改。
首先,我们回顾一下 signals 和 slots 的基本用法,来看一个官方示例。
头文件如下:
1class Counter : public QObject2{3 Q_OBJECT4 int m_value;5public:6 int value() const { return m_value; }7public slots:8 void setValue(int value);9signals:10 void valueChanged(int newValue);11};在 .cpp 文件中,我们实现 setValue():
xxxxxxxxxx71void Counter::setValue(int value)2{3 if (value != m_value) {4 m_value = value;5 emit valueChanged(value);6 }7}然后可以这样使用 Counter 对象:
x1Counter a, b;2QObject::connect(&a, SIGNAL(valueChanged(int)),3 &b, SLOT(setValue(int)));4
5a.setValue(12); // a.value() == 12, b.value() == 12这是 Qt 自 1992 年诞生以来几乎没有变化的原始语法。
尽管 API 基本没变,但其底层实现经历了多次演进:增加了新特性,也在内部发生了大量变化。 其实并没有任何魔法,本文将向你展示它究竟是如何实现的。
Qt 的 signals/slots 以及属性系统,建立在运行时自省(introspection)能力之上。
所谓自省,就是能够在运行时:
列出对象的方法和属性
获取参数类型、返回值等信息
如果没有这套机制,QtScript 和 QML 几乎不可能实现。
然而,C++ 本身并不支持自省,因此 Qt 提供了一个工具来弥补这一点:MOC。
Important
MOC 是一个代码生成器(而不是预处理器)。
它会解析头文件,并生成一个额外的 moc_xxx.cpp 文件,与项目其余部分一起编译。 这个生成的文件包含了运行时自省所需的全部信息。
Qt 有时会因为引入代码生成器而受到语言纯粹主义者的批评。对此,Qt 在官方文档中这么说:
https://thinkinginqt.com/20251226_why-moc/20251226_why-moc.html
代码生成器没有任何问题,MOC 是一个非常有用的工具。
下面这些都不是 C++ 的关键字
signals
slots
Q_OBJECT
emit
SIGNAL
SLOT
这些被称为 Qt 对 C++ 的扩展,实际上它们都是宏,定义在 qobjectdefs.h 中。
xxxxxxxxxx21#define signals public2#define slots /* nothing */
译者注:
Qt5 中 define signals Q_SIGNALS 定义在 qobjectdefs.h
Qt6 中 define signals Q_SIGNALS 定义在 qtmetamacros.h
没错,signals 和 slots 本质上只是普通函数,编译器会像对待普通函数一样处理它们。
这些宏存在的真正意义是:让 MOC 能识别它们。
Note
在 Qt4 及之前,signals 是 protected; 在 Qt5 中,它们变成了 public,以支持新的连接语法。
xxxxxxxxxx9123 4 5 6 7 89 Q_OBJECT 定义了一系列函数以及一个静态的 QMetaObject。
这些函数全部由 MOC 生成。
xxxxxxxxxx11#define emit /* nothing */
emit 是一个 空宏,MOC 甚至不会解析它。
换句话说:
emit 完全可以不写,仅用于提高代码可读性。
xxxxxxxxxx91Q_CORE_EXPORT const char *qFlagLocation(const char *method);23456789这些宏只是:
把参数转换成字符串
在前面加上标识符(1 或 2)
在 Debug 模式下,还会附加文件和行号,用于在连接失败时输出更友好的警告信息。
为了知道哪些字符串包含行号信息,我们使用 qFlagLocation 函数,该函数会在一个 table 中注册该字符串的地址,并记录两项信息。
下面我们来看 Qt5 中 MOC 生成的部分代码。
xxxxxxxxxx81const QMetaObject Counter::staticMetaObject = {2 { &QObject::staticMetaObject, qt_meta_stringdata_Counter.data,3 qt_meta_data_Counter, qt_static_metacall, Q_NULLPTR, Q_NULLPTR}4};5const QMetaObject *Counter::metaObject() const6{7 return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() : &staticMetaObject;8}这里可以看到 Counter::metaObject() 和 Counter::staticMetaObject 的实现,它们都在 Q_OBJECT 宏中声明。
QObject::d_ptr->metaObject 主要用于 动态元对象(如 QML 对象)
一般情况,metaObject() 直接返回 staticMetaObject
xxxxxxxxxx151struct QMetaObject2{3 /* ... Skiped all the public functions ... */4 enum Call { InvokeMetaMethod, ReadProperty, WriteProperty, /*...*/ };5
6 struct { // private data7 const QMetaObject *superdata;8 const QByteArrayData *stringdata;9 const uint *data;10 typedef void (*StaticMetacallFunction)(QObject *, QMetaObject::Call, int, void **);11 StaticMetacallFunction static_metacall;12 const QMetaObject **relatedMetaObjects;13 void *extradata; //reserved for future use14 } d;15};这里使用 d 是为了表示“私有数据”,但它并未真正设为 private,以便:
保持其为普通数据结构(POD)
支持静态初始化
QMetaObject 使用父对象的元对象(在这里是 QObject::staticMetaObject)作为 superdata 进行初始化。
stringdata 和 data 使用了一些数据进行初始化,这些数据将在本文后面进行说明。
static_metacall 是一个函数指针,被初始化为 Counter::qt_static_metacall。
下面是 QMetaObject 的整型数据
xxxxxxxxxx271static const uint qt_meta_data_Counter[] = {2
3 // content:4 7, // revision5 0, // classname6 0, 0, // classinfo7 2, 14, // methods8 0, 0, // properties9 0, 0, // enums/sets10 0, 0, // constructors11 0, // flags12 1, // signalCount13
14 // signals: name, argc, parameters, tag, flags15 1, 1, 24, 2, 0x06 /* Public */,16
17 // slots: name, argc, parameters, tag, flags18 4, 1, 27, 2, 0x0a /* Public */,19
20 // signals: parameters21 QMetaType::Void, QMetaType::Int, 3,22
23 // slots: parameters24 QMetaType::Void, QMetaType::Int, 5,25
26 0 // eod27};前 13 个整数是 头部信息
若某项有两列:第一列是数量,第二列是数据在数组中的起始索引
方法描述由 5 个整数组成:
名称(字符串表索引)
参数数量
参数描述起始索引
tag
flags
我们暂时忽略 tag 和 flags。对于每个函数,moc 还会将每个参数的返回类型、类型和索引保存到名称中。
xxxxxxxxxx221struct qt_meta_stringdata_Counter_t {2 QByteArrayData data[6];3 char stringdata0[46];4};56 7 8 9 10static const qt_meta_stringdata_Counter_t qt_meta_stringdata_Counter = {11 {12QT_MOC_LITERAL(0, 0, 7), // "Counter"13QT_MOC_LITERAL(1, 8, 12), // "valueChanged"14QT_MOC_LITERAL(2, 21, 0), // ""15QT_MOC_LITERAL(3, 22, 8), // "newValue"16QT_MOC_LITERAL(4, 31, 8), // "setValue"17QT_MOC_LITERAL(5, 40, 5) // "value"18 },19 "Counter\0valueChanged\0\0newValue\0setValue\0"20 "value"21};22这是一个静态的 QByteArray 数组,QT_MOC_LITERAL 宏创建一个静态 QByteArray,该对象引用下面字符串中的特定索引位置。
MOC 生成的 signal 本质上是普通函数,它们只是创建一个指向参数的指针数组,然后将该数组传递给 QMetaObject::activate 函数。
数组的第一个元素是返回值。在我们的示例中,返回值是 void,因此第一个元素为 0。
传递给 activate 函数的第三个参数是信号索引(在本例中为 0)。
xxxxxxxxxx51void Counter::valueChanged(int _t1)2{3 void *_a[] = { Q_NULLPTR, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) };4 QMetaObject::activate(this, &staticMetaObject, 0, _a);5}Slot 可以通过 qt_static_metacall 函数使用索引来调用:
xxxxxxxxxx111void Counter::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)2{3 if (_c == QMetaObject::InvokeMetaMethod) {4 Q_ASSERT(staticMetaObject.cast(_o));5 Counter *_t = static_cast<Counter *>(_o);6 Q_UNUSED(_t)7 switch (_id) {8 case 0: _t->valueChanged((*reinterpret_cast< int(*)>(_a[1]))); break;9 case 1: _t->setValue((*reinterpret_cast< int(*)>(_a[1]))); break;10 default: ;11 }Qt 内部存在 三种索引概念:
相对索引(relative index)
在单个类中,从 0 开始
顺序:signals → slots → methods
绝对索引(absolute index)
包含父类的方法
对外 API 使用(如 QMetaObject::indexOfSignal、QMetaObject::indexOfSlot、QMetaObject::indexOfMethod)
内部 signal index
仅包含 signal
用于优化存储(Qt 4.6 引入)
在使用 Qt 进行开发时,你通常 只需要了解绝对方法索引 即可。
但在阅读和分析 Qt 的 QObject 源码时,必须清楚这三种索引之间的区别。
QObject::connect() 时,Qt 会:
在元对象的字符串表中查找 signal 和 slot
得到它们的索引
创建一个 QObjectPrivate::Connection
加入内部的链表
每个连接需要存储哪些信息?我们需要一种方法,能够 快速访问某个信号索引对应的连接。
由于同一个信号可能连接到多个槽,因此我们需要为 每个信号维护一个已连接槽的列表。
每条连接必须包含 接收者对象(receiver object)以及槽的索引。
我们还希望当接收者被销毁时,这些连接能够自动销毁,所以每个接收者对象也需要知道 谁连接到了它,以便它能清理这些连接。
下面是 qobject_p.h 中定义的 QObjectPrivate::Connection:
xxxxxxxxxx341struct QObjectPrivate::Connection2{3 QObject *sender;4 QObject *receiver;5 union {6 StaticMetaCallFunction callFunction;7 QtPrivate::QSlotObjectBase *slotObj;8 };9 // The next pointer for the singly-linked ConnectionList10 Connection *nextConnectionList;11 //senders linked list12 Connection *next;13 Connection **prev;14 QAtomicPointer<const int> argumentTypes;15 QAtomicInt ref_;16 ushort method_offset;17 ushort method_relative;18 uint signal_index : 27; // In signal range (see QObjectPrivate::signalIndex())19 ushort connectionType : 3; // 0 == auto, 1 == direct, 2 == queued, 4 == blocking20 ushort isSlotObject : 1;21 ushort ownArgumentTypes : 1;22 Connection() : nextConnectionList(0), ref_(2), ownArgumentTypes(true) {23 //ref_ is 2 for the use in the internal lists, and for the use in QMetaObject::Connection24 }25 ~Connection();26 int method() const { return method_offset + method_relative; }27 void ref() { ref_.ref(); }28 void deref() {29 if (!ref_.deref()) {30 Q_ASSERT(!receiver);31 delete this;32 }33 }34};然后,每个对象都会有一个 连接向量(connection vector):它是一个向量,用来把所有信号关联到一个 QObjectPrivate::Connection 的 链表(linked list)。
每个对象还有一个反向连接列表(reversed list),用于在对象销毁时自动删除连接:这是一个双向链表,记录“该对象连接到了哪些地方/被哪些连接引用”。

之所以使用链表,是因为链表可以 快速地插入和删除 节点。它们通过在 QObjectPrivate::Connection 内部保存指向前后节点的指针来实现。
注意:senderList 的 prev 指针是一个指向指针的指针(Connection prev),原因是它并不是直接指向前一个节点,而是指向“前一个节点里 next 指针本身”。这个指针只在销毁连接时使用,而不是用于反向遍历。这样做可以避免对链表第一个元素做特殊处理。

当调用 signal 时,会进入 QMetaObject::activate
以下是其实现的注释版本,来自 qobject.cpp
xxxxxxxxxx971void QMetaObject::activate(QObject *sender, const QMetaObject *m, int local_signal_index, void **argv)2{3 activate(sender, QMetaObjectPrivate::signalOffset(m), local_signal_index, argv);4 /* 这里我们只是转发到下面的函数。5 * 传递的是元对象的 signal offset,而不是 QMetaObject 本身。6 * 之所以拆分成两个函数,是因为 QML 内部会直接调用后一个函数。7 */8}9
10void QMetaObject::activate(QObject *sender, int signalOffset, int local_signal_index, void **argv)11{12 int signal_index = signalOffset + local_signal_index;13
14 /* 首先我们会快速检查一个 64 位的位掩码。15 * 如果它为 0,说明这个信号没有任何连接,16 * 我们可以直接返回。17 * 这意味着:发射一个没有连接任何槽的信号是非常快的。18 */19 if (!sender->d_func()->isSignalConnected(signal_index))20 return; // 没有连接到该信号的槽,也没有 spy(信号监视器)21
22 /* …… 这里省略了一些调试代码、QML 钩子以及健全性检查 …… */23
24 /* 我们在这里加锁一个互斥量,因为对 connectionLists 的所有操作都是线程安全的 */25 QMutexLocker locker(signalSlotLock(sender));26
27 /* 获取该信号对应的 ConnectionList。28 * 这里我做了一些简化,真实代码中还会对列表做引用计数和健全性检查。29 */30 QObjectConnectionListVector *connectionLists = sender->d_func()->connectionLists;31 const QObjectPrivate::ConnectionList *list =32 &connectionLists->at(signal_index);33
34 QObjectPrivate::Connection *c = list->first;35 if (!c) continue;36
37 // 这里需要与 last 进行比较,38 // 以确保在信号发射过程中新增的连接39 // 不会在本次信号发射中被调用40 QObjectPrivate::Connection *last = list->last;41
42 /* 现在开始遍历,对每一个已连接的槽进行处理 */43 do {44 if (!c->receiver)45 continue;46
47 QObject * const receiver = c->receiver;48 const bool receiverInSameThread =49 QThread::currentThreadId() == receiver->d_func()->threadData->threadId;50
51 // 判断该连接是应该立即调用,52 // 还是应该放入事件队列中53 if ((c->connectionType == Qt::AutoConnection && !receiverInSameThread)54 || (c->connectionType == Qt::QueuedConnection)) {55 /* 基本上是复制参数并投递一个事件 */56 queued_activate(sender, signal_index, c, argv);57 continue;58 } else if (c->connectionType == Qt::BlockingQueuedConnection) {59 /* …… 此处省略 …… */60 continue;61 }62
63 /* 辅助结构体,用于设置 sender(),64 * 并在作用域结束时自动恢复65 */66 QConnectionSenderSwitcher sw;67 if (receiverInSameThread)68 sw.switchSender(receiver, sender, signal_index);69
70 const QObjectPrivate::StaticMetaCallFunction callFunction = c->callFunction;71 const int method_relative = c->method_relative;72 if (c->isSlotObject) {73 /* …… 此处省略……74 * Qt5 风格:连接到函数对象(function pointer / functor)75 */76 } else if (callFunction77 && c->method_offset <= receiver->metaObject()->methodOffset()) {78 /* 如果存在 callFunction(即 moc 生成的 qt_static_metacall 函数指针),79 * 我们就调用它。80 * 同时还需要检查保存的 methodOffset 是否仍然有效81 * (因为有可能是在析构函数中被调用)。82 */83 locker.unlock(); // 调用用户代码时不能持有锁84 callFunction(receiver, QMetaObject::InvokeMetaMethod, method_relative, argv);85 locker.relock();86 } else {87 /* 动态对象的兜底处理方式 */88 const int method = method_relative + c->method_offset;89 locker.unlock();90 metacall(receiver, QMetaObject::InvokeMetaMethod, method, argv);91 locker.relock();92 }93
94 // 检查对象是否在槽函数中被删除95 if (connectionLists->orphaned) break;96 } while (c != last && (c = c->nextConnectionList) != 0);97}我们已经看到:
signals / slots 的真实本质
MOC 如何生成元对象信息
连接是如何建立的
信号是如何被高效地触发并分发的
本文尚未涉及 Qt5 新语法(函数指针 / lambda 连接)的实现细节,这将留到下一篇文章中讨论。