[翻译] [Qt] 通过 QMetaType 获取类型信息

2020-10-05 01:14:58 +08:00
 wzzzx

来源: https://woboq.com/blog/qmetatype-knows-your-types.html

QMetaTypeQt用来获取类型动态运行信息的工具。通过它能够使诸如QVariant的类能够封装自定义类型,复制信号队列中的参数等事情。 如果你想知道Q_DECLARE_META_TYPEqRegisterMetaType是做什么的,该什么时候使用,可以读一下。这篇文章会告知关于QMetaType的相关信息:为什么需要这么一个类,什么时候使用它,它是如何工作的。

为什么 Qt 需要运行时动态类型信息

首先需要回顾一下Qt的历史。QMetaTypeQt4.0被引入。创建这样一个类是为了解决异步的信号机制(Qt::QueuedConnection)。为了使队列中等待调用的槽函数能够正常工作,信号中参数会被复制和存储起来,以便事件循环函数能够正确调用。 为了能够正常调用队列中的槽函数,信号中的参数会被复制,存储在稍后处理的事件中。同时,当完成槽函数的调用后,还需要删除这些副本。但是使用Qt::DirectConnection参数的连接就不需要了,槽函数可以直接使用栈上的参数。 在QMetaObject::activate中用于分发信号的代码有一个指针数组指向信号参数。但是Qt知道的参数类型都是moc所导出的名称字符串。 QMetaType提供了一种方式能够直接从字符串,例如"QPoint",去获取副本和销毁对象的方法。Qt能够使用void *QMetaType::create(int type, void *copy)QMetaType::destroy(int type, void *data)来拷贝和销毁参数。对于int类型,则可以使用QMetaType::type(const char *typeName)来获取,这些都是moc所提供的能力。QMetaType还提供了方法让开发者能够注册任意类型到元对象数据库中。 另一个例子是QVariant。在Qt3.x版本中的QVariant仅支持内置类型,因为QVariant所包含的类型需要跟QVariant一起被拷贝和销毁。但是有QMetaType后,QVariant能够封装任意已注册的类型,因为QVariant能够跟拷贝和销毁这些对象实例。

QMetaType 持有什么信息

Qt4.0之后很多东西发生了改变。QtScriptQML加强了对动态类型集成的使用,所以需要额外的进行很多优化。 下面是每个类型在元对象系统中会持有的信息:

QTypeInfo

QTypeInfo是一个与QMetaType相互正交的trait class,它允许开发者通过Q_DECLARE_TYPEINFO手动指定类型是否可以借助memmove移动或类型的构造函数 /析构函数是否能够运行。QTypeInfo主要用于优化容器的存储。 例如,隐式分享类可以通过memmove移动。而普通的拷贝则需要在拷贝构造函数中增加引用计数,在析构函数用减少引用计数。 C++11引入移动构造函数和标准traits类型来解决这个问题,但是QTypeInfo是在C++11之前设计出来的,并且需要兼容一些旧版本的编译器,所以只好将就使用。

工作方式

因为一些历史原因,对于内置类型和自定义类型有不同的处理方式。对于QtCore中的内置类型,每个meta-type函数都指定了特定的函数进行处理。不过在Qt5.0使用模板进行了重构。但让我们感兴趣的是对自定义类型处方式。 QVector<QCustomTypeInfo>这个类含有相关信息和一系列函数指针。

Q_DECLARE_METATYPE

这个宏针对于特殊的类型特化了模板QMetaTypeId。实际上,它特化了模板类QMetaTypeId2和大多数QMetaTypeId2用到的函数。这里并不清楚这背后的原因。也许这样Qt可以添加更多的内置类型而不会破坏以前使用Q_DECLARE_METATYPE的代码。 QMetaTypeId通过调用qMetaType<T>()中的QMetaTypeId::qt_metatype_id在编译时指定类型所对应的元类型id。第一次调用的时候,这个函数会调用QMetaType的内部函数来给自定义类型注册和分配元类型id,同时使用调用这个宏时所填写的名称。这些信息会被存储在一个static变量中。 除了类型名称,其他的信息会通过模板在编译自动推导。

qRegisterMetaType

使用Q_DECLARE_METATYPE注册的类型将在首次使用qMetaTypeId()时进行实际注册并分配一个id。使用QVariant封装一个类型就是典型的例子。但是在连接信号和槽函数的时候并不会注册,这需要你使用qRegisterMetaType进行手动注册。

自动注册

开发者经常在看到编译错误或者运行错误的是才想起来他们忘了注册自定义类型。如果不需要这一步骤,那不是很好吗?其实,Q_DECLARE_METATYPE存在的唯一原因就是需要来获取类型的名称。但是在某些情况下,我们并不需要这个宏也可以在运行时获取到类型名称。例如,对于QList<T>,如果T是已经注册的类型,我们可以在类型系统里检索到,也可以使用"QList<" + QMetaType::name(qMetaTypeId<T>()) + ">"来进行构建。我们可以在一系列模板类上做这种操作,例如QList, QVector, QSharedPointer, QPointer, QMap, QHash...。我们甚至可以在moc的帮助下,直接定义指向QObject子类的指针,使用T::staticMetaObject.className() + "*"即可。在Qt5.5,还能自动声明Q_GADGETQ_ENUM。 这就是Q_DECLARE_METATYPE所作的工作,但是要在Q_PROPERTY或者信号参数中使用自定义类型,还是需要使用qRegisterMetaType注册。但是从Qt5.x开始,如果moc可以确定该类型可以注册为元类型,则moc生成的代码将自动调用qRegisterMetaType

探索

Qt5.0之前,我尝试过能够在不需要名称的情况下摆脱Q_DECLARE_METATYPE的使用,大概的做法如下:

template<typename T> QMetaTypeId {
    static int qt_metatype_id() {
        static int typeId = QMetaType::registerMetaType(/*...*/);
        return typeId;
    }
};

按照C++的标准实现,对于每个类型都会有一个对应的实例来生成QMetaTypeId::qt_metatype_id()::typeId。但是实际上一些编译器和链接器并不遵守这些规则。特别的,在Windows上,即便使用了导出宏,对于每个库还是只会生成一份实例。因此我们始终需要一个我们没有的名称标识符,所有在Qt5上,还是需要通过注册类型的方式来获取名称。

1719 次点击
所在节点    程序员
2 条回复
wzzzx
2020-10-05 01:15:42 +08:00
哈哈,有翻译的不好的地方请多多指教~
QBugHunter
2020-10-06 15:52:14 +08:00
马克一下,话说 qt6 快出了吧😄

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/712554

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX