【问题标题】:How to go about serializing a large, complex object?如何序列化一个大而复杂的对象?
【发布时间】:2015-08-12 04:41:22
【问题描述】:

我有一个“User”类,其中包含 40 多个私有变量,包括复杂对象,如私钥/公钥(QCA 库)、自定义 QObjects 等。这个想法是该类有一个名为 sign() 的函数,它可以加密,签名、序列化自身并返回 QByteArray,然后可以将其存储在 SQLite blob 中。

序列化复杂对象的最佳方法是什么?使用QMetaObject 迭代属性?将其转换为 protobuf 对象?

是否可以转换为 char 数组?

【问题讨论】:

    标签: c++ qt serialization introspection qmetaobject


    【解决方案1】:

    是否可以转换为 char 数组?

    不,因为您将强制转换您一无所知的 QObject 的内部结构、第二次运行程序时无效的指针等等。

    TL;DR:对于显式数据元素而言,手动实现它是可以的,而利用 QObjectQ_GADGET 类的元对象系统将有助于一些苦差事。

    最简单的解决方案可能是为您使用的对象和类型实现QDataStream 运算符。确保遵循良好的做法:可以想象到的每个类都可以改变它所持有的数据格式,必须发出一个格式标识符。

    例如,让我们学习以下类:

    class User {
      QString m_name;
      QList<CryptoKey> m_keys;
      QList<Address> m_addresses;
      QObject m_props;
      ...
      friend QDataStream & operator<<(QDataStream &, const User &);
      friend QDataStream & operator>>(QDataStream &, User &);
    public:
      ...
    };
    Q_DECLARE_METATYPE(User) // no semi-colon
    
    class Address {
      QString m_line1;
      QString m_line2;
      QString m_postCode;
      ...
      friend QDataStream & operator<<(QDataStream &, const Address &);
      friend QDataStream & operator>>(QDataStream &, Address &);
    public:
      ...
    };
    Q_DECLARE_METATYPE(Address) // no semi-colon!
    

    Q_DECLARE_METATYPE 宏使QVariantQMetaType 类型系统知道这些类。因此,例如,可以将Address 分配给QVariant,将这种QVariant 转换为Address,将变体直接流式传输到数据流等。

    首先,让我们解决如何转储QObject 属性:

    QList<QByteArray> publicNames(QList<QByteArray> names) {
      names.erase(std::remove_if(names.begin(), names.end(),
                  [](const QByteArray & v){ return v.startsWith("_q_"); }), names.end());
      return names;
    }
    
    bool isDumpable(const QMetaProperty & prop) {
      return prop.isStored() && !prop.isConstant() && prop.isReadable() && prop.isWritable();
    }
    
    void dumpProperties(QDataStream & s, const QObject & obj)
    {
      s << quint8(0); // format
      QList<QByteArray> names = publicNames(obj.dynamicPropertyNames());
      s << names;
      for (name : names) s << obj.property(name);
      auto mObj = obj.metaObject();
      for (int i = 0; i < mObj->propertyCount(), ++i) {
        auto prop = mObj->property(i);
        if (! isDumpable(prop)) continue;
        auto name = QByteArray::fromRawData(prop.name(), strlen(prop.name());
        if (! name.isEmpty()) s << name << prop.read(&obj);
      }
      s << QByteArray();
    }
    

    一般来说,如果我们要处理来自没有m_props 成员的User 的数据,我们需要能够清除属性。每次扩展存储对象和升级序列化格式时都会出现这个习惯用法。

    void clearProperties(QObject & obj)
    {
      auto names = publicNames(obj.dynamicPropertyNames());
      const QVariant null;
      for (name : names) obj.setProperty(name, null);
      auto const mObj = obj.metaObject();
      for (int i = 0; i < mObj->propertyCount(), ++i) {
        auto prop = mObj->property(i);
        if (! isDumpable(prop)) continue;
        if (prop.isResettable()) {
          prop.reset(&obj);
          continue;
        }
        prop.write(&obj, null);
      }
    }
    

    现在我们知道如何从流中恢复属性:

    void loadProperties(QDataStream & s, QObject & obj)
    {
      quint8 format;
      s >> format;
      // We only support one format at the moment.
      QList<QByteArray> names;
      s >> names;
      for (name : names) {
        QVariant val;
        s >> val;
        obj.setProperty(name, val);
      }
      auto const mObj = obj.metaObject();
      forever {
        QByteArray name;
        s >> name;
        if (name.isEmpty()) break;
        QVariant value;    
        s >> value;
        int idx = mObj->indexOfProperty(name);
        if (idx < 0) continue;
        auto prop = mObj->property(idx);
        if (! isDumpable(prop)) continue;
        prop.write(&obj, value);
      }
    }
    

    因此我们可以实现流操作符来序列化我们的对象:

    #define fallthrough
    
    QDataStream & operator<<(QDataStream & s, const User & user) {
      s << quint8(1) // format
        << user.m_name << user.m_keys << user.m_addresses;
      dumpProperties(s, &m_props);
      return s;
    }
    
    QDataStream & operator>>(QDataStream & s, User & user) {
      quint8 format;
      s >> format;
      switch (format) {
      case 0:
        s >> user.m_name >> user.m_keys;
        user.m_addresses.clear();
        clearProperties(&user.m_props);
        fallthrough;
      case 1:
        s >> user.m_addresses;
        loadProperties(&user.m_props);
        break;
      }
      return s;
    }
    
    QDataStream & operator<<(QDataStream & s, const Address & address) {
      s << quint8(0) // format
        << address.m_line1 << address.m_line2 << address.m_postCode;
      return s;
    }
    
    QDataStream & operator>>(QDataStream & s, Address & address) {
      quint8 format;
      s >> format;
      switch (format) {
      case 0:
        s >> address.m_line1 >> address.m_line2 >> address.m_postCode;
        break;
      }
      return s;
    }
    

    属性系统也适用于任何其他类,只要您声明其属性并添加Q_GADGET 宏(而不是Q_OBJECT)。从 Qt 5.5 开始支持此功能。

    假设我们声明我们的Address 类如下:

    class Address {
      Q_GADGET
      Q_PROPERTY(QString line1 MEMBER m_line1)
      Q_PROPERTY(QString line2 MEMBER m_line2)
      Q_PROPERTY(QString postCode MEMBER m_postCode)
    
      QString m_line1;
      QString m_line2;
      QString m_postCode;
      ...
      friend QDataStream & operator<<(QDataStream &, const Address &);
      friend QDataStream & operator>>(QDataStream &, Address &);
    public:
      ...
    };
    

    然后让我们根据为处理小工具而修改的[dump|clear|load]Properties 声明数据流运算符:

    QDataStream & operator<<(QDataStream & s, const Address & address) {
      s << quint8(0); // format
      dumpProperties(s, &address);
      return s;
    }
    
    QDataStream & operator>>(QDataStream & s, Address & address) {
      quint8 format;
      s >> format;
      loadProperties(s, &address);
      return s;
    }
    

    即使属性集已更改,我们也不需要更改格式指示符。我们应该保留格式指示符,以防我们有其他无法再表示为简单属性转储的更改。在大多数情况下这不太可能,但必须记住,不使用格式说明符的决定会立即确定流数据的格式。以后无法更改!

    最后,属性处理程序是用于QObject 属性的稍微缩减和修改的变体:

    template <typename T> void dumpProperties(QDataStream & s, const T * gadget) {
      dumpProperties(s, T::staticMetaObject, gadget);
    }
    
    void dumpProperties(QDataStream & s, const QMetaObject & mObj, const void * gadget)
    {
      s << quint8(0); // format
      for (int i = 0; i < mObj.propertyCount(), ++i) {
        auto prop = mObj.property(i);
        if (! isDumpable(prop)) continue;
        auto name = QByteArray::fromRawData(prop.name(), strlen(prop.name());
        if (! name.isEmpty()) s << name << prop.readOnGadget(gadget);
      }
      s << QByteArray();
    }
    
    template <typename T> void clearProperties(T * gadget) {
      clearProperties(T::staticMetaObject, gadget);
    }
    
    void clearProperties(const QMetaObject & mObj, void * gadget)
    {
      const QVariant null;
      for (int i = 0; i < mObj.propertyCount(), ++i) {
        auto prop = mObj.property(i);
        if (! isDumpable(prop)) continue;
        if (prop.isResettable()) {
          prop.resetOnGadget(gadget);
          continue;
        }
        prop.writeOnGadget(gadget, null);
      }
    }
    
    template <typename T> void loadProperties(QDataStream & s, T * gadget) {
      loadProperties(s, T::staticMetaObject, gadget);
    }
    
    void loadProperties(QDataStream & s, const QMetaObject & mObj, void * gadget)
    {
      quint8 format;
      s >> format;
      forever {
        QByteArray name;
        s >> name;
        if (name.isEmpty()) break;
        QVariant value;    
        s >> value;
        auto index = mObj.indexOfProperty(name);
        if (index < 0) continue;
        auto prop = mObj.property(index);
        if (! isDumpable(prop)) continue;
        prop.writeOnGadget(gadget, value);
      }
    }
    

    TODOloadProperties 实现中未解决的问题是清除对象中存在但序列化中不存在的属性。

    当涉及到QDataStream 格式的内部版本时,确定整个数据流的版本是非常重要的。 documentation 是必读。

    还必须决定如何处理软件版本之间的兼容性。有几种方法:

    1. (最典型和不幸的)不兼容:不存储格式信息。新成员以特别的方式添加到序列化中。旧版本的软件在面对新数据时会表现出未定义的行为。较新的版本将对较旧的数据执行相同的操作。

    2. 向后兼容性:格式信息存储在每个自定义类型的序列化中。新版本可以正确处理旧版本的数据。旧版本必须检测未处理的格式、中止反序列化并向用户指示错误。 忽略较新的格式会导致未定义的行为

    3. 完全的前后兼容性:每个序列化的自定义类型都存储在QByteArray 或类似的容器中。通过这样做,您可以获得有关整个类型的数据记录多长时间的信息。 QDataStream 版本必须是固定的。要读取自定义类型,首先读取其字节数组,然后设置 QBuffer,您可以使用 QDataStream 进行读取。您阅读可以以您知道的格式解析的元素,并忽略其余数据。这迫使对格式采用增量方法,其中较新的格式只能在现有格式上附加元素。但是,如果新格式放弃了旧格式中的某些数据元素,它仍然必须转储它,但使用 null 或其他安全的默认值,让旧版本的代码“快乐”。

    如果您认为格式字节可能会用完,您可以采用可变长度编码方案,称为扩展或扩展八位字节,在各种 ITU 标准中都很熟悉(例如Q.931 4.5.5 承载能力信息元素) .思路如下:一个八位字节(字节)的最高位用于指示该值是否需要更多八位字节来表示。这使得字节有 7 位来表示值,1 位来标记扩展。如果设置了该位,则读取后续八位字节并以小端方式将它们连接到现有值。您可以这样做:

    class VarLengthInt {
    public:
      quint64 val;
      VarLengthInt(quint64 v) : val(v) { Q_ASSERT(v < (1ULL<<(7*8))); }
      operator quint64() const { return val; }
    };
    
    QDataStream & operator<<(QDataStream & s, VarLengthInt v) {
      while (v.val > 127) {
        s << (quint8)((v & 0x7F) | 0x80);
        v.val = v.val >> 7;
      }
      Q_ASSERT(v.val <= 127);
      s << (quint8)v.val;
      return s;
    }
    
    QDataStream & operator>>(QDataStream & s, VarLengthInt & v) {
      v.val = 0;
      forever {
        quint8 octet;
        s >> octet;
        v.val = (v.val << 7) | (octet & 0x7F);
        if (! (octet & 0x80)) break;
      }
      return s;
    }
    

    VarLengthInt 的序列化具有可变长度,并且始终使用给定值可能的最小字节数:1 字节至 0x7F,2 字节至 0x3FFF,3 字节至 0x1F'FFFF,4 字节至0x0FFF'FFFF等。撇号在C++14 integer literals中有效。

    它的用法如下:

    QDataStream & operator<<(QDataStream & s, const User & user) {
      s << VarLengthInt(1) // format
        << user.m_name << user.m_keys << user.m_addresses;
      dumpProperties(s, &m_props);
      return s;
    }
    
    QDataStream & operator>>(QDataStream & s, User & user) {
      VarLengthInt format;
      s >> format;
      ...
      return s;
    }
    

    【讨论】:

    • 这真是太棒了——我感激不尽。我希望我能写出如此简洁优雅的代码!
    【解决方案2】:

    二进制转储序列化是一个坏主意,它会包含很多你不需要的东西,比如对象的 v-table 指针,以及其他指针,直接包含或来自其他类成员,这对被序列化,因为它们不会在应用程序会话之间持续存在。

    如果只是一个单独的类,手动实现它,肯定不会杀了你。如果您有一系列类,并且它们是QObject 派生的,则可以使用元系统,但这只会注册属性,而不会与属性绑定的int something 成员将被跳过。如果您有很多不是 Qt 属性的数据成员,那么将它们公开为 Qt 属性会比手动编写序列化方法需要更多的输入,我可能会添加不必要的内容。

    【讨论】:

      猜你喜欢
      • 2018-10-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多