二十多年前,我会(并且没有)想到使用 POD 结构进行二进制 I/O:
struct S { std::uint32_t x; std::uint16_t y; };
S s;
read(fd, &s, sizeof(s)); // assume this succeeds and reads sizeof(s) bytes
std::cout << s.x + s.y;
(我忽略了填充和字节顺序问题,因为它们不是我要问的部分。)
“显然”,我们可以读入s并且编译器需要假定 和 的内容s.x是s.y的别名read()。因此,s.x在read()不是未定义的行为之后(因为s未初始化)。
同样的情况下
S s = { 1, 2 };
read(fd, &s, sizeof(s)); // assume this succeeds and reads sizeof(s) bytes
std::cout << s.x + s.y;
编译器不能假定它s.x仍然1在read().
快进到现代世界,我们实际上必须遵循别名规则并避免未定义的行为,等等,我无法向自己证明这是允许的。
例如,在 C++14 中,[basic.types] ¶2 说:
对于普通可复制类型 T 的任何对象(基类子对象除外),无论该对象是否拥有类型 T 的有效值,构成该对象的底层字节(1.7)都可以复制到 char 或无符号的字符。
42 如果将 char 或 unsigned char 数组的内容复制回对象,则该对象随后应保持其原始值。
¶4 说:
T 类型对象的对象表示是由 T 类型对象占用的 N 个 unsigned char 对象的序列,其中 N 等于 sizeof(T)。
[basic.lval] ¶10 说:
如果程序尝试通过非下列类型之一的泛左值访问对象的存储值,则行为未定义:54
...
— char 或 unsigned char 类型。
54此列表的目的是指定对象可能或可能不会别名的情况。
总而言之,我认为这是标准的说法,即“您可以形成一个unsigned char或char指向任何可简单复制(因此是 POD)类型并读取或写入其字节的指针”。事实上,在N2342中,它给了我们现代措辞,介绍性表格说:
程序可以安全地应用编码优化,尤其是 std::memcpy。
后来:_
然而,该类中唯一的数据成员是一个 char 数组,因此程序员直观地期望该类是 memcpyable 和二进制 I/O-able。
使用建议的解决方案,可以通过使默认构造函数变得微不足道(使用 N2210,语法将是 endian()=default)将类变成 POD,从而解决所有问题。
听起来 N2342 确实在试图说“我们需要更新措辞以使其能够像这些类型一样进行 I/O read()” write(),而且看起来更新后的措辞确实已成为标准。
另外,我经常听到提到“std::memcpy()洞”或类似的东西,你可以用它std::memcpy()来基本上“允许混叠”。但是该标准似乎并没有std::memcpy()特别指出(实际上在一个脚注中提到了它,std::memmove()并将其称为实现此目的的“示例”)。
另外,像这样的 I/O 函数read()往往是 POSIX 特定于操作系统的,因此在标准中没有讨论。
因此,考虑到所有这些,我的问题是:
什么真正保证我们可以对 POD 结构进行真实世界的 I/O(如上所示)?
我们真的需要
std::memcpy()将内容进出unsigned char缓冲区(当然不需要)还是我们可以直接读入 POD 类型?操作系统 I/O 函数是否“承诺”它们会“像通过读取或写入
unsigned char值一样”或“像通过std::memcpy()”一样操作底层内存?当我和原始 I/O 函数之间存在层(例如Asio )时,我应该注意什么?