Fabian Kosmale 于2020年10月21日星期三| 評論
您可能知道,Qt有一個元類型系統,該系統提供有關類型的運作時動态資訊。它可以将您的類型存儲在QVariant中,并在信号插槽系統中排成隊列,并在整個QML引擎中使用。在即将釋出的Qt 6.0版本中,我們借此機會重新審視了它的基礎知識,并利用了C ++ 17為我們提供的功能。在下文中,我們将檢查這些更改,并說明它們如何影響您的項目。
QMetaType現在更加了解您的類型
在Qt 5中,QMetaType包含預設構造一個類型,複制它并銷毀它所必需的資訊。此外,它知道如何将其儲存到QDataStream以及從QDataStream加載它,并存儲了一些标志來描述它的各種屬性(例如,類型是否瑣碎,枚舉等)。另外,它将存儲該類型的QMetaObject(如果有的話)和一個數字ID,以辨別該類型以及類型名稱。
最後,QMetaType包含用于比較某種(元)類型的對象,進行列印
qDebug
并從一種類型轉換為另一種類型的功能。但是,您必須使用
QMetaType::registerComparators()
QMetaType中的和其他靜态寄存器函數才能真正利用該功能。這會将指向這些函數的指針放入相應的系統資料庫中,基本上是從元類型ID到函數指針的映射。
對于Qt 6,我們要做的第一件事就是擴充存儲在QMetaType中的資訊:Modern C ++已有近10年的曆史了,是以是時候在QMetaType中存儲有關move構造函數的資訊了。為了更好地支援過度對齊的類型,我們現在還存儲您的類型的對齊要求。此外,我們認為注冊管理機構有些笨拙。畢竟,如果
QMetaType::registerEqualsComparator()
僅通過檢視類型就知道了這一點,為什麼還要要求您緻電?是以,在Qt的6, ,,并且已被删除。相反,元類型系統将自動知道這些資訊。這裡的離群值是
QMetaType::registerEqualsComparator
QMetaType::registerComparators
qRegisterMetaTypeStreamOperators
QMetaType::registerDebugStreamOperator
QMetaType::registerConverterFunction
。由于無法可靠地知道應該使用哪些函數進行轉換,并且我們允許注冊基本上任意的轉換,是以該功能與Qt 5中的相同。
通過這些更改,我們還可以統一處理Qt内部類型和使用者注冊的類型:這意味着例如
QMetaType::compare
現在可以使用
int
:
#include <QMetaType>
#include <QDebug>
int main() {
int i = 1;
int j = 2;
int result = 0;
const bool ok = QMetaType::compare(&i, &j, QMetaType::Int, &result);
if (ok) {
// prints -1 as expected in Qt 6
qDebug() << result;
} else {
// This would get printed in Qt 5
qDebug() << "Cannot compare integer with QMetaType :-(";
}
}
QMetaType在編譯時知道您的類型
由于C ++反射功能的各種進步,我們現在可以在編譯時從類型擷取所需的所有資訊,包括名稱。如果您對如何實作此方法感興趣,則應該檢視這個出色的StackOverflow答案。在Qt中,我們使用了一種非常相似的方法,盡管它具有針對舊編譯器的某些擴充和解決方法。比實作更有趣的是對您而言意味着什麼。首先,不要通過任何一個建立QMetaType
QMetaType oldWay1 = QMetaType::fromName("KnownTypeName");
要麼
現在建議您使用以下指令建立QMetaType
QMetaType newWay = QMetaType::fromType<MyType>();
如果您知道類型。其他方法仍然存在,當您在編譯時不知道類型時,這些方法很有用。但是,
fromType
避免
QMetaType
在運作時從ID /名稱到名稱進行一次查找。請注意,從Qt 5.15開始,您已經可以使用
fromType
,但在那裡仍然可以進行查找。而且,您無法複制
QMetaType
,限制了它的用途,并使傳遞類型ID更加友善。但是,在Qt 6中
QMetaType
是可複制的。
您現在可能想知道這對
Q_DECLARE_METATYPE
和意味着什麼
qRegisterMetaType
。畢竟,如果我們可以在編譯時建立QMetaTypes,我們真的需要它們嗎?
讓我們先來看一個例子:
#include <QMetaType>
#include <QVariant>
#include <QDebug>
struct MyType {
int i = 42;
friend QDebug operator<<(QDebug dbg, MyType t) {
QDebugStateSaver saver(dbg);
dbg.nospace() << "MyType with i = " << t.i;
return dbg;
}
};
int main() {
MyType myInstance;
QVariant var = QVariant::fromValue(myInstance);
qDebug() << var;
}
在Qt 5中,這将導緻以下帶有gcc的錯誤消息(+有關執行個體化失敗的更多警告):
/usr/include/qt/QtCore/qmetatype.h: In instantiation of 'constexpr
int qMetaTypeId() [with T = MyType]':
/usr/include/qt/QtCore/qvariant.h:371:37: required from 'static QVariant
QVariant::fromValue(const T&) [with T = MyType]'
test.cpp:16:48: required from here
/usr/include/qt/QtCore/qglobal.h:121:63: error: static assertion failed: Type is
not registered, please use the Q_DECLARE_METATYPE macro to make it known to Qt's
meta-object system
121 | # define Q_STATIC_ASSERT_X(Condition, Message) static_assert(bool(Condition), Message)
|
^~~~~~~~~~~~~~~
/usr/include/qt/QtCore/qmetatype.h:1916:5: note: in expansion of macro 'Q_STATIC_ASSERT_X'
1916 | Q_STATIC_ASSERT_X(QMetaTypeId2<T>::Defined, "Type is not registered, please use the Q_DECLARE_METATYPE macro to make it known to Qt's meta-object system");
那不是很好,但是至少它告訴您您需要使用
Q_DECLARE_METATYPE
。但是,使用Qt 6可以很好地進行編譯,并且可執行檔案将按
QVariant(MyType, MyType with i = 42)
預期列印。不僅QVariant,而且隊列連接配接也可以在沒有explicit的情況下工作
Q_DECLARE_METATYPE
。
現在
qRegisterMetaType
呢?不幸的是,假設您需要輸入鍵入查詢的名稱,仍然需要這樣做。雖然QMetaType對象知道從其構造類型的名稱,但是全局名稱到元類型的映射僅在任一調用時發生
qRegisterMetaType
。為了顯示:
struct Custom {};
const auto myMetaType = QMetaType::fromType<Custom>();
// At this point, we do not know that the name "Custom" maps to the type Custom
int id = QMetaType::type("Custom");
Q_ASSERT(id == QMetaType::UnknownType);
qRegisterMetaType<Custom>();
// from now on, the name -> type mapping works, too
id = QMetaType::type("Custom")
Q_ASSERT(id == myMetaType.id());
如果您使用舊樣式的signal-slot-connections或使用,仍然需要具有可用的類型映射名稱
QMetaObject::invokeMethod
。
QMetaType知道您的屬性和方法的類型
在編譯時建立QMetaType的能力還使我們能夠将類屬性的元類型存儲在其QMetaObject中。此更改主要是由QML推動的,該更改為我們帶來了增強的性能,并有望在未來減少記憶體消耗1個。不幸的是,此更改對屬性聲明中使用的類型提出了新要求:當moc看到該類型時(或者,如果它是指針/引用,則指向類型)需要完整。為了說明此問題,請考慮以下示例:
// example.h
#include <QObject>
struct S;
class MyClass : public QObject
{
Q_OBJECT
Q_PROPERTY(S* m_s MEMBER m_s);
S *m_s = nullptr;
public:
MyClass(QObject *parent = nullptr) : QObject(parent) {}
};
在Qt 5中,這沒有問題。但是,在Qt 6中,您可能會收到類似
In file included from qt/qtbase/include/QtCore/qmetatype.h:1,
from qt/qtbase/include/QtCore/../../../../qtdev/qtbase/src/corelib/kernel/qobject.h:54,
from qt/qtbase/include/QtCore/qobject.h:1,
from qt/qtbase/include/QtCore/QObject:1,
from example.h:1,
from moc_example.cpp:10:
qt/qtbase/include/QtCore/../../../../qtdev/qtbase/src/corelib/kernel/qmetatype.h: In instantiation of 'struct QtPrivate::IsPointerToTypeDerivedFromQObject<S*>':
qt/qtbase/include/QtCore/../../../../qtdev/qtbase/src/corelib/kernel/qmetatype.h:1073:63: required from 'struct QtPrivate::QMetaTypeTypeFlags<S*>'
qt/qtbase/include/QtCore/../../../../qtdev/qtbase/src/corelib/kernel/qmetatype.h:2187:40: required from 'QtPrivate::QMetaTypeInterface QtPrivate::QMetaTypeForType<S*>::metaType'
qt/qtbase/include/QtCore/../../../../qtdev/qtbase/src/corelib/kernel/qmetatype.h:2309:16: required from 'constexpr QtPrivate::QMetaTypeInterface* QtPrivate::qTryMetaTypeInterfaceForType() [with Unique = qt_meta_stringdata_MyClass_t; TypeCompletePair = QtPrivate::TypeAndForceComplete<S*, std::integral_constant<bool, true> >]'
qt/qtbase/include/QtCore/../../../../qtdev/qtbase/src/corelib/kernel/qmetatype.h:2328:55: required from 'QtPrivate::QMetaTypeInterface* const qt_incomplete_metaTypeArray [1]<qt_meta_stringdata_MyClass_t, QtPrivate::TypeAndForceComplete<S*, std::integral_constant<bool, true> > >'
moc_example.cpp:102:1: required from here
qt/qtbase/include/QtCore/../../../../qtdev/qtbase/src/corelib/kernel/qmetatype.h:766:23: error: invalid application of 'sizeof' to incomplete type 'S'
766 | static_assert(sizeof(T), "Type argument of Q_PROPERTY or Q_DECLARE_METATYPE(T*) must be fully defined");
| ^~~~~~~~~
make: *** [Makefile:882: moc_example.o] Error 1
注意靜态斷言,它告訴您必須完全定義類型。可以通過三種不同的方式解決此問題:
- 無需前向聲明該類,隻需包含定義了S的标頭即可。
- 由于包括其他頭可能會對建構時間産生負面影響,是以可以改用
宏。然後,隻有moc才能看到包含。隻需使用Q_MOC_INCLUDE
代替Q_MOC_INCLUDE("myheader.h")
#include "myheader.h"
- 另外,您可以在cpp檔案中包含moc生成的檔案。當然,這需要在其中實際包含所需的标頭。2
最後,在極少數情況下,您會故意使用不透明的指針。在這種情況下,您需要使用
Q_DECLARE_OPAQUE_POINTER
被使用。
盡管在我們的經驗中具有不完整類型的屬性并不常見,但這肯定不是最佳選擇。此外,我們目前正在研究擴充工具支援,以至少自動檢測到此問題。
同樣,我們還嘗試為元對象系統已知的方法的傳回類型和參數(信号,槽和
Q_INVOKABLE
函數)建立元類型。這具有避免在基于字元串的連接配接中以及在QML引擎内部鍵入名稱的麻煩。但是,我們知道不完全類型在方法中很常見。是以,對于方法,我們仍然有一個後備路徑,并且不需要方法類型是完整的,是以在那裡不需要進行任何更改。如果可以的話,我們會在編譯時将元類型存儲在元對象中,如果不能,我們将在運作時簡單地查找它。不過,有一個例外:如果您通過使用聲明性類型注冊宏之一向QML注冊您的類(
QML_ELEMENT
和朋友),我們甚至需要完整的方法類型。在那種情況下,我們假定您公開的所有元方法實際上都是要在QML中使用的,是以您希望避免進行任何其他運作時類型查找(請注意,這不會影響父類的元方法)。
QMetaType為QVariant提供動力
修改了QMetaType之後,我們還可以清理古老的QVariant類的内部。在Qt 6之前,QVariant在内部區分了使用者類型和内置Qt類型,這使類變得非常複雜。同樣的QVariant隻能存儲在了最大的最值的大小
sizeof(void *)
,并
sizeof(double)
在其内部緩沖區。其他任何東西都将配置設定給堆。使用Qt 6時,其他任何東西都将包括諸如QString之類的常用類(因為QString
sizeof(void *)
在Qt 6中是3 *大)。顯然,我們必須對Qt 6進行QVariant返工,并對其進行返工!我們設法簡化了其内部架構,并加快了常見用例的速度。這包括更改QVariant,以便現在将類型存儲
<= 3*sizeof(void *)
在其SSO中緩沖。除了允許在沒有其他配置設定的情況下繼續存儲QString之外,這還使得可以存儲多态PIMPL類型的類型,例如QImage3在QVariant中。這對于傳回data()中的圖像的項目模型将是有益的。
我們還介紹了現有QVariant方法中的某些行為更改。我們知道靜默的行為更改是錯誤的常見來源,但是我們認為目前的行為容易發生錯誤,足以保證它的正确性。以下是更改内容的清單:
- QVariant過去将
調用轉發到其包含的類型-但僅用于Qt自己的類型的有限集合。這已更改,isNull()
現在僅當QVariant為空或包含時傳回trueisNull()
。nullptr
-
現在将QVariantoperator==
用于比較。這意味着對于某些圖形類型(如QPixmap,QImage和QIcon)的行為更改将在Qt 6中永遠不會比較相等(因為它們沒有比較運算符)。而且,現在不再通過QMetaType::equals
來比較QVariant内部的浮點數,而是使用精确比較。qFuzzyCompare
另一個值得注意的變化是,我們删除了使用QDataStream的QVariant的構造函數。與其構造一個持有QDataStream的QVariant(與其他構造函數一緻),不如嘗試從資料流中加載QVariant。如果您确實想要這種行為,請
operator>>
改用。還請注意,
QVariant::Type
在Qt 6中已棄用了它及其相關方法(但仍然存在)。
QMetaType::Type
已添加使用的替代API 。這很有用,因為
QVariant::type()
隻能傳回
QVariant::UserType
使用者類型,而新的
QVariant::typeId()
總是傳回具體的元類型。
QVariant::userType
這樣做(在Qt 5中已經這樣做了),但是從它的名字來看,它還不能用于内置類型。
最後,我們向QVariant添加了一些新功能:
-
可用于比較兩個變體。它傳回一個QVariant::compare(const Variant &lhs, const QVariant &rhs)
。如果值不可比(因為類型不同,或者因為類型本身不具有可比性),std::optional<int>
則傳回。否則,傳回包含int的可選。如果所包含的值in中的值std::nullopt
小于,則為負數lhs
;如果相等,則為0;否則為正數。rhs
- 現在可以從QMetaType構造一個空的QVariant(而不是傳入QMetaType :: Type,然後将其用于構造QMetaType)。由于類似的原因,可以将QMetaType傳遞給該
函數。convert
- 由于QMetaType在Qt 6中存儲對齊資訊,是以QVariant現在支援存儲超對齊類型。
結論與展望
Qt的元類型系統的内部是Qt的一部分,大多數使用者很少與之互動。但是,它是架構的核心,用于實作更多以使用者為中心的部分,例如QML,QVariant,QtDbus,Qt Remote Objects和ActiveQt。借助Qt 6中的更新,我們希望它在下一個十年中能夠像上一個一樣為我們服務。
說到下一個十年,您可能想知道元類型系統的未來将如何發展。除了我們已經提到的使用它來增強QML引擎的計劃之外,我們還打算改善信号/插槽連接配接邏輯。這些更改都不應該以任何方式影響您的代碼,而隻是在幾個地方提高性能和記憶體使用率。在更遠的将來,我們當然也将監視C ++的發展,特别是涉及靜态反射和元類時。盡管我們預計moc不會很快消失,但我們确實考慮在它們廣泛可用後,将其某些功能替換為C ++功能。
哦,順便說一句,我們在Qt 6.0中又增加了一項新功能:QMetaContainer。你問什麼 好吧,請關注此空間,以備即将釋出的另一篇部落格文章,您将知道。
- 在5.15和6.0中,QML引擎将資訊從屬性元類型複制到名為PropertyCache的自定義資料結構中。通過使屬性元類型可用,我們已經可以加快速度,因為我們不必按名稱查找元類型。在即将釋出的版本中,我們希望完全删除PropertyCache,而是重新使用元對象中的元類型。↩︎
- 這樣做也可能會縮短您的建構時間,這是一個受歡迎的副作用。↩︎
- 雖然這取決于挂起的更改以使QPaintDevice變小。↩︎