天天看點

QT6 QMetaType + QVariant的新增功能

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
           

注意靜态斷言,它告訴您必須完全定義類型。可以通過三種不同的方式解決此問題:

  1. 無需前向聲明該類,隻需包含定義了S的标頭即可。
  2. 由于包括其他頭可能會對建構時間産生負面影響,是以可以改用

    Q_MOC_INCLUDE

    宏。然後,隻有moc才能看到包含。隻需使用

    Q_MOC_INCLUDE("myheader.h")

    代替

    #include "myheader.h"

  3. 另外,您可以在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過去将

    isNull()

    調用轉發到其包含的類型-但僅用于Qt自己的類型的有限集合。這已更改,

    isNull()

    現在僅當QVariant為空或包含時傳回true 

    nullptr

  • operator==

    現在将QVariant

    QMetaType::equals

    用于比較。這意味着對于某些圖形類型(如QPixmap,QImage和QIcon)的行為更改将在Qt 6中永遠不會比較相等(因為它們沒有比較運算符)。而且,現在不再通過

    qFuzzyCompare

    來比較QVariant内部的浮點數,而是使用精确比較。

另一個值得注意的變化是,我們删除了使用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>

    。如果值不可比(因為類型不同,或者因為類型本身不具有可比性),

    std::nullopt

    則傳回。否則,傳回包含int的可選。如果所包含的值in中的值

    lhs

    小于,則為負數

    rhs

    ;如果相等,則為0;否則為正數。
  • 現在可以從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。你問什麼 好吧,請關注此空間,以備即将釋出的另一篇部落格文章,您将知道。

  1. 在5.15和6.0中,QML引擎将資訊從屬性元類型複制到名為PropertyCache的自定義資料結構中。通過使屬性元類型可用,我們已經可以加快速度,因為我們不必按名稱查找元類型。在即将釋出的版本中,我們希望完全删除PropertyCache,而是重新使用元對象中的元類型。↩︎
  2. 這樣做也可能會縮短您的建構時間,這是一個受歡迎的副作用。↩︎
  3. 雖然這取決于挂起的更改以使QPaintDevice變小。↩︎

繼續閱讀