我是否需要使一个类型为POD,以便用内存映射文件持久化它?

Do I need to make a type a POD to persist it with a memory-mapped file?

本文关键字:内存 映射 持久化 文件 POD 是否 类型 一个      更新时间:2023-10-16

指针不能直接持久化到file,因为它们指向绝对地址。为了解决这个问题,我编写了一个relative_ptr模板,它保存偏移量而不是绝对地址。

基于这样一个事实,即只有平凡的可复制类型才能安全地逐位复制,我假设这种类型需要平凡的可复制才能安全地保存在内存映射文件中,并在以后检索。

这个限制结果有点问题,因为编译器生成的复制构造函数没有以有意义的方式运行。我没有发现任何禁止我默认复制构造函数并将其设为私有,所以我将其设为私有以避免意外复制导致未定义行为。

后来,我发现boost::interprocess::offset_ptr的创建是由相同的需求驱动的。然而,事实证明offset_ptr不是普通的可复制的,因为它实现了自己的自定义复制构造函数。

是我的假设,智能指针需要平凡的可复制被持久安全错误?

如果没有这样的限制,我想知道我是否也可以安全地做下面的事情。如果不是,那么在我上面描述的场景中,类型必须满足哪些需求才能可用?

struct base {
    int x;
    virtual void f() = 0;
    virtual ~base() {} // virtual members!
};
struct derived : virtual base {
    int x;
    void f() { std::cout << x; }
};
using namespace boost::interprocess;
void persist() {
    file_mapping file("blah");
    mapped_region region(file, read_write, 128, sizeof(derived));
    // create object on a memory-mapped file
    derived* d = new (region.get_address()) derived();
    d.x = 42;
    d->f();
    region.flush();
}
void retrieve() {
    file_mapping file("blah");
    mapped_region region(file, read_write, 128, sizeof(derived));
    derived* d = region.get_address();
    d->f();
}
int main() {
    persist();
    retrieve();
}

感谢所有提供替代方案的人。我不太可能在短期内使用其他东西,因为正如我解释的那样,我已经有了一个可行的解决方案。从上面使用问号可以看出,我真的很想知道为什么Boost可以不使用普通的可复制类型,以及您可以在多大程度上使用它:很明显,具有虚拟成员的类无法工作,但是您在哪里画线?

为了避免混淆,让我重述一下问题。

您希望在映射内存中创建一个对象,以便在应用程序关闭和重新打开后,可以再次映射文件并使用对象,而无需进一步反序列化。

POD是你试图做的事情的一种转移注意力的方法。你不需要二进制可复制(POD的意思);你需要独立于地址。

地址独立性要求:

  • 避免使用绝对指针。
  • 只使用偏移指针指向映射内存中的地址。

从这些规则中有一些相关关系。

  • 你不能使用virtual任何东西。c++虚函数是用类实例中的隐藏虚函数表指针实现的。虚表指针是一个绝对指针,你对它没有任何控制。
  • 您需要非常小心您的地址无关对象使用的其他c++对象。基本上,如果您使用标准库中的所有内容都可能会中断。即使它们不使用new,它们也可以在内部使用虚函数,或者只是存储指针的地址。
  • 你不能在地址无关的对象中存储引用。引用成员只是绝对指针的语法糖。

继承仍然是可能的,但是用处有限,因为virtual是非法的。

只要遵守上述规则,任何和所有构造函数/析构函数都是可以的。

甚至增加。Interprocess并不完全适合您要做的事情。提振。进程间还需要管理对对象的共享访问,而您可以假设只有您一个人在使用内存。

最后,使用Google Protobufs和传统的序列化可能更简单/更理智。

是的,但原因似乎与你有关。

你有虚函数和虚基类。这将导致编译器在您背后创建一系列指针。你不能把它们变成偏移量或其他任何东西。

如果你想做这种类型的持久化,你需要避免使用'virtual'。在那之后,就是语义问题了。真的,只要假装您正在用c语言做这些就行了。

如果您对跨不同系统或跨时间的互操作感兴趣,即使是PoD也有缺陷。

你可以看看Google Protocol Buffers以一种可移植的方式来做这件事

与其说是一个答案,不如说是一个长得太大的注释:

我认为这将取决于你愿意用多少安全性来换取速度/易用性。如果你有这样的struct:

struct S { char c; double d; };

您必须考虑填充和某些架构可能不允许您访问double,除非它在正确的内存地址上对齐。添加访问器函数和修复填充处理了这个问题,并且结构仍然是memcpy的,但是现在我们进入了一个领域,我们并没有真正从使用内存映射文件中获得多少好处。

由于似乎您只会在本地和固定设置中使用它,因此稍微放松要求似乎是可以的,因此我们回到正常使用上述struct。那么这个函数必须是可复制的吗?我不这么认为,考虑一下这个(可能是坏的)类:

   1 #include <iostream>
   2 #include <utility>
   3 
   4 enum Endian { LittleEndian, BigEndian };
   5 template<typename T, Endian e> struct PV {
   6         union {
   7                 unsigned char b[sizeof(T)];
   8                 T x;
   9         } val;  
  10         
  11         template<Endian oe> PV& operator=(const PV<T,oe>& rhs) {
  12                 val.x = rhs.val.x;
  13                 if (e != oe) {
  14                         for(size_t b = 0; b < sizeof(T) / 2; b++) {
  15                                 std::swap(val.b[sizeof(T)-1-b], val.b[b]);
  16                         }       
  17                 }       
  18                 return *this;
  19         }       
  20 };      

它不是微不足道的可复制的,你不能只是使用memcpy来移动它,但我不认为在内存映射文件的上下文中使用这样的类有什么问题(特别是如果文件匹配本机字节顺序)。


更新:你的底线在哪里?

我认为一个体面的经验法则是:如果等效的C代码是可以接受的,而c++只是作为一种方便,来强制类型安全,或适当的访问,它应该是好的。

这将使boost::interprocess::offset_ptr OK,因为它只是一个有特殊语义规则的ptrdiff_t的有用包装。同样,上面的struct PV也可以,因为它只是意味着自动交换字节,尽管在C中,您必须小心地跟踪字节顺序,并假设结构可以简单地复制。虚函数不会像C中等价的,结构中的函数指针那样起作用。然而,像下面这样(未经测试)的代码也可以:

struct Foo { 
    unsigned char obj_type;
    void vfunc1(int arg0) { vtables[obj_type].vfunc1(this, arg0); }
};

这行不通。您的class Derived不是POD,因此它取决于编译器如何编译您的代码。换句话说——不要这么做。

顺便说一下,你在哪里释放你的对象?在原地创建对象,但没有调用析构函数。

绝对不行。序列化是一种成熟的功能,可以在许多情况下使用,当然不需要pod。它所需要的是你指定一个定义良好的序列化二进制接口(SBI)。

任何时候你的对象离开运行时环境都需要序列化,包括共享内存、管道、套接字、文件和许多其他持久性和通信机制。

pod帮助的地方是您知道您不会离开处理器架构的地方。如果您永远不会在对象的编写器(序列化器)和读取器(反序列化器)之间更改版本,并且您不需要动态大小的数据,那么pod允许使用简单的基于内存的序列化器。

但是,通常需要存储字符串之类的东西。然后,您需要一种存储和检索动态信息的方法。有时,使用以0结尾的字符串,但这是特定于字符串的,不适用于向量、映射、数组、列表等。你会经常看到字符串和其他动态元素序列化为[size][element 1][element 2]…这是Pascal数组格式。此外,当处理跨机器通信时,SBI必须定义整型格式来处理潜在的端序问题。

指针通常是通过id实现的,而不是偏移量。每个需要序列化的对象都可以被赋予一个递增的数字作为ID,这可以是SBI中的第一个字段。通常不使用偏移量的原因是,如果不经过大小调整步骤或第二次处理,可能无法轻松计算未来的偏移量。id可以在序列化例程的第一次传递中计算。

序列化的其他方法包括使用XML或JSON等语法的基于文本的序列化器。使用用于重建对象的标准文本工具对这些内容进行解析。这些使SBI保持简单,但代价是性能和带宽的悲观。

最后,您通常构建一个体系结构,在该体系结构中,您构建序列化流,该流接受您的对象,并将它们逐个成员转换为SBI的格式。在共享内存的情况下,它通常在获得共享互斥锁后直接将成员压入内存。

通常看起来像

void MyClass::Serialise(SerialisationStream & stream)
{
  stream & member1;
  stream & member2;
  stream & member3;
  // ...
}

其中&操作符为不同的类型重载。你可以看看boost。序列化以获得更多示例。