查看原文
其他

对象属性的自动序列化与反序列化

CPP开发者 2021-07-20

(给CPP开发者加星标,提升C/C++技能)

来源:CSDN - ZJU_fish1996
https://blog.csdn.net/ZJU_fish1996/article/details/102643222

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

目标

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

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

class Object
{

protected:
    QOpenGLShaderProgram* program = nullptr;
public:
    REGISTER_SERIALIZE
    Property_Param (Vector3f,  m_f3Position,     Vector3f(0,0,0))
    Property_Param (Vector3f,  m_f3Rotation,     Vector3f(0,0,0))
    Property_Param (Vector3f,  m_f3Scale,        Vector3f(1,1,1))
    Property_Param (bool,      m_bCastShadow,     true)
    Property_Param (bool,      m_bRender,         true)
    Property_Param (float,     m_fAlpha,          1.0f)
    Property_Param_Func (int,  m_nRenderPriority, -1, OnInitRenderPriority)
    Property_Func (string,    m_strObjName, OnLoadObj)
    Property (string,    m_strName)
    Property (Shape,     m_shape  )
    Property (string,    m_strType)
    Property (int,       m_nId    )

    void                 OnInitRenderPriority(const int& value)
;
    void                 OnLoadObj(const string& name);

    virtual void         UpdateLocation();
    virtual void         Create() 0;
    virtual void         Render() { }
    virtual void         Draw(bool bTess = false);
    virtual void         Draw(QOpenGLShaderProgram*,bool bTess = false);
    virtual              ~Object() { }
};

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


class PBRObject : public Object
{
private:
    void        SetImage(const string& name, QImage& image, GLuint& texId);
public:
    ~PBRObject ()    override;

    GLuint      m_nAlbedo = 0;
    GLuint      m_nNormal = 0;
    GLuint      m_nMaskTex = 0;

    QImage      m_imgAlbedo;
    QImage      m_imgNormal;
    QImage      m_imgMask;

    Property_Func (string, m_strAlbedo, SetImage, m_imgAlbedo, m_nAlbedo)
    Property_Func (string, m_strNormal, SetImage, m_imgNormal, m_nNormal)
    Property_Func (string, m_strMask, SetImage, m_imgMask, m_nMaskTex)

    Property_Param (Vector3f, m_f3Color, Vector3f(1,1,1))
    Property_Param (float, m_fAo, 0.4f)
    Property_Param (float, m_fRough, 0.0f)
    Property_Param (float, m_fMetal, 0.0f)
    Property_Param (bool,  m_bFire,  false)
    Property_Param (bool,  m_bBloom, false)
    Property_Param (bool,  m_bSSR, false)
    Property_Param (bool,  m_bXRay,  false)
    Property_Param (bool,  m_bOutline, false)

    Model*      pModel = nullptr;
    

    void        Create() override;
    void        Render() override;
};

实现细节

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

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

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

序列化注册

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

class CSerializeHelper
{

public:
    class BaseObject 
    {

        // ...
    }; // 数据格式定义

    void PushBack(BaseObject* obj) // 支持添加数据
    
{
        // ...
    }
    void Serialize(QDomElement& child) // 序列化
    
{
        // ...
    }
    void Deserialize(QDomElement& child) // 反序列化
    
{
        // ...
    }
private:
    list<BaseObject*> listObjs; // 数据容器
};

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

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

#define REGISTER_SERIALIZE                                      \
    CSerializeHelper serializeHelper;                           \
    void Save(QDomElement& child)                               \
    {                                                           \
        serializeHelper.Serialize(child);                       \
    }                                                           \
    void Load(QDomElement& child)                               \
    {                                                           \
        serializeHelper.Deserialize(child);                     \
    }                                                           \

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

#define Property(type, name)                                    \
    type name;                                                  \

当我们在类中编写形如:

Property(int,x)

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

int x;

序列化对象管理

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

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

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

    class BaseObject
    {

    public:
        BaseObject(const string& inName)
            : m_strName(inName) { }

        virtual string getStrName() final return m_strName; }
        virtual string getStrValue() 0;
        virtual void   setStrValue(const string& strValue) 0;
        virtual ~BaseObject() = 0;
    private:
        string m_strName;
    };

    template<typename T>
    class Param : public BaseObject
    {
    public:
        Param() { }
        // ...
    private:
       unique_ptr<T>  m_value;
    }

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

class CSerializeHelper
{

public:
    // ...

    void PushBack(BaseObject* obj)
    
{
        listObjs.push_back(obj);
    }
    void Serialize(QDomElement& child)
    
{
        for(BaseObject* obj : listObjs)
        {
            child.setAttribute(QString::fromStdString(obj->getStrName()),
                               QString::fromStdString(obj->getStrValue()));
        }
    }
    void Deserialize(QDomElement& child)
    
{
        for(BaseObject* obj : listObjs)
        {
            QString attributeName = QString::fromStdString(obj->getStrName());
            if(child.hasAttribute(attributeName))
            {
                QString strValue = child.attribute(attributeName);
                obj->setStrValue(strValue.toStdString());
            }
        }
    }
};

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

#define Property(type, name)                                    \
    type name;                                                  \
    CSerializeHelper::Param<type>* name##Param                  \

            = new CSerializeHelper::Param<type>                 \
               (#name, &name, serializeHelper);                 \

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

    template<typename T>
    class Param : public BaseObject
    {
    public:
        Param(const string& inName, T* inValue, CSerializeHelper& helper)
            : BaseObject(inName), m_value(inValue)
        {
            helper.PushBack(this);
        }
        // ...
    };

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

int x;
CSerializeHelper::Param<int>* xParam
        = new CSerializeHelper::Param<int>
            ("x", &x, serializeHelper);

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

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

    template<typename T>
    class Param : public BaseObject
    {
    public:
       string getStrValue() override
       
{
           if(m_value)
           {
               stringstream ss;
               ss << *(m_value);
               return ss.str();
           }
           return string();
       }

       void setStrValue(const string& strValue) override
       
{
            T res;
            stringstream ss(str);
            ss >> res;
       }
    };

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

struct Vector3f
{
    float x,y,z;
    Vector3f() { }
    Vector3f(float _x,float _y,float _z) :x(_x),y(_y),z(_z) { }

    friend ostream& operator<<(ostream& out, const Vector3f& vec)
    {
        out << vec.x << " " <<vec.y << " " << vec.z ;
        return out;
    }
    friend istream& operator>>(istream& in, Vector3f& vec)
    {
        in >> vec.x >> vec.y >> vec.z;
        return in;
    }
};

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

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

template<typename T>
typename enable_if<is_enum<T>::value, T>::type
strConvert(const string& str)
{
    int res = strConvert<int>(str);
    return static_cast<T>(res);
}
template<typename T>
typename enable_if<!is_enum<T>::value, T>::type
strConvert(const string& str)
{
    T res;
    stringstream ss(str);
    ss >> res;
    return res;
}

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

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

template<typename T>
class Param
{

public:
       // ...
       void setStrValue(const string& strValue) override
       
{
           *m_value = strConvert<T>(strValue);        
       }
};

绑定回调函数

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

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

#define Property_Param(type, name, arg)                         \
    type name = arg;                                            \
    CSerializeHelper::Param<type>* name##Param                  \

            = new CSerializeHelper::Param<type>                 \
               (#name, &name, serializeHelper);                 \

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

#define Property_Func(type, name, func)                         \
    type name;                                                  \
    CSerializeHelper::Param<type>* name##Param                  \

            = new CSerializeHelper::Param<type>                 \
               (#name, &name,  serializeHelper,                 \
                      [&](type val){func(val);});               \

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

    template<typename T>
    class Param : public BaseObject
    {
         // ... 
    public:
        Param(const string& inName, T* inValue, CSerializeHelper& helper)
            : BaseObject(inName), m_value(inValue)
        {
            helper.PushBack(this);
        }
        Param(const string& inName, T* inValue, CSerializeHelper& helper, function<void(T)> onInit)
            : Param(inName, inValue, helper)
        {
            m_funcOnInit = onInit;
        }
       void setStrValue(const string& strValue) override
       
{
           if(m_funcOnInit)
           {
               m_funcOnInit(strConvert<T>(strValue));
           }
           else
           {
               *m_value = strConvert<T>(strValue);
           }
       }
    private:
       // ...
       function<void(T)> m_funcOnInit;
    };

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

#define Property_Func(type, name, func, ...)                    \
    type name;                                                  \
    CSerializeHelper::Param<type>* name##Param                  \

            = new CSerializeHelper::Param<type>                 \
               (#name, &name,  serializeHelper,                 \
                      [&](type val){func(val,__VA_ARGS__);});   \
    template<typename T,typename... Args>
    class Param : public BaseObject
    {
    public:
        // ...
        Param(const string& inName, T* inValue, CSerializeHelper& helper, function<void(T, Args...)> onInit)
            : Param(inName, inValue, helper)
        {
            m_funcOnInit = onInit;
        }
    private:
       // ...
       function<void(T, Args...)> m_funcOnInit;
    };

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

class PBRObject : public Object
{
    // ...
    GLuint      m_nAlbedo = 0;
    QImage      m_imgAlbedo;

    Property_Func (string, m_strAlbedo, SetImage, m_imgAlbedo, m_nAlbedo)

    void SetImage(const string& name, QImage& image, GLuint& texId);
};

入口

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

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

void ObjectInfo::Save(const QString& fileName)
{
    QFile file(fileName);
    if(!file.open(QFile::WriteOnly|QFile::Truncate))
        return;

    QDomDocument doc;
    QDomProcessingInstruction instruction;
    instruction = doc.createProcessingInstruction("xml""version=\"1.0\" encoding=\"UTF-8\"");

    doc.appendChild(instruction);

    QDomElement root = doc.createElement("objectlist");
    doc.appendChild(root);
    for(size_t i = 0;i < vecObjs.size(); i++)
    {
        QDomElement child = doc.createElement("object");
        vecObjs[i]->Save(child);
        root.appendChild(child);
    }
    QTextStream out_stream(&file);
    doc.save(out_stream,4);
    file.close();
}

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

void ObjectInfo::Load(const QString& fileName)
{
    QFile file(fileName);
    if(!file.open(QFile::ReadOnly))
    {
        qDebug() << "fail open";
        return;
    }

    QString errorStr;
    int errorLine;
    int errorColumn;

    QDomDocument doc;
    if (!doc.setContent(&file, false, &errorStr, &errorLine, &errorColumn))
    {
        qDebug() << "Error: Parse error at line " << errorLine << ", "
                                                 << "column " << errorColumn;
        return;
    }
    QDomElement root = doc.documentElement();
    if (root.tagName() != "objectlist")
    {
        qDebug() << "failed load object list";
        return;
    }

    QDomNode child = root.firstChild();
    while (!child.isNull())
    {
        QDomElement element = child.toElement();
        if (element.tagName() == "object")
        {
            QString type = element.attribute("m_strType");

            if(!type.isEmpty())
            {
                shared_ptr<Object> obj = CreateObject(type.toStdString());
                if(obj)
                {
                   obj->Load(element);
                }
            }
        }
        child = child.nextSibling();
    }
}

代码

.h

#ifndef OBJECTPROPERTY_H
#define OBJECTPROPERTY_H

#include <QtXml/QDomDocument>
#include <QtXml/QDomElement>
#include <sstream>
#include <list>
#include <memory>
#include <functional>
using namespace std;

template<typename T>
typename enable_if<is_enum<T>::value, T>::type
strConvert(const string& str)
{
    int res = strConvert<int>(str);
    return static_cast<T>(res);
}
template<typename T>
typename enable_if<!is_enum<T>::value, T>::type
strConvert(const string& str)
{
    T res;
    stringstream ss(str);
    ss >> res;
    return res;
}

class CSerializeHelper
{

public:
    class BaseObject
    {

    public:
        BaseObject(const string& inName)
            : m_strName(inName) { }

        virtual string getStrName() final return m_strName; }
        virtual string getStrValue() 0;
        virtual void   setStrValue(const string& strValue) 0;
        virtual ~BaseObject() = 0;
    private:
        string m_strName;
    };

    template<typename T,typename... Args>
    class Param : public BaseObject
    {
    public:
        Param(const string& inName, T* inValue, CSerializeHelper& helper)
            : BaseObject(inName), m_value(inValue)
        {
            helper.PushBack(this);
        }
        Param(const string& inName, T* inValue, CSerializeHelper& helper, function<void(T, Args...)> onInit)
            : Param(inName, inValue, helper)
        {
            m_funcOnInit = onInit;
        }
       string getStrValue() override
       
{
           if(m_value)
           {
               stringstream ss;
               ss << *(m_value);
               return ss.str();
           }
           return string();
       }

       void setStrValue(const string& strValue) override
       
{
           if(m_funcOnInit)
           {
               m_funcOnInit(strConvert<T>(strValue));
           }
           else
           {
               *m_value = strConvert<T>(strValue);
           }
       }

       ~Param() override { }
    private:
       unique_ptr<T>  m_value;
       function<void(T, Args...)> m_funcOnInit;
    };

    void PushBack(BaseObject* obj)
    
{
        listObjs.push_back(obj);
    }
    void Serialize(QDomElement& child)
    
{
        for(BaseObject* obj : listObjs)
        {
            child.setAttribute(QString::fromStdString(obj->getStrName()),
                               QString::fromStdString(obj->getStrValue()));
        }
    }
    void Deserialize(QDomElement& child)
    
{
        for(BaseObject* obj : listObjs)
        {
            QString attributeName = QString::fromStdString(obj->getStrName());
            if(child.hasAttribute(attributeName))
            {
                QString strValue = child.attribute(attributeName);
                obj->setStrValue(strValue.toStdString());
            }
        }
    }
private:
    list<BaseObject*> listObjs;
};


#define Property(type, name)                                    \
    type name;                                                  \
    CSerializeHelper::Param<type>* name##Param                  \

            = new CSerializeHelper::Param<type>                 \
               (#name, &name, serializeHelper);                 \

#define Property_Param(type, name, arg)                         \
    type name = arg;                                            \
    CSerializeHelper::Param<type>* name##Param                  \

            = new CSerializeHelper::Param<type>                 \
               (#name, &name, serializeHelper);                 \

#define Property_Func(type, name, func, ...)                    \
    type name;                                                  \
    CSerializeHelper::Param<type>* name##Param                  \

            = new CSerializeHelper::Param<type>                 \
               (#name, &name,  serializeHelper,                 \
                      [&](type val){func(val,__VA_ARGS__);});   \

#define Property_Param_Func(type, name, arg, func, ...)         \
    type name = arg;                                            \
    CSerializeHelper::Param<type>* name##Param                  \

            = new CSerializeHelper::Param<type>                 \
               (#name, &name,  serializeHelper,                 \
                      [&](type val){func(val,__VA_ARGS__);});   \


#define REGISTER_SERIALIZE                                      \
    CSerializeHelper serializeHelper;                           \
    void Save(QDomElement& child)                               \
    {                                                           \
        serializeHelper.Serialize(child);                       \
    }                                                           \
    void Load(QDomElement& child)                               \
    {                                                           \
        serializeHelper.Deserialize(child);                     \
    }                                                           \

#endif // OBJECTPROPERTY_H

cpp:

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



- EOF -


推荐阅读  点击标题可跳转

1、一次蓝屏故障分析

2、共享智能指针探究

3、如何让new操作符只构造,不申请内存


关注『CPP开发者』

看精选C++技术文章 . 加C++开发者专属圈子

↓↓↓


点赞和在看就是最大的支持❤️

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存