文章目录
前言
笔者日常工作使用Qt开发项目时,发现伴随着文件编译Qt总会自动生成两个文件。其一为ui_XXX.h文件,内部多为UI创建逻辑,并不复杂,而且日常工作中我们也经常手撸UI。反倒是moc_XXX.cpp文件,经常会有一些看不懂的逻辑穿插其中,伴随着各式各样的C++强制转换与宏定义,再点缀一些奇怪的数字表,让人被它的奥秘所深深吸引。所以今日乘此机会查询资料,一窥moc文件的底细。
一、moc简介
- moc 全称是 Meta-Object Compiler,也就是元对象编译器。Qt 程序在交由标准编译器编译之前,先要使用 moc 分析 C++ 代码文件。如果它发现在一个头文件中包含了宏 Q_OBJECT,则会生成另外一个 C++ 源文件。这个源文件中包含了 Q_OBJECT 宏的实现代码。这个新的文件名字将会是原文件名前面加上 moc_ 构成。这个新的文件同样将进入编译系统,最终被链接到二进制代码中去。因此我们可以知道,这个新的文件不是“替换”掉旧的文件,而是与原文件一起参与编译。另外,我们还可以看出一点,moc 的执行是在预处理器之前。因为预处理器执行之后,Q_OBJECT 宏就不存在了。[①qt中moc的作用 - 鬼谷子com - 博客园 (cnblogs.com)]
- 我们可以在Visual Stdio中的项目属性设置中对“元对象编译器”的配置参数进行修改,比如修改名称、路径、修改宏定义等等。
二、Q_OBJECT宏与moc文件结构解析
- 前文提到了moc的机理是扫描C++头文件,并匹配Q_OBJECT关键字。那么Q_OBJECT中到底有什么内容呢?其定义如下:
#define Q_OBJECT \
public: \
Q_OBJECT_CHECK \
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: \
Q_OBJECT_NO_ATTRIBUTES_WARNING \
Q_DECL_HIDDEN_STATIC_METACALL static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **); \
QT_WARNING_POP \
struct QPrivateSignal {}; \
QT_ANNOTATE_CLASS(qt_qobject, "")
- 排除其他较为晦涩的宏定义,我们可以发现一共多了四个函数、一个结构体定义与一个静态成员变量。
三、符号表static const qt_meta_stringdata_XXX与元数据表static const uint qt_meta_data_XXX
- 对于此二者的讲解,此篇博文[②Qt moc_xxx.cpp 文件研究之——反射机制 - 知乎 (zhihu.com)]的解释比较详细,这里直接搬运博主的图片,博文中的示例类sender的定义如下,拥有三个型号和四个槽函数。
3.1、符号表static const qt_meta_stringdata_XXX
- 符号表static const qt_meta_stringdata_XXX={};主要用于记录类名、信号、信号参数、槽函数、槽函数参数,属性名 等字符串变量。且分为两部分存储:QByteArraData数组及stringdata0。其QByteArrayData存入了访问stringdata0子字符串的指针变量及长度,其moc生成的符号表如下:
- 这里注意两个小细节:第一点signalOne与slotOne的参数类型与名称都是(MyString str, MyInt i),所以可以看到符号表中实际上只存在了一份。第二点Qt内置的一些类型如QString、int等并没有被记录在内,而只记录了用户的自定义类型MyString 与MyInt。如此节省内存的小细节,Qt也算是用心良苦了。
- 再仔细一想,符号表中不正是记录下了用户自由发挥的内容吗?如类名可以自定义、函数名称可以自定义、参数类型可以自定义,如此想来,便也合理了。
3.2、QByteArrayData
- QByteArrayData实际是上QArrayData,因为可以在qbytearray.h中找到如下代码
typedef QArrayData QByteArrayData;
,其定义如下:
struct Q_CORE_EXPORT QArrayData
{
QtPrivate::RefCount ref;
int size;
uint alloc : 31;
uint capacityReserved : 1;
qptrdiff offset; // in bytes from beginning of header
void *data()
{
Q_ASSERT(size == 0
|| offset < 0 || size_t(offset) >= sizeof(QArrayData));
return reinterpret_cast<char *>(this) + offset;
}
const void *data() const
{
Q_ASSERT(size == 0
|| offset < 0 || size_t(offset) >= sizeof(QArrayData));
return reinterpret_cast<const char *>(this) + offset;
}
/* XXXX */
}
- 可以看到这里实际上也确实保存了offset,而且通过data函数可以返回其保存的内容。
- 需要注意的是QArrayData本身并不保存字符串,而只是保存索引,字符串文本统一保存在stringdata0中,这样可以节省内存,需要访问的时候通过qt_meta_stringdata_XXX.data[i].data()即可以获取到保存的字符串。
3.3、QT_MOC_LITERAL
- QT_MOC_LITERAL宏相关的定义如下:
#define QT_MOC_LITERAL(idx, ofs, len) \
Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET(len, \
qptrdiff(offsetof(qt_meta_stringdata_LoadingDialog_t, stringdata0) + ofs \
- idx * sizeof(QByteArrayData)) \
)
#define Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET(size, offset) \
Q_STATIC_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET(size, offset)
#define Q_STATIC_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET(size, offset) \
{ Q_REFCOUNT_INITIALIZE_STATIC, size, 0, 0, offset } \
- 可以看到核心逻辑Q_STATIC_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET接收两个参数size与offset并拼凑出对应结构体QArrayData。
四、元数据表static const uint qt_meta_data_XXX
- 元数据表 static const uint qt_meta_data_XXX[] = {};与上文的符号表都是静态变量,只会在内存中存在一份,这与其设计初衷是一致的,因为每个类的实例“特征”必然是相同的。
- 这里依旧搬用博文[②Qt moc_xxx.cpp 文件研究之——反射机制 - 知乎 (zhihu.com)]的图片解释如下:
- 根据注释,我们可以看到数据被分成几块存储,后文我们将逐一讲解。
4.1、content
- content中的内容与QMetaObjectPrivate是一一对应的,具体定义如下[③((一):Qt信号槽原理—元对象与moc | 码农家园 (codenong.com))],这里不加以赘述。
struct QMetaObjectPrivate
{
enum { OutputRevision = 7 }; // Used by moc, qmetaobjectbuilder and qdbus
int revision;
int className;
int classInfoCount, classInfoData;
int methodCount, methodData;
int propertyCount, propertyData;
int enumeratorCount, enumeratorData;
int constructorCount, constructorData; //since revision 2
int flags; //since revision 3
int signalCount; //since revision 4
// revision 5 introduces changes in normalized signatures, no new members
// revision 6 added qt_static_metacall as a member of each Q_OBJECT and inside QMetaObject itself
// revision 7 is Qt 5
//后面的一大堆函数省略
};
4.2、signals及slots
// signals: name, argc, parameters, tag, flags
1, 1, 24, 2, 0x06 /* Public */,
// slots: name, argc, parameters, tag, flags
4, 1, 27, 2, 0x09 /* Protected */,
protected slots:
void slotExit(int iCode);
signals:
void signalExit(int iCode);
- 参看如上源码和moc代码,其中关于信号槽注释的参数是一致的,其中第一个参数name 与第四个参数tag 都是符号表static const qt_meta_stringdata_XXX.data的下标索引,前者代表函数名称,后者代表没有元方法标签描述。
- 第二个参数argc代表函数的参数个数,这一点在下一个模块中有体现。第三个参数parameters 为 24与27,这代表它的参数类型详细描述位于本数组 下标为 24与27的位置。第五个参数为函数访问权限,Qt已经贴心为我们加好了注释。
4.3、parameters
// signals: parameters
QMetaType::Void, QMetaType::Int, 3,
// slots: parameters
QMetaType::Void, QMetaType::Int, 3,
- 依旧参看如上源码,可以发现Void 是返回值类型,Int是参数参数类型,3 代表 static const qt_meta_stringdata_XXX.data的下标索引,也就是我们的参数名称 “iCode”。
4.4、properties
- properties属性并没有在上方出现过,原因是在.h文件中没有使用到Q_PROPERTY宏。而Q_PROPERTY在我们项目中实际使用较少,这里只略微提及,感兴趣的读者可以参考笔者另一篇关于Q_PROPERTY的博文 Qt之Q_PROPERTY使用实例
- 参看如下源码properties定义如下:
Q_PROPERTY(bool selectable READ isSelectable WRITE setSelectable)
// properties: name, type, flags
1, QMetaType::Bool, 0x00095103,
- name是符号表中的索引,type是返回值类型,flags比较麻烦,他是根据PropertyFlags中的定义组合而成的,这里给出PropertyFlags的定义如下:
enum PropertyFlags {
Invalid = 0x00000000,
Readable = 0x00000001,
Writable = 0x00000002,
Resettable = 0x00000004,
EnumOrFlag = 0x00000008,
StdCppSet = 0x00000100,
// Override = 0x00000200,
Constant = 0x00000400,
Final = 0x00000800,
Designable = 0x00001000,
ResolveDesignable = 0x00002000,
Scriptable = 0x00004000,
ResolveScriptable = 0x00008000,
Stored = 0x00010000,
ResolveStored = 0x00020000,
Editable = 0x00040000,
ResolveEditable = 0x00080000,
User = 0x00100000,
ResolveUser = 0x00200000,
Notify = 0x00400000,
Revisioned = 0x00800000
};
五、qt_metacast
- 注:此部分之后的章节将会出现一些C++强转的逻辑,对于不了解的读者,可以参看这篇博文C++类型转换总结【含实例】(explicit、static_cast、dynamic_cast、reinterpret_cast、const_cast)。
- qt_metacast函数的定义如下:
void *LoadingDialog::qt_metacast(const char *_clname)
{
if (!_clname) return nullptr;
if (!strcmp(_clname, qt_meta_stringdata_LoadingDialog.stringdata0))
return static_cast<void*>(this);
return BaseDialog::qt_metacast(_clname);
}
- 其基类中的qt_metacast函数定义如下:
void *BaseDialog::qt_metacast(const char *_clname)
{
if (!_clname) return nullptr;
if (!strcmp(_clname, qt_meta_stringdata_BaseDialog.stringdata0))
return static_cast<void*>(this);
if (!strcmp(_clname, "AbstractBaseImpl"))
return static_cast< AbstractBaseImpl*>(this);
return QDialog::qt_metacast(_clname);
}
- 注意:这里Qt直接对stringdata0进行了匹配,由上文我们得知stringdata0不仅包括类名,还包括其他字符串,但因为保存类名后紧跟着保存了’\0’,所以实际访问stringdata0只会匹配到类名。
- 可以发现qt_metacast的作用是返回指定类型转换后的指针,这是Qt自身提供的程序运行时的类型转换,但一般我们不直接调用,而是调用Qt封装好qobject_cast或inherits。
template <> inline IFace *qobject_cast<IFace *>(const QObject *object) \
{ return reinterpret_cast<IFace *>((object ? const_cast<QObject *>(object)->qt_metacast(IId) : nullptr)); }
inline bool inherits(const char *classname) const
{ return const_cast<QObject *>(this)->qt_metacast(classname) != nullptr; }
auto b = qobject_cast<XXX *>(this);
- qt_metacast 是程序运行时的对象指针转换,它可以将派生类对象的指针安全地转为基类对象指针,如果转换不成功,返回 NULL。这是 Qt 不依赖编译器特性,自己实现的运行时类型转换。它与C++的dynamic_cast主要有几点不同:
- ①dynamic_cast可以转换所有类,但qobject_cast顾名思义只能转换Qt类
- ②qobject_cast转换速度要快于dynamic_cast,原因是二者实现机理不同,qobject_cast本质上是字符串匹配。而dynamic_cast则会真的尝试对类进行转换,如不成功才会返回NULL。笔者实测转换次数为百万量级时,前者耗时14ms,而后者为164ms。
六、staticMetaObject
- staticMetaObject在Q_OBJECT中其已经被声明过
static const QMetaObject staticMetaObject;
- 这里定义如下,我们可以发现我们前文所述的为元对象系统构建的所有信息都被保存在其中。qt_static_metacall是元对象系统的信号槽调用逻辑,后文将会详细介绍。
QT_INIT_METAOBJECT const QMetaObject LoadingDialog::staticMetaObject = { {
&BaseDialog::staticMetaObject,
qt_meta_stringdata_LoadingDialog.data,
qt_meta_data_LoadingDialog,
qt_static_metacall,
nullptr,
nullptr
} };
- 查看staticMetaObject的类型是QMetaObject元对象,这就是封装和处理元对象系统数据的核心类,它的内部有一个关键的私有数据块 d,与上面大括号里的赋值一一对应,它的定义如下:
struct Q_CORE_EXPORT QMetaObject
{
......
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 * const *relatedMetaObjects;
void *extradata; //reserved for future use
} d;
};
七、metaObject()
const QMetaObject *LoadingDialog::metaObject() const
{
return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() : &staticMetaObject;
}
- 代码逻辑的简单理解就是,metaObject函数总是趋于获取基类的staticMetaObject。
- 对于普通的 Qt 图形界面程序,QObject::d_ptr->metaObject 总是为 NULL,只有 QML 界面程序才会使用动态元对象(即dynamicMetaObject)。
八、信号SIGNAL
// SIGNAL 0
void Widget::startReboot(const std::string & _t1, int _t2)
{
void *_a[] = { nullptr, const_cast<void*>(reinterpret_cast<const void*>(&_t1)), const_cast<void*>(reinterpret_cast<const void*>(&_t2)) };
QMetaObject::activate(this, &staticMetaObject, 0, _a);
}
- 可以看到这里moc自动为我们生成了信号的函数定义,内部实现为创建了一个指向参数的指针的数组,数组的第一个元素是返回值。本例中值是nullptr,因为返回值是void。
- activate函数拥有四个参数,第一个参数传递this指针代表实例的作用域。第二个参数为前文提到的静态元对象staticMetaObject,第三个参数是信号的索引(本例中是0),第四个参数便是指针的数组。
- 首先使用无类型指针数组的原因显而易见,因为Qt需要搭配各种变量类型,那么解决方案其一便是C风格的无类型指针,解决方案二是C++风格的模板。显然Qt选择了前者,至于原因,Qt在官方文档的Calling Performance is Not Everything中给出的解释如下[Why Does Qt Use Moc for Signals and Slots? | Qt 5.15],简单总结下就是Qt自己的信号槽机制肯定不如不弱模板快,但是胜在安全,胜在功能强大。
九、static_metacall
- qt_static_metacall在QObject中已有声明,moc后定义如下:
void LoadingDialog::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
if (_c == QMetaObject::InvokeMetaMethod) {
auto *_t = static_cast<LoadingDialog *>(_o);
Q_UNUSED(_t)
switch (_id) {
case 0: _t->signalExit((*reinterpret_cast< int(*)>(_a[1]))); break;
case 1: _t->slotExit((*reinterpret_cast< int(*)>(_a[1]))); break;
default: ;
}
} else if (_c == QMetaObject::IndexOfMethod) {
int *result = reinterpret_cast<int *>(_a[0]);
{
using _t = void (LoadingDialog::*)(int );
if (*reinterpret_cast<_t *>(_a[1]) == static_cast<_t>(&LoadingDialog::signalExit)) {
*result = 0;
return;
}
}
}
}
- 可以发现第二个参数QMetaObject::Call指明了函数执行的代码分支,其定义如下:
enum Call {
InvokeMetaMethod,
ReadProperty,
WriteProperty,
ResetProperty,
QueryPropertyDesignable,
QueryPropertyScriptable,
QueryPropertyStored,
QueryPropertyEditable,
QueryPropertyUser,
CreateInstance,
IndexOfMethod,
RegisterPropertyMetaType,
RegisterMethodArgumentMetaType
};
9.1、QMetaObject::InvokeMetaMethod
- 调用此方法时,可以发现第一个参数是对象实例指针,用于指明函数的作用域。第三个参数_id是信号槽的编号,第四个参数正是我们前文讲解信号时提及的参数数组。它对于参数较多的情况也会一一对应如下:
case 4: _t->slotHandleRebootControlResponse((*reinterpret_cast< int(*)>(_a[1])),(*reinterpret_cast< int(*)>(_a[2])),(*reinterpret_cast< const std::string(*)>(_a[3])),(*reinterpret_cast< int(*)>(_a[4]))); break;
- 因为moc代码是Qt自行根据函数定义所生成的,所以这里的下标访问不会产生越界错误。这同样也解释了为什么信号的参数可以比槽函数的参数多?因为槽函数接收到 _a 指针数组时,只需要取出自己需要的前面几个参数就够了,槽函数不管多余的参数。信号里的参数不能比槽函数里的少,那样槽函数访问指针数组时会越界,造成内存访问错误。
- 另,需要注意的一点是,对于每一个类的moc文件,实际上信号槽的编号都是从0开始的。
9.2、QMetaObject::IndexOfMethod
- IndexOfMethod 部分得代码比较简单,只是做了函数指针类型的比较,如果和信号是一种类型,则在_a[0]中填入该信号的序号,所以达到了获取index的目的。
十、qt_metacall
int LoadingDialog::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
_id = BaseDialog::qt_metacall(_c, _id, _a);
if (_id < 0)
return _id;
if (_c == QMetaObject::InvokeMetaMethod) {
if (_id < 2)
qt_static_metacall(this, _c, _id, _a);
_id -= 2;
} else if (_c == QMetaObject::RegisterMethodArgumentMetaType) {
if (_id < 2)
*reinterpret_cast<int*>(_a[0]) = -1;
_id -= 2;
}
return _id;
}
- 无论是属性的 get/set,还是从信号到槽函数的调用过程等,都可能用到这个函数。
- 我们可以看到qt_metacall实际上是一个递归函数,而且会优先调用基类的qt_metacall函数。因为我们有可能会调用基类的信号槽,但我们知道信号槽的编号都是从0开始的。那我们如果要调用某个类的基类信号槽改如何实现。答案就在于_id参数的递减处理。
- 如下例:我们有爷孙三代派生类A、B、C,他们所拥有的信号槽个数是2、3、4。如果此时我想调用类C的第二个信号槽,那么这里传递的_id就是2+3+2-1=6。
- 首先我们会递归到类A的qt_metacall函数,进入qt_static_metacall函数后,因为_id为6大于其拥有序号为0、1的两个信号槽,故不会发生调用。之后递减操作-2变为了4,同理类B的递归结束后,_id变为了1,之后进入类C的qt_static_metacall中成功调用了第二个信号槽。