Qt 元对象系统 & 信号槽机制

头文件扫描 → moc 代码生成 → 生成内容解析 → 运行时信号槽调度


一、整体架构:为什么需要 moc?

C++ 本身 不支持运行时反射。Qt 为了实现信号槽、属性系统、动态调用等能力,需要在 C++ 之上添加一层"元信息"。这层信息由 moc(Meta-Object Compiler) 在编译前自动生成。

Tip

截至目前,C++26已经确认支持反射系统了,据说Qt也已经在积极拥抱新版C++了,看来未来有望摆脱moc,迎接纯血C++!

flowchart LR
    A["Counter.h
(含 Q_OBJECT)"] -->|moc 扫描| B["moc_Counter.cpp
(生成的元对象代码)"] B -->|编译链接| C["可执行文件"] A -->|正常编译| C

核心链路:源码 → moc 预处理 → 生成 moc_*.cpp → 与原始代码一起编译链接


二、头文件扫描规则

2.1 When?

构建系统触发条件
qmake.proHEADERS += xxx.h,qmake 分析文件内容自动生成 moc 规则
CMakeset(CMAKE_AUTOMOC ON) 后,CMake 自动扫描所有 target 源文件
premake-qtfiles { "xxx.h" } 中列出的头文件,premake-qt 插件检测 Q_OBJECT 宏后生成 moc 自定义构建步骤
手动moc Counter.h -o moc_Counter.cpp

2.2 What?

moc 按行扫描源文件,寻找以下关键标记:

标记作用必须条件
Q_OBJECT启用完整的元对象系统(信号、槽、属性、反射)类必须直接或间接继承 QObject
Q_GADGET轻量版,仅启用 Q_ENUM/Q_FLAG/Q_PROPERTY/Q_INVOKABLE,不支持信号槽不需要继承 QObject
Q_NAMESPACE为命名空间启用 Q_ENUM_NS放在 namespace 块中

Important

建议将Q_OBJECT 宏放在类声明的 private 区域(类体的最顶部)。因为他的展开代码中包含private

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#define Q_OBJECT \
public: \
    QT_WARNING_PUSH \
    Q_OBJECT_NO_OVERRIDE_WARNING \
    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 \
private: \
    QT_OBJECT_GADGET_COMMON \
    QT_DEFINE_TAG_STRUCT(QPrivateSignal); \
    QT_WARNING_POP \
    QT_ANNOTATE_CLASS(qt_qobject, "")

根据扫描结果,moc会对包含相关宏的文件进行动态生成,产生moc_<BaseName>.cpp文件,并将其加入编译单元(这个动作由相关的构建工具来完成)。而不包含相关宏的文件则被忽略。


三、moc 生成内容详解

 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
class Counter : public QObject
{
    Q_OBJECT
    Q_PROPERTY(int value READ value WRITE setValue NOTIFY valueChanged)

    // 附加自定义的键值对元信息,可在运行时通过 QMetaObject 查询
    Q_CLASSINFO("Author",  "Demo")
    Q_CLASSINFO("Version", "1.0")

public:
    enum CountDirection {
        Up   = 1,
        Down = -1
    };
    Q_ENUM(CountDirection)  // 将枚举注册到元对象系统, 使其可以通过字符串名进行查询/转换
    explicit Counter(QObject *parent = nullptr)
        : QObject(parent), m_value(0), m_direction(Up) {}

    int value() const { return m_value; }

    // 标记为 Q_INVOKABLE 后,可以通过 QMetaObject::invokeMethod() 调用
    Q_INVOKABLE void reset()
    {
        qDebug() << "[Counter::reset] 通过 Q_INVOKABLE 重置计数器";
        setValue(0);
    }

    Q_INVOKABLE QString describe() const
    {
        return QString("Counter(value=%1, direction=%2)")
            .arg(m_value)
            .arg(m_direction == Up ? "Up" : "Down");
    }

    void setDirection(CountDirection dir) { m_direction = dir; }

public slots:
    void setValue(int newValue)
    {
        if (m_value == newValue)
            return;

        int oldValue = m_value;
        m_value = newValue;

        emit valueChanged(m_value);
    }

    void increment()
    {
        setValue(m_value + static_cast<int>(m_direction));
    }

signals:
    void valueChanged(int newValue);
    void overflowDetected(const QString &message);

private:
    int m_value;
    CountDirection m_direction;
};

以上 Counter 类使用了 Q_OBJECTQ_PROPERTYQ_CLASSINFOQ_ENUMQ_INVOKABLEsignalsslots 等各类元对象宏。moc 扫描到 Q_OBJECT 后,会为其生成 moc_Counter.cpp。下面我们逐段拆解这份生成文件的内容。

3.1 字符串存储(StringRefStorage)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// moc_Counter.cpp (Qt 6.10.1 生成)
template <> constexpr inline auto Counter::qt_create_metaobjectdata<qt_meta_tag_ZN7CounterE_t>()
{
    namespace QMC = QtMocConstants;
    QtMocHelpers::StringRefStorage qt_stringData {
        "Counter",           // 0
        "Author",            // 1
        "Demo",              // 2
        "Version",           // 3
        "1.0",               // 4
        "valueChanged",      // 5
        "",                  // 6 (空标签, tag)
        "newValue",          // 7
        "overflowDetected",  // 8
        "message",           // 9
        "setValue",          // 10
        "increment",         // 11
        "reset",             // 12
        "describe",          // 13
        "value",             // 14
        "CountDirection",    // 15
        "Up",                // 16
        "Down"               // 17
};

所有 类名、方法名、参数名、属性名、枚举键名 全部打表存储在一个紧凑的字符串数组中,运行时通过索引查找。比如 Q_CLASSINFO("Author", "Demo") 对应的就是索引 {1, 2}

这里的 StringRefStorage 类型是一个自定义的结构体类型,用来保存这个字符串数组和一些其他信息,通过可变长模板参数来实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
template <typename... Strings> struct StringRefStorage
{
    static constexpr int StringCount = sizeof...(Strings);
    static constexpr size_t StringSize = stringSizeHelper();
    static_assert(StringSize <= MaxStringSize, "Meta Object data is too big");
    const char *inputs[StringCount];

    constexpr StringRefStorage(const Strings &... strings) noexcept
        : inputs{ strings... }
    { }
	// ......
}

Tip

注释提到这里的数组最长只能有4G(64位)。但是可能还没等到触发断言编译器就崩溃了。

3.2 方法表(qt_methods)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
QtMocHelpers::UintData qt_methods {
    // Signal 'valueChanged'
    QtMocHelpers::SignalData<void(int)>(5, 6, QMC::AccessPublic, QMetaType::Void, {{
        { QMetaType::Int, 7 },
    }}),
    // Signal 'overflowDetected'
    QtMocHelpers::SignalData<void(const QString &)>(8, 6, QMC::AccessPublic, QMetaType::Void, {{
        { QMetaType::QString, 9 },
    }}),
    // Slot 'setValue'
    QtMocHelpers::SlotData<void(int)>(10, 6, QMC::AccessPublic, QMetaType::Void, {{
        { QMetaType::Int, 7 },
    }}),
    // Slot 'increment'
    QtMocHelpers::SlotData<void()>(11, 6, QMC::AccessPublic, QMetaType::Void),
    // Method 'reset'
    QtMocHelpers::MethodData<void()>(12, 6, QMC::AccessPublic, QMetaType::Void),
    // Method 'describe'
    QtMocHelpers::MethodData<QString() const>(13, 6, QMC::AccessPublic, QMetaType::QString),
};

每个方法条目都记录了:

  • 名称索引(指向字符串表)
  • 标签索引(为方法添加标签)
  • 访问级别(Public / Protected / Private)
  • 返回类型
  • 参数列表(每个参数的 QMetaType + 名称索引)

注意区别:SignalDataSlotDataMethodData 对应 signalsslotsQ_INVOKABLE 三种来源。运行时通过 QMetaMethod::methodType() 可以区分它们。

Note

方法在表中的 顺序就是方法 IDvalueChanged = 0, overflowDetected = 1, setValue = 2, increment = 3, reset = 4, describe = 5。信号始终排在最前面,这个顺序在信号发射时至关重要。

3.3 属性表、枚举表与类信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 属性表
QtMocHelpers::UintData qt_properties {
    // property 'value'
    QtMocHelpers::PropertyData<int>(14, QMetaType::Int,
        QMC::DefaultPropertyFlags | QMC::Writable | QMC::StdCppSet, 0),
    // 末尾的 0 就是 NOTIFY 信号的索引 → 即 valueChanged (方法ID=0)
};

// 枚举表
QtMocHelpers::UintData qt_enums {
    // enum 'CountDirection'
    QtMocHelpers::EnumData<enum CountDirection>(15, 15, QMC::EnumFlags{}).add({
        {   16, CountDirection::Up },    // "Up" = 1
        {   17, CountDirection::Down },  // "Down" = -1
    }),
};

// Q_CLASSINFO
QtMocHelpers::ClassInfos qt_classinfo({
        {    1,    2 },  // "Author" = "Demo"
        {    3,    4 },  // "Version" = "1.0"
});

这三张表让运行时可以:

  • 通过 QObject::property("value") 反射式读写属性
  • 通过 QMetaEnum::valueToKey(1) 把枚举值 1 转为字符串 "Up"
  • 通过 QMetaObject::classInfo(i) 查询附加的键值对元信息

3.4 staticMetaObject —— 类的元信息核心

1
2
3
4
5
6
7
8
9
Q_CONSTINIT const QMetaObject Counter::staticMetaObject = { {
    QMetaObject::SuperData::link<QObject::staticMetaObject>(),  // 父类元对象(继承链)
    qt_staticMetaObjectStaticContent<qt_meta_tag_ZN7CounterE_t>.stringdata,  // 字符串表
    qt_staticMetaObjectStaticContent<qt_meta_tag_ZN7CounterE_t>.data,        // 元数据
    qt_static_metacall,     // 调度函数指针
    nullptr,
    qt_staticMetaObjectRelocatingContent<qt_meta_tag_ZN7CounterE_t>.metaTypes,  // 类型信息
    nullptr
} };

这就是 Q_OBJECT 宏中声明的那个 static const QMetaObject staticMetaObject 的定义。它是整个元对象系统的入口,运行时所有的反射操作都从这里开始。

QMetaObject 的结构在 Qt 源码 qobjectdefs.h 中定义如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
struct QMetaObject
{
    struct { // d
        const QMetaObject *superdata;     // 父类的 QMetaObject
        const uint *stringdata;           // 字符串表
        const uint *data;                 // 元数据整型数组
        typedef void (*StaticMetacallFunction)(QObject *, QMetaObject::Call, int, void **);
        StaticMetacallFunction static_metacall;  // 调度函数
        const SuperData *relatedMetaObjects;
        const QtPrivate::QMetaTypeInterface *const *metaTypes;
        void *extradata;
    } d;
};

3.5 qt_static_metacall —— 信号-槽-属性的调度核心

这是整个 moc 生成代码中最关键的函数,它像一个总调度器,根据 QMetaObject::Call 的类型和方法 ID,路由到具体的 C++ 函数:

 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void Counter::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
    auto *_t = static_cast<Counter *>(_o);

    // 调用方法(信号/槽/Q_INVOKABLE)
    if (_c == QMetaObject::InvokeMetaMethod) {
        switch (_id) {
        case 0: _t->valueChanged((*reinterpret_cast<std::add_pointer_t<int>>(_a[1])));  break;
        case 1: _t->overflowDetected((*reinterpret_cast<std::add_pointer_t<QString>>(_a[1])));  break;
        case 2: _t->setValue((*reinterpret_cast<std::add_pointer_t<int>>(_a[1])));  break;
        case 3: _t->increment();  break;
        case 4: _t->reset();  break;
        case 5: { QString _r = _t->describe();
            if (_a[0]) *reinterpret_cast<QString*>(_a[0]) = std::move(_r); }  break;
        }
    }

    // 信号的函数指针 → 索引映射(connect 时使用)
    if (_c == QMetaObject::IndexOfMethod) {
        if (QtMocHelpers::indexOfMethod<void (Counter::*)(int)>(_a, &Counter::valueChanged, 0))
            return;
        if (QtMocHelpers::indexOfMethod<void (Counter::*)(const QString &)>(_a, &Counter::overflowDetected, 1))
            return;
    }

    // 读属性
    if (_c == QMetaObject::ReadProperty) {
        void *_v = _a[0];
        switch (_id) {
        case 0: *reinterpret_cast<int*>(_v) = _t->value(); break;  // 调用 READ 函数
        }
    }

    // 写属性
    if (_c == QMetaObject::WriteProperty) {
        void *_v = _a[0];
        switch (_id) {
        case 0: _t->setValue(*reinterpret_cast<int*>(_v)); break;  // 调用 WRITE 函数
        }
    }
}

四个分支各自负责:

QMetaObject::Call作用触发场景
InvokeMetaMethodswitch-case 调用对应的信号/槽/Q_INVOKABLE 函数emitinvokeMethod()、信号槽连接触发
IndexOfMethod函数指针映射为方法 IDQObject::connect 新式写法(函数指针连接)
ReadProperty调用属性的 READ 函数QObject::property("value")
WriteProperty调用属性的 WRITE 函数QObject::setProperty("value", 42)

参数传递使用 void **_a 数组:_a[0] 是返回值,_a[1] 起是各个参数,通过 reinterpret_cast 还原类型。这就是 Qt 信号槽的 类型擦除 机制。

3.6 信号的函数体(moc 生成)

信号只需要在头文件里 声明不需要也不能自己实现signals: 区域下的函数没有函数体)。moc 会自动生成信号的实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// SIGNAL 0
void Counter::valueChanged(int _t1)
{
    QMetaObject::activate<void>(this, &staticMetaObject, 0, nullptr, _t1);
}

// SIGNAL 1
void Counter::overflowDetected(const QString & _t1)
{
    QMetaObject::activate<void>(this, &staticMetaObject, 1, nullptr, _t1);
}

当你写 emit valueChanged(42) 时:

  1. emit 是一个空宏(#define emit),所以 emit valueChanged(42) 就是 valueChanged(42)
  2. 调用上面这个 moc 生成的函数体
  3. 该函数调用 QMetaObject::activate(),传入 thisstaticMetaObject、信号索引 0、以及参数

activate() 内部会遍历这个信号上所有已注册的连接(connection list),根据连接类型决定是 直接调用 还是 投递到事件队列

3.7 qt_metacall —— 虚函数调度入口

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
int Counter::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
    _id = QObject::qt_metacall(_c, _id, _a);  // 先让父类处理(如 QObject 自己的属性 "objectName")
    if (_id < 0)
        return _id;

    if (_c == QMetaObject::InvokeMetaMethod) {
        if (_id < 6)                              // Counter 有 6 个方法
            qt_static_metacall(this, _c, _id, _a);
        _id -= 6;
    }
    // ... ReadProperty / WriteProperty 同理,Counter 有 1 个属性
    return _id;
}

这个函数是 Q_OBJECT 宏中声明的 virtual int qt_metacall(...) 的实现。它的作用是将 全局方法 ID 按继承链分段:父类处理父类的,本类处理本类的。_id -= 6 就是把本类的 6 个方法"消费"掉,剩下的 ID 留给子类。

3.8 metaObject()qt_metacast()

1
2
3
4
const QMetaObject *Counter::metaObject() const
{
    return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() : &staticMetaObject;
}

metaObject() 通常直接返回 &staticMetaObject。只有当对象有 动态属性(通过 setProperty 设置了未在 Q_PROPERTY 中声明的属性)时,才会返回 dynamicMetaObject()

1
2
3
4
5
6
7
void *Counter::qt_metacast(const char *_clname)
{
    if (!_clname) return nullptr;
    if (!strcmp(_clname, qt_staticMetaObjectStaticContent<...>.strings))
        return static_cast<void*>(this);
    return QObject::qt_metacast(_clname);   // 沿继承链向上查找
}

qt_metacast 是 Qt 自己的类型转换机制(类似 dynamic_cast,但不依赖 C++ RTTI),qobject_cast<Counter*>(obj) 内部就是调用它。


四、信号槽连接的底层机制

4.1 QObject::connect 做了什么?

Qt 有两种 connect 风格,它们的底层路径不同:

旧式(字符串匹配)

1
connect(&counter, SIGNAL(valueChanged(int)), &notifier, SLOT(onValueChanged(int)));

SIGNAL()SLOT() 是宏,展开后在方法名前加一个标识字符:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# define QMETHOD_CODE  0                        // member type codes
# define QSLOT_CODE    1					  // SLOT 为 1
# define QSIGNAL_CODE  2					  // SIGNAL 为 2
# define QT_PREFIX_CODE(code, a) QT_STRINGIFY(code) #a
# define QT_STRINGIFY_METHOD(a) QT_PREFIX_CODE(QMETHOD_CODE, a)
# define QT_STRINGIFY_SLOT(a) QT_PREFIX_CODE(QSLOT_CODE, a)
# define QT_STRINGIFY_SIGNAL(a) QT_PREFIX_CODE(QSIGNAL_CODE, a)
# define SLOT(a)     QT_STRINGIFY_SLOT(a)
# define SIGNAL(a)   QT_STRINGIFY_SIGNAL(a)

// qobjectdefs.h
#define SLOT(a)   "1"#a   // "1onValueChanged(int)"
#define SIGNAL(a) "2"#a   // "2valueChanged(int)"

connect 内部用这个字符串到 QMetaObject 的方法表中逐一比较签名,找到方法 ID 后建立连接。

新式(函数指针)

1
connect(&counter, &Counter::valueChanged, &notifier, &Notifier::onValueChanged);

connect 内部将函数指针传给 qt_static_metacall(_, IndexOfMethod, _, _),让 moc 生成的代码直接 把函数指针映射为方法 ID,编译期就能做类型检查。

两种方式最终都会创建一个 Connection 对象,追加到发送者的连接列表中:

1
2
3
4
5
6
7
8
struct Connection {
    QObject    *sender;
    QObject    *receiver;
    int        signal_index;     // 信号在 sender 的方法表中的 ID
    int        method_index;     // 槽在 receiver 的方法表中的 ID(或 -1 表示 lambda)
    uint       connectionType;   // Direct / Queued / Auto ...
    // ...
};

4.2 emit signal() 做了什么?

emit counter.valueChanged(42) 为例,完整调用链为:

 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
28
emit counter.valueChanged(42)
  
  ├─ emit 是空宏,等价于 counter.valueChanged(42)
  
  
Counter::valueChanged(int _t1)           moc 生成的信号函数体
  
  ├─ 将参数打包到 void*[] 数组
  
  
QMetaObject::activate(this, &staticMetaObject, 0 /*signalIdx*/, nullptr, _t1)
  
  ├─ 查找 signalIdx=0 对应的连接列表
  ├─ 遍历所有 Connection:
      ├─ DirectConnection:
           直接调用 receiver->qt_static_metacall(_, InvokeMetaMethod, slotId, args)
            └─ switch(slotId)  调用实际的 C++ 槽函数
      ├─ QueuedConnection:
           将参数深拷贝,封装为 QMetaCallEvent
            └─ 投递到 receiver 所在线程的事件队列
                └─ 事件循环取出后再调用 qt_static_metacall
      └─ AutoConnection:
            判断 sender  receiver 是否在同一线程
             ├─ 同线程  Direct
             └─ 跨线程  Queued
  
  
 返回

4.3 连接类型对比

类型行为线程安全使用场景
AutoConnection同线程→Direct;跨线程→Queued默认且最常用
DirectConnection在发射者线程中立即同步调用槽❌(槽需自行保证线程安全)同线程、需要立即响应
QueuedConnection参数拷贝后投递到接收者线程的事件队列跨线程通信
BlockingQueuedConnection同 Queued,但发射者阻塞直到槽完成⚠ 死锁风险需要等待跨线程结果
UniqueConnection防止重复连接(可与上述 OR 组合)防止重复 connect

Important

QueuedConnection 要求参数类型必须已通过 qRegisterMetaType<T>() 注册(或是 Qt 内建类型如 intQString),因为参数需要被拷贝并序列化到事件中。

对于挂载在同一个信号的多个槽函数,根据FIFO的规则进行顺序调用。


五、Q_PROPERTY 属性系统

1
Q_PROPERTY(int value READ value WRITE setValue NOTIFY valueChanged)

这行声明告诉 moc 为 Counter 注册一个名为 "value" 的属性。moc 为它生成了 ReadPropertyWriteProperty 的 switch 分支(见 3.5 节)。

运行时通过 QObject 的通用接口访问:

1
2
3
4
5
6
7
// 反射式读写(不需要知道具体类型)
QVariant v = counter.property("value");       // 内部: qt_static_metacall(_, ReadProperty, 0, _)
counter.setProperty("value", 100);            // 内部: qt_static_metacall(_, WriteProperty, 0, _)

// 动态属性(未在 Q_PROPERTY 中声明)
counter.setProperty("customTag", "hello");    // 存储在 QObject 内部的 QVariantMap 中
counter.property("customTag");                // 照样可以读取

动态属性不会出现在 QMetaObject 的属性表中,也不会触发 moc 生成的 ReadProperty/WriteProperty 分支。

常用的属性声明关键字:

关键字必需说明
READ读取函数(const 成员函数)
WRITE写入函数
NOTIFY属性变化时发射的信号(QML 绑定必需)
MEMBER直接绑定到成员变量(Qt5.1+),可替代 READ + WRITE
CONSTANT属性不可变,不可与 NOTIFY / WRITE 共存
BINDABLEQt6 绑定属性(QBindable<T>

六、Q_ENUM 与枚举反射

1
2
enum CountDirection { Up = 1, Down = -1 };
Q_ENUM(CountDirection)

注册后,moc 在枚举表中写入了 {16, Up}, {17, Down} 这样的 key→value 映射。运行时可以:

1
2
3
4
QMetaEnum me = QMetaEnum::fromType<Counter::CountDirection>();
me.valueToKey(1);        // → "Up"
me.keyToValue("Down");   // → -1
qDebug() << Counter::Up; // 输出 "Counter::Up" 而非 "1"

七、Q_INVOKABLE 与动态调用

1
2
Q_INVOKABLE void reset();
Q_INVOKABLE QString describe() const;

标记为 Q_INVOKABLE 的函数在方法表中类型为 MethodData(既不是 Signal 也不是 Slot),但同样通过 qt_static_metacallInvokeMetaMethod 分支调用。运行时可以通过字符串名调用:

1
2
3
4
5
6
7
8
// 无返回值
QMetaObject::invokeMethod(&counter, "reset");

// 有返回值
QString result;
QMetaObject::invokeMethod(&counter, "describe",
                          Qt::DirectConnection,
                          Q_RETURN_ARG(QString, result));

这也是 QML 调用 C++ 方法 的底层机制——QML 引擎通过 QMetaObject 查找方法名,然后走 invokeMethod 路径。