【问题标题】:Understanding serialization of polymorphic objects in C++理解 C++ 中多态对象的序列化
【发布时间】:2018-01-13 18:52:04
【问题描述】:

编辑:我意识到下面的代码是一个很好的例子,说明你不能在 C++ 中使用任何不是 POD 的东西。

似乎没有办法摆脱在类中的 typeid 并在接收方进行某种切换或表查找(两者都必须小心维护)以重建对象。


我已经编写了一些玩具代码来序列化对象和两个独立的主代码来将它们写入/读取文件。

common.h:

#include <iostream>
using namespace std;

template <typename T>
size_t serialize(std::ostream & o, const T & t) {
  const char * bytes = reinterpret_cast<const char*>(&t);
  for (size_t i = 0; i < t.size(); ++i) {
    o << bytes[i];
  }
  return t.size();
}

size_t deserialize(std::istream & i, char * buffer) {
  size_t len = 0;
  char c;
  while (i.get(c)) {
    buffer[len] = c;
    ++len;
  }
  return len;
}

// toy classes
struct A {
  int a[4];
  virtual ~A() {}
  virtual void print(){cout << "A\n";}
  virtual size_t size() const {return sizeof(*this);}
};
struct B: A {
  int b[16];
  virtual ~B() {}
  virtual void print(){cout << "B\n";}
  virtual size_t size() const {return sizeof(*this);}
};

out.cpp:

#include <fstream>
#include "common.h"

int main() {
  B b;
  A& a = *static_cast<A*>(&b);
  ofstream ofile("serial.bin");
  cout << "size = " << serialize(ofile, a) << endl;
  ofile.close();
  return 0;
}

in.cpp:

#include <fstream>
#include "common.h"

int main() {
  char buffer[1024];
  ifstream ifile("serial.bin");
  cout << "size = " << deserialize(ifile, buffer) << endl;
  ifile.close();
  A& a = *reinterpret_cast<A*>(buffer);
  a.print();
  return 0;
}

如果我的类没有虚函数,这似乎可以正常工作,但是 in.cpp 当他们这样做时会崩溃。

我的理解是out.cpp创建的vptr不适合in.cpp使用。

有什么可以做的,可能避免手动创建和维护一个 vtable?

【问题讨论】:

  • 我宁愿建议寻找合适的序列化库(例如 boost 存档),为您做到这一点。您基本上有未定义的行为,原始转换仅适用于 POD 类型。
  • @user0042 代码应该转到微控制器,所以没有机会引入 Boost :( 加上我想了解。
  • 在这种情况下,我建议使用 Google Protobuf 和 Nano-PB C 接口。您必须自己提供带有类的映射代码。不,没有办法以某种方式手动管理 vtable。
  • 问题不仅在于 vtable 指针,还在于一般的指针。如果您将指针(或引用)保存到磁盘并将其加载到另一个程序中-它们现在有效的几率是多少?尽可能接近于零!
  • 您无法通过重新解释某些字节来创建对象。 (如果可以,我们可以通过调用 memcpy 来替换复制构造函数)。使用构造函数创建对象。

标签: c++ serialization polymorphism


【解决方案1】:

如果您绝对不能使用任何库(因为仍然可能有一些选项,即使对于嵌入式平台也是如此),序列化多态类的一种选择可能是提供虚拟序列化/反序列化方法。

以本例为例:

struct A {
  int a[4];
  virtual ~A() {}
  virtual void print(){cout << "A\n";}
  virtual size_t size() const {return sizeof(*this);}
  virtual void serialize(std::ostream & o) const
  {
      for (int i = 0; i < 4; ++i) o << a[i];
  }
  virtual void deserialize(std::istream & i)
  {
      for (int i = 0; i < 4; ++i) i >> a[i];
  }
};
struct B: A {
  int b[16];
  virtual ~B() {}
  virtual void print(){cout << "B\n";}
  virtual size_t size() const {return sizeof(*this);}
  virtual void serialize(std::ostream & o) const
  {
      A::serialize(o);
      for (int i = 0; i < 16; ++i) o << b[i];
  }
  virtual void deserialize(std::istream & i)
  {
      A::deserialize(i);
      for (int i = 0; i < 16; ++i) i >> b[i];
  }
};

// prg 1
B b;
b.serialize(ofile);

// prg 2
B.b;
b.deserialize(ifile);

基本上,您会将特定成员一一写入文件。

但是,这只是为了简单的情况,您实际上知道您希望文件中包含什么类。如果可以有多个类,您还需要编写一些类标识(例如一些结构序列化 id)来知道要读取哪个类。此外,如果类可能发生变化,您可能需要对这些类进行某种版本控制。

如上所述,指针也很棘手,特别是因为它们可以为 NULL - 您可以先编写一个 bool(字节)来确定指针是否为 NULL,然后再写入内容(如果有)。类似的方式你可以序列化/反序列化例如std::string 或 std::vector:先写长度,然后写项目。读取时,您会读取长度,保留或调整字符串/向量的大小,然后读取项目。

另一个问题可能是如果文件被传输到不同的机器,它可能有不同的字节顺序(endian)。如您所见,如果仍有一些可用的库,最好使用它而不是从头开始编写所有内容。


要添加多态反序列化(我可以看到您只在阅读器端使用 A),例如:

struct A {
  ...
  virtual int get_serialization_id() const = 0;
};
struct B: A {
  ...
  static const int SERIALIZATION_ID = 1; // needs to be different in every polymorphic class
  virtual int get_serialization_id() const
  { return SERIALIZATION_ID; }
};

void serialize(std::ostream & o, const A & a)
{
  o << a.get_serialization_id();
  o << a.serialize();
}

std::unique_ptr<A> deserialize(std::istream & i)
{
  std::unique_ptr<A> result;
  int id;
  i >> id;
  switch (id)
  {
  case B::SERIALIZATION_ID:
    result = std::make_unique<B>();
    break:
  case C::SERIALIZATION_ID:
    result = std::make_unique<C>();
    break:
  ...
  default:
    // leave NULL or throw exception
    return result;
  }
  result->deserialize(i);
  return result;
}

为避免切换,您可以更花哨并提供某种工厂注册(在映射中注册序列化 ID 和类工厂,然后使用注册表查找工厂并创建类)。你可以用反序列化来非常漂亮:)。

请注意,有些情况确实很难解决(例如,使用从多个其他实例指向同一个实例的共享指针重新创建实例结构等)。

【讨论】:

  • 但是现在接收者必须知道错过发送多态对象的确切类型,我错了吗?
  • 是的,在实际情况下,您需要一些类的标识(例如序列化 id),正如我所提到的。
  • 从技术上讲,您可以使用某种 [序列化 id]/[类工厂] 注册表来避免切换,还在答案中添加了注释。
猜你喜欢
  • 1970-01-01
  • 2019-04-12
  • 2013-12-04
  • 2023-04-09
  • 1970-01-01
  • 1970-01-01
  • 2013-05-07
  • 1970-01-01
相关资源
最近更新 更多