在項目開發(fā)過程中,我們可能會遇到這么一種場景:某個或某幾個軟件組件可以產(chǎn)生許多不同類型的數(shù)據(jù),無論是出于性能的考慮,或者是接口簡潔性的考慮,這些數(shù)據(jù)需要被一次性塞到一個類似于數(shù)據(jù)庫的數(shù)據(jù)容器中。而這個容器將會被眾多接收者使用,它們各自從容器中取出自己感興趣的內(nèi)容進行處理。此外,不同的接收者可能運行在不同線程中的,這個容器還需要支持復(fù)制操作,使得這些接受者在訪問時互不干擾。最后,為了便于調(diào)試以及數(shù)據(jù)的離線分析,這個容器還應(yīng)該支持序列化與反序列化。
成都服務(wù)器托管,成都創(chuàng)新互聯(lián)提供包括服務(wù)器租用、綿陽電信機房、帶寬租用、云主機、機柜租用、主機租用托管、CDN網(wǎng)站加速、域名與空間等業(yè)務(wù)的一體化完整服務(wù)。電話咨詢:028-86922220簡而言之,該場景的需求如下:
我們姑且將滿足以上需求的容器稱為可存儲通用類型的容器。本文假設(shè)項目中使用了Qt庫,在此基礎(chǔ)上,為實現(xiàn)這種容器提供了一種可行的思路,并給出實現(xiàn)這種容器的要點。
2. 設(shè)計思路 2.1. 為單個數(shù)據(jù)項選擇合適的容器首先,我們需要解決的問題就是如何存儲各種類型的單個數(shù)據(jù)項。我們根據(jù)需求逐一分析:
void *
,存儲任意類型的數(shù)據(jù)。void *
將無法滿足該需求。這是因為void *
擦除了類型信息,在數(shù)據(jù)項目需要被復(fù)制時,我們已經(jīng)無從得知該數(shù)據(jù)的長度以及其他信息了。那能否用void *
+ size來存儲這些內(nèi)容呢?對于基礎(chǔ)數(shù)據(jù)類型以及其數(shù)組,例如int
、float
以及它們數(shù)組等,這種方案是可以應(yīng)付的。但是,一旦數(shù)據(jù)項目中使用了容器類,例如std::vector
,因為通常情況下,它們實際的存儲空間是在堆中申請的,我們不能直接通過其指針來訪問實際的數(shù)據(jù)內(nèi)容,因此void *
+ size的方案不可行。C++ 17中引入了std::any
用于存儲單個任意類型的對象(包括自定義類型),這似乎是一個很不錯的選擇。std::any
本身并沒有直接支持序列化與反序列化功能,如果每個基礎(chǔ)類型都需要自己再實現(xiàn)一次序列化與反序列,那實在太折騰了。既然我們都已經(jīng)基于Qt庫進行開發(fā)了,那是不是可以直接利用Qt中已經(jīng)提供的各種序列化與反序列化功能(通過QDataStream
)。我們知道,Qt Core模塊中的QVariant
類提供了與std::any
相似的功能,并且更加強大。更幸運地是,在Serializing Qt Data Types列表中,QVarant
赫然在列。綜上,為了避免重復(fù)造輪子,我們選擇了QVariant
作為存儲單個數(shù)據(jù)項目的容器。該類支持對象之間的拷貝,因此,選擇QVariant
讓我們可以同時滿足3個需求,前提是我們使用的數(shù)據(jù)類型都是Qt內(nèi)置的類型。對于自定義類型,我們還需要做一些簡單的開發(fā)工作,這將在后面的小節(jié)中介紹。
在確定了單個數(shù)據(jù)項目的容器之后,我們還需要選擇一個容器來存儲多個數(shù)據(jù)項目,這里假設(shè)使用std::unordered_map<>
,那么,我們只需要按照如下方式定義:
std::unordered_mapcontainer;
我們就得到了一個簡易的,可同時存儲多種數(shù)據(jù)類型的關(guān)聯(lián)容器。接收者通過key值便能夠以正確的方式解析真實的數(shù)據(jù)類型。
由于QVaraiant
是實現(xiàn)通用類型的容器的核心,需要重點介紹一下。
QVariant
類的作用類似于Qt數(shù)據(jù)類型的聯(lián)合,它還能支持用戶自定義的類型。
QVariant
對象一次保存一個type()
的單個值。(有些type()
是多值的,例如字符串列表。)我們可以使用convert()
將其轉(zhuǎn)換為不同的類型,使用眾多toT()
函數(shù)之一(例如,toSize()
)獲取其值,并使用canConvert()
檢查該類型是否可以轉(zhuǎn)換為某個特定類型。
名為toT()
(例如toInt()
、toString()
)的方法是const
方法。如果想要獲取實際存儲的類型,這些函數(shù)會返回存儲對象的副本(注意,這里返回的是副本,所以無法通過返回的值來修改QVariant
存儲的內(nèi)容)。如果想使用可以從存儲的類型生成的類型,toT()
會復(fù)制并轉(zhuǎn)換,并保持對象本身不變。如果請求了一個不能從存儲的類型生成的類型,結(jié)果取決于該類型;有關(guān)的詳細信息,請參見函數(shù)文檔。
下列是官方的實例代碼,它們闡述了如何使用QVariant
:
QDataStream out(...);
QVariant v(123); // The variant now contains an int
int x = v.toInt(); // x = 123
out<< v; // Writes a type tag and an int to out
v = QVariant("hello"); // The variant now contains a QByteArray
v = QVariant(tr("hello")); // The variant now contains a QString
int y = v.toInt(); // y = 0 since v cannot be converted to an int
QString s = v.toString(); // s = tr("hello") (see QObject::tr())
out<< v; // Writes a type tag and a QString to out
...
QDataStream in(...); // (opening the previously written stream)
in >>v; // Reads an Int variant
int z = v.toInt(); // z = 123
qDebug("Type is %s", // prints "Type is int"
v.typeName());
v = v.toInt() + 100; // The variant now hold the value 223
v = QVariant(QStringList());
甚至可以將QList
和QMap
的值存入到一個QVariant
對象中,因此,我們可以輕松地構(gòu)造任意類型的復(fù)雜的數(shù)據(jù)結(jié)構(gòu),并將其存入到QVariant
中。這是非常強大和通用的,但可能比在標準數(shù)據(jù)結(jié)構(gòu)中存儲相應(yīng)類型的內(nèi)存和速度效率低。
QVariant
還支持null的概念,在這種情況下,我們可以定義一個沒有值的類型。但是,請注意,QVariant
類型只有在設(shè)置了值后才能進行強制轉(zhuǎn)換。例如:
QVariant x, y(QString()), z(QString(""));
x.convert(QVariant::Int);
// x.isNull() == true
// y.isNull() == true, z.isNull() == false
除了支持內(nèi)置的類型枚舉中的類型之外,QVariant
可以通過擴展以支持其他類型。如何實現(xiàn)讓QVariant
可識別的類型,參看讓QVariant支持自定義類型的存儲。
首先, 我們需要確保自定義的類型滿足QMetaType
的所有要求。換句話說,它必須提供:
例如,我們有一個MyData
自定義類型:
class MyData {public:
uint32_t dataId;
std::vectordata;
MyData() = default;
~MyData() = default;
MyData(const MyData &) = default;
MyData & operator=(const MyData &) = default;
};
在此基礎(chǔ)上,我們還需要做一些額外的簡單操作,否則,Qt的類型系統(tǒng)將無法理解如何存儲、檢索和序列化該類的實例。例如,我們將無法在QVariant
中存儲MyData
值。
Qt中負責(zé)定制類型的類是QMetaType
。為了讓這個類能識別這個類型,我們在定義MyData
的頭文件中調(diào)用這個類的Q_DECLARE_METATYPE()
宏,代碼如下:
Q_DECLARE_METATYPE(MyData);
這使得將MyData
對象存儲在QVariant
對象中并在以后檢索成為可能。官方文檔也給出了一個示例代碼,請參見自定義類型示例。
如前文提到,我們將使用QDataStream
對QVarant
進行序列化與反序列化。對于自定義類型,我們則需要實現(xiàn)自定義的序列化與反序列化函數(shù)。在這個例子中,我們需要實現(xiàn)兩個MyData
的友元函數(shù):
class MyData {public:
uint32_t dataId;
std::vectordata;
MyData() = default;
~MyData() = default;
MyData(const MyData &) = default;
MyData & operator=(const MyData &) = default;
friend QDataStream & operator<<(QDataStream & stream, const MyData &data);
friend QDataStream & operator>>(QDataStream & stream, MyData &data);
};
QDataStream & operator<<(QDataStream & stream, const MyData &data)
{stream<< data.dataId;
stream<< static_cast(data.data.size());
for (const auto & val : data.data) {stream<< val;
}
return stream;
}
QDataStream & operator>>(QDataStream & stream, MyData &data)
{stream >>data.dataId;
quint64 count = 0;
stream >>count;
uint32_t val = 0;
for (int i = 0; i< count; i++) {stream >>val;
data.data.push_back(val);
}
return stream;
}
在完成以上代碼后,我們便能支持自定義類型的序列化了。代碼如下:
QDataStream stream(...);
MyData data;
data.dataId = 10086;
data.data = {1, 0, 0, 8, 6};
QVariant var;
var.setValue(data);
stream<< var;
若此時我們執(zhí)行反序列化是可行的,因為在setValue()
時,內(nèi)部幫我們在元對象系統(tǒng)中注冊了這個自定義類型。但如果是我們重新運行程序,反序列化就會失效。反序列化時需要重新創(chuàng)建出這個自定義類型的對象,因為此時元對象系統(tǒng)中并不清楚該對象的信息,所以它不知道如何在運行時處理自定義類型對象的創(chuàng)建和銷毀。
要在運行時創(chuàng)建對象,需要通過調(diào)用qRegisterMetaType()
模板函數(shù)將自定義類型注冊到元對象系統(tǒng)。此后,除了讓反序列化時元對象系統(tǒng)可識別之外,這也使該類型可用于隊列式(queued)信號-槽通信。我們需要在使用該類型的反序列化之前調(diào)用它,本例子中,我們選擇在MainWindow
的構(gòu)造函數(shù)中進行注冊:
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{ui->setupUi(this);
qRegisterMetaType();
}
此后,我們便可以使用如下的方式進行反序列化了:
QDataStream stream(...);
QVariant var;
stream >>var;
auto data = qvariant_cast(var);
// do something on data...
3. Reference你是否還在尋找穩(wěn)定的海外服務(wù)器提供商?創(chuàng)新互聯(lián)www.cdcxhl.cn海外機房具備T級流量清洗系統(tǒng)配攻擊溯源,準確流量調(diào)度確保服務(wù)器高可用性,企業(yè)級服務(wù)器適合批量采購,新人活動首月15元起,快前往官網(wǎng)查看詳情吧