当前位置:   article > 正文

[C++] 对象属性的自动序列化与反序列化_c++对象序列化和反序列化源码

c++对象序列化和反序列化源码

        对于一个有着多个属性的类对象而言,我们通常希望能够对其进行序列化与反序列化,以保存和导入我们记录下来的物体数据。编写这样的代码通常是繁琐的,并且会带来大量冗余。

       目标

       我们期望能够达到这样的效果,在类中声明变量的时候,能够自动注册相关的信息。在序列化和反序列化的过程中,该变量的值就会自动被解析,而无需额外的编码。

       这意味着,我们的代码可以按如下形式编写,达到自动序列化/反序列化的目的:

  1. class Object
  2. {
  3. protected:
  4. QOpenGLShaderProgram* program = nullptr;
  5. public:
  6. REGISTER_SERIALIZE
  7. Property_Param (Vector3f, m_f3Position, Vector3f(0,0,0))
  8. Property_Param (Vector3f, m_f3Rotation, Vector3f(0,0,0))
  9. Property_Param (Vector3f, m_f3Scale, Vector3f(1,1,1))
  10. Property_Param (bool, m_bCastShadow, true)
  11. Property_Param (bool, m_bRender, true)
  12. Property_Param (float, m_fAlpha, 1.0f)
  13. Property_Param_Func (int, m_nRenderPriority, -1, OnInitRenderPriority)
  14. Property_Func (string, m_strObjName, OnLoadObj)
  15. Property (string, m_strName)
  16. Property (Shape, m_shape )
  17. Property (string, m_strType)
  18. Property (int, m_nId )
  19. void OnInitRenderPriority(const int& value);
  20. void OnLoadObj(const string& name);
  21. virtual void UpdateLocation();
  22. virtual void Create() = 0;
  23. virtual void Render() { }
  24. virtual void Draw(bool bTess = false);
  25. virtual void Draw(QOpenGLShaderProgram*,bool bTess = false);
  26. virtual ~Object() { }
  27. };

       特别地,派生类的写法也基本类似:

  1. class PBRObject : public Object
  2. {
  3. private:
  4. void SetImage(const string& name, QImage& image, GLuint& texId);
  5. public:
  6. ~PBRObject () override;
  7. GLuint m_nAlbedo = 0;
  8. GLuint m_nNormal = 0;
  9. GLuint m_nMaskTex = 0;
  10. QImage m_imgAlbedo;
  11. QImage m_imgNormal;
  12. QImage m_imgMask;
  13. Property_Func (string, m_strAlbedo, SetImage, m_imgAlbedo, m_nAlbedo)
  14. Property_Func (string, m_strNormal, SetImage, m_imgNormal, m_nNormal)
  15. Property_Func (string, m_strMask, SetImage, m_imgMask, m_nMaskTex)
  16. Property_Param (Vector3f, m_f3Color, Vector3f(1,1,1))
  17. Property_Param (float, m_fAo, 0.4f)
  18. Property_Param (float, m_fRough, 0.0f)
  19. Property_Param (float, m_fMetal, 0.0f)
  20. Property_Param (bool, m_bFire, false)
  21. Property_Param (bool, m_bBloom, false)
  22. Property_Param (bool, m_bSSR, false)
  23. Property_Param (bool, m_bXRay, false)
  24. Property_Param (bool, m_bOutline, false)
  25. Model* pModel = nullptr;
  26. void Create() override;
  27. void Render() override;
  28. };

        实现细节

        我采取的方案是宏定义 + 模板编程。

        对象的序列化有着多种格式,最为常见的是键值对的存储方式,类似Xml,Json或者flatbuffers(当然我们可以选择将其存为文本格式或二进制格式),相比起直接把所有值按序存储的暴力方式,它的好处在于添加和移除一些对象并不会影响数据的读取,非常适合应用于一个可能需要不断更新的应用。

        本文中,选择了Xml作为存储格式。

        序列化注册

        首先,为了能够把每个变量对象加入到序列化管理,我们有必要定义一个管理类,那么它应该有一个容器,存储所有的变量名字以及对应的数据;具备加入数据的方法;支持数据的序列化与反序列化:

  1. class CSerializeHelper
  2. {
  3. public:
  4. class BaseObject
  5. {
  6. // ...
  7. }; // 数据格式定义
  8. void PushBack(BaseObject* obj) // 支持添加数据
  9. {
  10. // ...
  11. }
  12. void Serialize(QDomElement& child) // 序列化
  13. {
  14. // ...
  15. }
  16. void Deserialize(QDomElement& child) // 反序列化
  17. {
  18. // ...
  19. }
  20. private:
  21. list<BaseObject*> listObjs; // 数据容器
  22. };

         BaseObject中记录了每个变量的一些辅助数据。

         之后,为了快速在类中注册这一序列化管理类,我们定义如下宏,在类中直接引用即可:

  1. #define REGISTER_SERIALIZE \
  2. CSerializeHelper serializeHelper; \
  3. void Save(QDomElement& child) \
  4. { \
  5. serializeHelper.Serialize(child); \
  6. } \
  7. void Load(QDomElement& child) \
  8. { \
  9. serializeHelper.Deserialize(child); \
  10. } \

         接下来,我们定义变量,以下是最为简单的定义变量的宏:

  1. #define Property(type, name) \
  2. type name; \

         当我们在类中编写形如:

Property(int,x)

         时,我们实际上就得到了如下的代码:

int x;

         序列化对象管理

         但这也仅仅定义了变量,我们还没有将其加入到序列化管理中。为了管理该对象,首先我们需要考虑到,对象可以有很多类型,所以我们需要使用泛型编程,也就是将CSerializeHelper中容器管理的对象定义为泛型对象。

         其中,m_strName记录了变量名字的字符串形式,unique_ptr<T> m_value记录了对象当前的值的引用。

         此外,提供getStrValue() 和 setStrValue()的接口来实现泛型数据到字符串之间的转化,便于序列化。

  1. class BaseObject
  2. {
  3. public:
  4. BaseObject(const string& inName)
  5. : m_strName(inName) { }
  6. virtual string getStrName() final { return m_strName; }
  7. virtual string getStrValue() = 0;
  8. virtual void setStrValue(const string& strValue) = 0;
  9. virtual ~BaseObject() = 0;
  10. private:
  11. string m_strName;
  12. };
  13. template<typename T>
  14. class Param : public BaseObject
  15. {
  16. public:
  17. Param() { }
  18. // ...
  19. private:
  20. unique_ptr<T> m_value;
  21. }

        此时,CSerializeHelper中的三个方法可以定义如下:

  1. class CSerializeHelper
  2. {
  3. public:
  4. // ...
  5. void PushBack(BaseObject* obj)
  6. {
  7. listObjs.push_back(obj);
  8. }
  9. void Serialize(QDomElement& child)
  10. {
  11. for(BaseObject* obj : listObjs)
  12. {
  13. child.setAttribute(QString::fromStdString(obj->getStrName()),
  14. QString::fromStdString(obj->getStrValue()));
  15. }
  16. }
  17. void Deserialize(QDomElement& child)
  18. {
  19. for(BaseObject* obj : listObjs)
  20. {
  21. QString attributeName = QString::fromStdString(obj->getStrName());
  22. if(child.hasAttribute(attributeName))
  23. {
  24. QString strValue = child.attribute(attributeName);
  25. obj->setStrValue(strValue.toStdString());
  26. }
  27. }
  28. }
  29. };

         为了能够在定义变量的同时将变量加入序列化管理,我们在变量声明的同时,生成一个变量数据辅助类BaseObject的实例,此时Propery()宏扩展如下(备注:#代表字符串化,##代表连接):

  1. #define Property(type, name) \
  2. type name; \
  3. CSerializeHelper::Param<type>* name##Param \
  4. = new CSerializeHelper::Param<type> \
  5. (#name, &name, serializeHelper); \

      同时,在构造函数中完成相应的操作(把数据加入序列化管理):

  1. template<typename T>
  2. class Param : public BaseObject
  3. {
  4. public:
  5. Param(const string& inName, T* inValue, CSerializeHelper& helper)
  6. : BaseObject(inName), m_value(inValue)
  7. {
  8. helper.PushBack(this);
  9. }
  10. // ...
  11. };

        此时,对于Property(int, x)而言,我们实际得到了如下代码:

  1. int x;
  2. CSerializeHelper::Param<int>* xParam
  3. = new CSerializeHelper::Param<int>
  4. ("x", &x, serializeHelper);

        序列化实现(字符串和任意类型相互转换)

        至此,我们已经在类中注册了序列化管理类,生成保存和导入的函数,并能够将不同类型的变量自动加入管理维护。剩余的一个工作就是实现任意类型到字符串的相互转换。大部分情况下,我们都可以利用stringstream来辅助这一实现:

  1. template<typename T>
  2. class Param : public BaseObject
  3. {
  4. public:
  5. string getStrValue() override
  6. {
  7. if(m_value)
  8. {
  9. stringstream ss;
  10. ss << *(m_value);
  11. return ss.str();
  12. }
  13. return string();
  14. }
  15. void setStrValue(const string& strValue) override
  16. {
  17. T res;
  18. stringstream ss(str);
  19. ss >> res;
  20. }
  21. };

       对于内置类型,基本上可以直接使用;对于自定义类型,只需重载operator<<和>>,具备比较好的扩展性。例如,对于自定义类型Vector3f,需要添加如下两个运算符重载:

  1. struct Vector3f
  2. {
  3. float x,y,z;
  4. Vector3f() { }
  5. Vector3f(float _x,float _y,float _z) :x(_x),y(_y),z(_z) { }
  6. friend ostream& operator<<(ostream& out, const Vector3f& vec)
  7. {
  8. out << vec.x << " " <<vec.y << " " << vec.z ;
  9. return out;
  10. }
  11. friend istream& operator>>(istream& in, Vector3f& vec)
  12. {
  13. in >> vec.x >> vec.y >> vec.z;
  14. return in;
  15. }
  16. };

        但也有一些例外。比如对于我们自定义的枚举类型,我们就无法为其重载运算符。为了照顾这些特殊情况,我们第一个考虑到的可能是模板特例化,而枚举类型是一类类型,并不是单个类型,这意味我们需要对每一个自定义枚举类型做特例化,这样意味着每次我们添加新的自定义枚举类型时,还需要修改框架代码。

        实际上,我们需要的是有条件的编译,也就是在条件A时生成函数A,而在条件B时生成函数B。根据类型的不同,来得到不同的字符串转换方法。我们可以利用enable_if特性来实现:

  1. template<typename T>
  2. typename enable_if<is_enum<T>::value, T>::type
  3. strConvert(const string& str)
  4. {
  5. int res = strConvert<int>(str);
  6. return static_cast<T>(res);
  7. }
  8. template<typename T>
  9. typename enable_if<!is_enum<T>::value, T>::type
  10. strConvert(const string& str)
  11. {
  12. T res;
  13. stringstream ss(str);
  14. ss >> res;
  15. return res;
  16. }

        上述代码的含义是:若T类型为enum,生成第一个函数,我们将enum视为int来处理;否则,生成第二个函数。

        对于其余可能存在的特殊情况,我们也可以采用这种方法实现,此时,我们改进了setStrValue的实现:

  1. template<typename T>
  2. class Param
  3. {
  4. public:
  5. // ...
  6. void setStrValue(const string& strValue) override
  7. {
  8. *m_value = strConvert<T>(strValue);
  9. }
  10. };

        绑定回调函数

        在有些情况下,在类中声明变量,我们希望为其赋值初值;又有些情况下,我们希望自定义赋值的过程,或者说,在赋值的同时完成一些别的操作。比如当我们读入一个图像的名字时,我们希望能够同时加载这张图像,存到另一个变量中。

        我们扩展了变量声明的宏,同时也支持设定初始值的写法:

  1. #define Property_Param(type, name, arg) \
  2. type name = arg; \
  3. CSerializeHelper::Param<type>* name##Param \
  4. = new CSerializeHelper::Param<type> \
  5. (#name, &name, serializeHelper); \

        为了完成回调,我们需要传入一个函数,记录在BaseObject中,在加载数据的时候回调。为了实现这一目的,我们扩展宏如下:

  1. #define Property_Func(type, name, func) \
  2. type name; \
  3. CSerializeHelper::Param<type>* name##Param \
  4. = new CSerializeHelper::Param<type> \
  5. (#name, &name, serializeHelper, \
  6. [&](type val){func(val);}); \

       同时在构造函数中支持函数的传入:

  1. template<typename T>
  2. class Param : public BaseObject
  3. {
  4. // ...
  5. public:
  6. Param(const string& inName, T* inValue, CSerializeHelper& helper)
  7. : BaseObject(inName), m_value(inValue)
  8. {
  9. helper.PushBack(this);
  10. }
  11. Param(const string& inName, T* inValue, CSerializeHelper& helper, function<void(T)> onInit)
  12. : Param(inName, inValue, helper)
  13. {
  14. m_funcOnInit = onInit;
  15. }
  16. void setStrValue(const string& strValue) override
  17. {
  18. if(m_funcOnInit)
  19. {
  20. m_funcOnInit(strConvert<T>(strValue));
  21. }
  22. else
  23. {
  24. *m_value = strConvert<T>(strValue);
  25. }
  26. }
  27. private:
  28. // ...
  29. function<void(T)> m_funcOnInit;
  30. };

        这样,我们就能支持形如void(T)类型的函数回调了。但这可能是不够的,有时候我们还希望传入其它参数,同样地以图片地址为例,我们希望传入一个图像对象,把结果绑定到特定的对象中。我们假定第一个参数始终是T,如果希望支持传入任意类型的函数参数,我们需要使用可变长模板,传入一个模板参数包:

  1. #define Property_Func(type, name, func, ...) \
  2. type name; \
  3. CSerializeHelper::Param<type>* name##Param \
  4. = new CSerializeHelper::Param<type> \
  5. (#name, &name, serializeHelper, \
  6. [&](type val){func(val,__VA_ARGS__);}); \
  1. template<typename T,typename... Args>
  2. class Param : public BaseObject
  3. {
  4. public:
  5. // ...
  6. Param(const string& inName, T* inValue, CSerializeHelper& helper, function<void(T, Args...)> onInit)
  7. : Param(inName, inValue, helper)
  8. {
  9. m_funcOnInit = onInit;
  10. }
  11. private:
  12. // ...
  13. function<void(T, Args...)> m_funcOnInit;
  14. };

        此时,我们就可以按照如下的写法,传入一个SetImage的回调函数,同时传入QImage和QLuint类型的参数,在回调函数中完成导入图片,并生成纹理id的操作:

  1. class PBRObject : public Object
  2. {
  3. // ...
  4. GLuint m_nAlbedo = 0;
  5. QImage m_imgAlbedo;
  6. Property_Func (string, m_strAlbedo, SetImage, m_imgAlbedo, m_nAlbedo)
  7. void SetImage(const string& name, QImage& image, GLuint& texId);
  8. };

        入口

       完成了上述一系列参数后,我们需要设置一个入口来调用以上序列化/反序列化函数。

       对于保存而言,我们遍历所有Object对象,为其生成子结点,并交由每个对象自己填充child结点。

  1. void ObjectInfo::Save(const QString& fileName)
  2. {
  3. QFile file(fileName);
  4. if(!file.open(QFile::WriteOnly|QFile::Truncate))
  5. return;
  6. QDomDocument doc;
  7. QDomProcessingInstruction instruction;
  8. instruction = doc.createProcessingInstruction("xml", "version=\"1.0\" encoding=\"UTF-8\"");
  9. doc.appendChild(instruction);
  10. QDomElement root = doc.createElement("objectlist");
  11. doc.appendChild(root);
  12. for(size_t i = 0;i < vecObjs.size(); i++)
  13. {
  14. QDomElement child = doc.createElement("object");
  15. vecObjs[i]->Save(child);
  16. root.appendChild(child);
  17. }
  18. QTextStream out_stream(&file);
  19. doc.save(out_stream,4);
  20. file.close();
  21. }

        对于导入而言,我们需要先生成一个对象,然后才能让每个对象读入数据。为了能够生成对象,我们首先从xml中读入类型。此时的类型为字符串格式,这里需要我们实现从字符串构造对象,这是另外一个课题,实现在这篇文章中提及:根据字符串自动构造对应类

  1. void ObjectInfo::Load(const QString& fileName)
  2. {
  3. QFile file(fileName);
  4. if(!file.open(QFile::ReadOnly))
  5. {
  6. qDebug() << "fail open";
  7. return;
  8. }
  9. QString errorStr;
  10. int errorLine;
  11. int errorColumn;
  12. QDomDocument doc;
  13. if (!doc.setContent(&file, false, &errorStr, &errorLine, &errorColumn))
  14. {
  15. qDebug() << "Error: Parse error at line " << errorLine << ", "
  16. << "column " << errorColumn;
  17. return;
  18. }
  19. QDomElement root = doc.documentElement();
  20. if (root.tagName() != "objectlist")
  21. {
  22. qDebug() << "failed load object list";
  23. return;
  24. }
  25. QDomNode child = root.firstChild();
  26. while (!child.isNull())
  27. {
  28. QDomElement element = child.toElement();
  29. if (element.tagName() == "object")
  30. {
  31. QString type = element.attribute("m_strType");
  32. if(!type.isEmpty())
  33. {
  34. shared_ptr<Object> obj = CreateObject(type.toStdString());
  35. if(obj)
  36. {
  37. obj->Load(element);
  38. }
  39. }
  40. }
  41. child = child.nextSibling();
  42. }
  43. }

       代码

.h

  1. #ifndef OBJECTPROPERTY_H
  2. #define OBJECTPROPERTY_H
  3. #include <QtXml/QDomDocument>
  4. #include <QtXml/QDomElement>
  5. #include <sstream>
  6. #include <list>
  7. #include <memory>
  8. #include <functional>
  9. using namespace std;
  10. template<typename T>
  11. typename enable_if<is_enum<T>::value, T>::type
  12. strConvert(const string& str)
  13. {
  14. int res = strConvert<int>(str);
  15. return static_cast<T>(res);
  16. }
  17. template<typename T>
  18. typename enable_if<!is_enum<T>::value, T>::type
  19. strConvert(const string& str)
  20. {
  21. T res;
  22. stringstream ss(str);
  23. ss >> res;
  24. return res;
  25. }
  26. class CSerializeHelper
  27. {
  28. public:
  29. class BaseObject
  30. {
  31. public:
  32. BaseObject(const string& inName)
  33. : m_strName(inName) { }
  34. virtual string getStrName() final { return m_strName; }
  35. virtual string getStrValue() = 0;
  36. virtual void setStrValue(const string& strValue) = 0;
  37. virtual ~BaseObject() = 0;
  38. private:
  39. string m_strName;
  40. };
  41. template<typename T,typename... Args>
  42. class Param : public BaseObject
  43. {
  44. public:
  45. Param(const string& inName, T* inValue, CSerializeHelper& helper)
  46. : BaseObject(inName), m_value(inValue)
  47. {
  48. helper.PushBack(this);
  49. }
  50. Param(const string& inName, T* inValue, CSerializeHelper& helper, function<void(T, Args...)> onInit)
  51. : Param(inName, inValue, helper)
  52. {
  53. m_funcOnInit = onInit;
  54. }
  55. string getStrValue() override
  56. {
  57. if(m_value)
  58. {
  59. stringstream ss;
  60. ss << *(m_value);
  61. return ss.str();
  62. }
  63. return string();
  64. }
  65. void setStrValue(const string& strValue) override
  66. {
  67. if(m_funcOnInit)
  68. {
  69. m_funcOnInit(strConvert<T>(strValue));
  70. }
  71. else
  72. {
  73. *m_value = strConvert<T>(strValue);
  74. }
  75. }
  76. ~Param() override { }
  77. private:
  78. unique_ptr<T> m_value;
  79. function<void(T, Args...)> m_funcOnInit;
  80. };
  81. void PushBack(BaseObject* obj)
  82. {
  83. listObjs.push_back(obj);
  84. }
  85. void Serialize(QDomElement& child)
  86. {
  87. for(BaseObject* obj : listObjs)
  88. {
  89. child.setAttribute(QString::fromStdString(obj->getStrName()),
  90. QString::fromStdString(obj->getStrValue()));
  91. }
  92. }
  93. void Deserialize(QDomElement& child)
  94. {
  95. for(BaseObject* obj : listObjs)
  96. {
  97. QString attributeName = QString::fromStdString(obj->getStrName());
  98. if(child.hasAttribute(attributeName))
  99. {
  100. QString strValue = child.attribute(attributeName);
  101. obj->setStrValue(strValue.toStdString());
  102. }
  103. }
  104. }
  105. private:
  106. list<BaseObject*> listObjs;
  107. };
  108. #define Property(type, name) \
  109. type name; \
  110. CSerializeHelper::Param<type>* name##Param \
  111. = new CSerializeHelper::Param<type> \
  112. (#name, &name, serializeHelper); \
  113. #define Property_Param(type, name, arg) \
  114. type name = arg; \
  115. CSerializeHelper::Param<type>* name##Param \
  116. = new CSerializeHelper::Param<type> \
  117. (#name, &name, serializeHelper); \
  118. #define Property_Func(type, name, func, ...) \
  119. type name; \
  120. CSerializeHelper::Param<type>* name##Param \
  121. = new CSerializeHelper::Param<type> \
  122. (#name, &name, serializeHelper, \
  123. [&](type val){func(val,__VA_ARGS__);}); \
  124. #define Property_Param_Func(type, name, arg, func, ...) \
  125. type name = arg; \
  126. CSerializeHelper::Param<type>* name##Param \
  127. = new CSerializeHelper::Param<type> \
  128. (#name, &name, serializeHelper, \
  129. [&](type val){func(val,__VA_ARGS__);}); \
  130. #define REGISTER_SERIALIZE \
  131. CSerializeHelper serializeHelper; \
  132. void Save(QDomElement& child) \
  133. { \
  134. serializeHelper.Serialize(child); \
  135. } \
  136. void Load(QDomElement& child) \
  137. { \
  138. serializeHelper.Deserialize(child); \
  139. } \
  140. #endif // OBJECTPROPERTY_H

cpp:

  1. string CSerializeHelper::BaseObject::getStrValue() { return string(); }
  2. CSerializeHelper::BaseObject::~BaseObject() { }

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/花生_TL007/article/detail/162922
推荐阅读
相关标签
  

闽ICP备14008679号