自定義Cereal XML輸出容器節點
CEREAL_SERIALIZE_INTRUSIVE
在 1.優化Cereal宏 一行聲明序列化函數QString、QVector、QList、QMap序列化在2.在Cereal中支持Qt容器序列化
靜態成員函數type_node檢測在 3.利用SFINAE檢測成員函數
🚀 告別value0:自定義Cereal序列化節點名稱的終極方案
—— 讓XML輸出語義化,告別自動生成的冗余標簽
🔧 問題背景:默認序列化的痛點
使用Cereal序列化Qt容器(如QVector<Animal>
)時,XML默認生成<value0>
、<value1>
等無意義節點:
沒改之前,節點是value0…輸出為
<?xml version="1.0" encoding="utf-8"?>
<cereal><zoo size="dynamic"><value0><!-- 無意義標簽 --><age>1</age><type>1</type><name>a1</name><master>m1</master></value0><value1><age>2</age><type>2</type><name>b2</name><master>m2</master></value1><value2>.........................................................................</zoo>
</cereal>
這種設計導致:
- 可讀性差:節點名無法反映數據結構含義
- 處理困難:XPath查詢需依賴順序而非語義
改完注冊,節點不再是value0…,輸出
<?xml version="1.0" encoding="utf-8"?>
<cereal><zoo size="dynamic"><Animal><age>1</age><type>1</type><name>a1</name><master>m1</master></Animal><Animal><age>2</age><type>2</type><name>b2</name><master>m2</master></Animal><Animal>.........................................................................</zoo>
</cereal>
?? 解決方案:動態注冊自定義節點名
通過編譯期類型檢測 + SFINAE模板特化,實現節點名稱的語義化替換:
// 1. 定義類型注冊宏
#define REGISTER_TYPE_NODE(Type) \static const char* type_node(){ return #Type; };// 2. 檢測類型是否注冊
template<class T>
struct has_static_member_type_node {template<class TT>static auto test(int) -> decltype(std::decay_t<TT>::type_node(), std::true_type{});template<class> static std::false_type test(...);static constexpr bool value = decltype(test<T>(0))::value;
};// 3. 根據注冊狀態動態設置節點名
template <class Archive, typename T>
typename std::enable_if<has_static_member_type_node<T>::value, void>::type
customNodeName(Archive& ar, const T& t) {ar.setNextName(std::decay_t<T>::type_node()); // 使用注冊名
}
🧠 關鍵技術解析
技術點 | 作用 | 優勢 |
---|---|---|
REGISTER_TYPE_NODE | 為類型注入type_node() 靜態函數 | 非侵入式,無需修改原結構定義 |
SFINAE檢測 | 編譯期判斷類型是否包含type_node() | 零運行時開銷4,6 |
ar.setNextName() | 覆蓋Cereal默認節點命名邏輯 | 精準控制XML輸出結構 |
💻 完整實現:Qt容器+自定義類型適配
1. 自定義類型注冊示例
struct Animal {int age, type;QString name, master;// 序列化成員CEREAL_SERIALIZE_INTRUSIVE(age, type, name, master)// 關鍵!注冊XML節點名REGISTER_TYPE_NODE(Animal) // 輸出標簽變為<Animal>
};
2. Qt容器序列化改造
namespace cereal {template <class Archive, typename T>typename std::enable_if<!Archive::is_loading::value>::typeserialize(Archive& ar, QVector<T>& vec) {ar(make_size_tag(vec.size()));for (auto&& item : vec) {customNodeName(ar, item); // 動態設置節點名ar(item); // 正常序列化}}
}
3. QString優化(避免二次嵌套)
void CEREAL_SAVE_FUNCTION_NAME(XMLOutputArchive& ar, QString const& str) {ar.saveValue(str.toStdString()); // 直接輸出文本值
}
? 效果對比
修改前
<value0> <!-- 無意義標簽 --><age>1</age><name>a1</name>
</value0>
修改后
<Animal> <!-- 語義化標簽 --><age>1</age><name>a1</name>
</Animal>
優化收益:
- 可讀性↑:節點名直接反映數據類型
- 兼容性:完全兼容Cereal反序列化邏輯
🧪 測試驗證
// 序列化測試
QList<Animal> animals = {{1,1,"a1","m1"}, {2,2,"b2","m2"}};
cereal::XMLOutputArchive archive(oss);
archive(cereal::make_nvp("zoo", animals));// 輸出結果
std::cout << oss.str();
輸出驗證:
<zoo size="dynamic"><Animal> <!-- 自定義節點名 --><age>1</age><type>1</type><name>a1</name></Animal><Animal><age>2</age><type>2</type><name>b2</name></Animal>
</zoo>
💎 總結
通過 REGISTER_TYPE_NODE
+customNodeName
+SFINAE檢測 的三段式設計,實現了:
- 零侵入改造:不修改原有類結構
- 編譯期優化:無運行時性能損失
- 格式統一:輸出語義化XML,提升數據處理效率
最佳實踐:建議結合
#define CEREAL_XML_STRING_VALUE "root"
使用,實現全鏈路節點控制
技術改變輸出,細節決定體驗。讓序列化結果不再是一堆冰冷的value0
標簽,而是充滿業務語義的數據藍圖。
核心代碼
CEREAL_SERIALIZE_INTRUSIVE
在 1.優化Cereal宏 一行聲明序列化函數QString、QVector、QList、QMap序列化在2.在Cereal中支持Qt容器序列化
靜態成員函數type_node檢測在 3.利用SFINAE檢測成員函數
#pragma region 節點不再是value0...
#define REGISTER_TYPE_NODE(Type) static const char* type_node(){ return #Type;};// 靜態成員函數type_node檢測
template<class T>
struct has_static_member_type_node {template<class TT>static auto test(int) -> decltype(std::decay_t<TT>::type_node(), std::true_type{});template<class>static std::false_type test(...) {};static constexpr bool value = decltype(test<T>(0))::value;
};template <class Archive, typename T>
typename std::enable_if<has_static_member_type_node<T>::value, void>::type
customNodeName(Archive& ar, const T& t)
{ar.setNextName(std::decay_t<T>::type_node());
}template <class Archive, typename T>
typename std::enable_if<!has_static_member_type_node<T>::value, void>::type
customNodeName(Archive& ar, const T& t)
{// default in xml.hpp std::string getValueName()
}
#pragma endregion namespace cereal
{// 避免XML中生成<value0>節點//! saving string to xmlvoid CEREAL_SAVE_FUNCTION_NAME(XMLOutputArchive& ar, QString const& str){ar.saveValue(str.toStdString());}//! loading string from xmlvoid CEREAL_LOAD_FUNCTION_NAME(XMLInputArchive& ar, QString& str){std::string temp;ar.loadValue(temp);str = QString::fromStdString(temp);}// 保存 QVectortemplate <class Archive, typename T>typename std::enable_if<!Archive::is_loading::value, void>::typeserialize(Archive& ar, QVector<T>& vec) {ar(make_size_tag(vec.size())); // number of elementsfor (auto&& item : vec) {
#pragma region 節點不再是value0...customNodeName(ar, item);
#pragma endregion ar(item);}}// 加載 QVectortemplate <class Archive, typename T>typename std::enable_if<Archive::is_loading::value, void>::typeserialize(Archive& ar, QVector<T>& vec) {size_type size;ar(make_size_tag(size));vec.resize(size);for (auto&& item : vec) {ar(item);}}
};// 動物
struct Animal
{int age = 0;int type = 0;QString name;QString master;Animal() = default;Animal(int a,int t,QString n,QString m):age(a), type(t), name(n), master(m){};Animal(const Animal&) = default;Animal(Animal&&) = default;CEREAL_SERIALIZE_INTRUSIVE(age, type, name, master)
#pragma region 節點不再是value0...REGISTER_TYPE_NODE(Animal)
#pragma endregion
};
測試代碼
QList<Animal> list_Animal;
list_Animal.push_back(Animal(1, 1, "a1", "m1"));
list_Animal.push_back(Animal(2, 2, "b2", "m2"));
list_Animal.push_back(Animal(3, 3, "c3", "m3"));
list_Animal.push_back(Animal(4, 4, "d4", "m4"));
list_Animal.push_back(Animal(5, 5, "e5", "m5"));std::ostringstream oss2; // 內存輸出流
{cereal::XMLOutputArchive archiveXML(oss2/*, cereal::XMLOutputArchive::Options().indent(true).outputType(false).sizeAttributes(false)*/);archiveXML(::cereal::make_nvp("zoo", list_Animal));
}
std::string xmlStr2 = oss2.str(); // 獲取XML字符串
qDebug() << __LINE__ << QString::fromStdString(xmlStr2);// ===== 2. 從字符串反序列化 =====
QList<Animal> list_Animal_in;
std::istringstream iss2(xmlStr2); // 輸入流綁定XML字符串
{cereal::XMLInputArchive archiveXMLIn(iss2);archiveXMLIn(cereal::make_nvp("zoo", list_Animal_in));
}
qDebug() << __LINE__ << list_Animal_in.size();