自定义Cereal XML输出容器节点

发布于:2025-07-03 ⋅ 阅读:(15) ⋅ 点赞:(0)

自定义Cereal XML输出容器节点

CEREAL_SERIALIZE_INTRUSIVE1.优化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>

这种设计导致:

  1. 可读性差:节点名无法反映数据结构含义
  2. 处理困难: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>::type
    serialize(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检测 的三段式设计,实现了:

  1. 零侵入改造:不修改原有类结构
  2. 编译期优化:无运行时性能损失
  3. 格式统一:输出语义化XML,提升数据处理效率

最佳实践:建议结合#define CEREAL_XML_STRING_VALUE "root"使用,实现全链路节点控制


技术改变输出,细节决定体验。让序列化结果不再是一堆冰冷的value0标签,而是充满业务语义的数据蓝图。

核心代码

CEREAL_SERIALIZE_INTRUSIVE1.优化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 xml
	void CEREAL_SAVE_FUNCTION_NAME(XMLOutputArchive& ar, QString const& str)
	{
		ar.saveValue(str.toStdString());
	}

	//! loading string from xml
	void CEREAL_LOAD_FUNCTION_NAME(XMLInputArchive& ar, QString& str)
	{
		std::string temp;
		ar.loadValue(temp);
		str = QString::fromStdString(temp);
	}
	// 保存 QVector
	template <class Archive, typename T>
	typename std::enable_if<!Archive::is_loading::value, void>::type
		serialize(Archive& ar, QVector<T>& vec) {
		ar(make_size_tag(vec.size())); // number of elements
		for (auto&& item : vec) {
#pragma region 节点不再是value0...
			customNodeName(ar, item);
#pragma endregion 
			ar(item);
		}
	}

	// 加载 QVector
	template <class Archive, typename T>
	typename std::enable_if<Archive::is_loading::value, void>::type
		serialize(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();