如何序列化大型复杂对象?

时间:2015-08-11 17:04:13

标签: c++ qt serialization introspection qmetaobject

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

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

可以将它转换为char数组吗?

2 个答案:

答案 0 :(得分:3)

  

可以将它转换为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();
}

一般情况下,如果我们处理的User数据中没有m_props成员,我们就需要清除这些属性。每次扩展存储对象并升级序列化格式时,都会出现这个习惯用法。

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);
  }
}

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

确定整个数据流在QDataStream格式的内部版本中的版本化非常重要。 documentation是必读的。

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

  1. (最典型和不幸)没有兼容性:不存储格式信息。新成员以ad-hoc方式添加到序列化中。在面对较新的数据时,较旧版本的软件将显示未定义的行为。较新版本将对旧数据执行相同操作。

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

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

  4. 如果您认为格式字节可能耗尽,您可以采用可变长度编码方案,称为扩展或扩展八位字节,熟悉各种ITU标准(例如Q.931 4.5.5承载能力信息元件)。这个想法如下:八位字节(字节)的最高位用于指示值是否需要更多八位字节用于表示。这使得字节有7位表示值,1位用于标记扩展。如果该位置位,则读取后续八位字节并以little-endian方式将它们连接到现有值。您可以通过以下方式执行此操作:

    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&#39; FFFF, 4个字节,最多0x0FFF&#39; 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;
    }
    

答案 1 :(得分:0)

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

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