首页 > 解决方案 > POD 类型的二进制 I/O 如何不破坏别名规则?

问题描述

二十多年前,我会(并且没有)想到使用 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.xs.y的别名read()。因此,s.xread()不是未定义的行为之后(因为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仍然1read().

快进到现代世界,我们实际上必须遵循别名规则并避免未定义的行为,等等,我无法向自己证明这是允许的。

例如,在 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 charchar指向任何可简单复制(因此是 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 特定于操作系统的,因此在标准中没有讨论。


因此,考虑到所有这些,我的问题是:

标签: c++c++14language-lawyermemcpystrict-aliasing

解决方案


严格的别名是关于通过指针/引用访问一个对象,该类型不是该对象的实际类型。但是,严格别名规则允许通过指向字节数组的指针访问任何类型的任何对象。这条规则至少从 C++14 开始就已经存在了。

现在,这并不意味着什么,因为必须定义这种访问的含义。为此(就写作而言),我们实际上只有两条规则:[basic.types]/2 和 /3,它们涵盖了复制 Trivially Copyable 类型的字节。问题最终归结为:

您是否正在从文件中读取“构成 [an] 对象的基础字节”?

如果您正在读入的数据s实际上是从 的实时实例的字节中复制的S,那么您就 100% 没问题。从标准中可以清楚地看出,执行fwrite将给定字节写入文件,并fread从文件中读取这些字节。因此,如果您将现有S实例的字节写入文件,并将这些写入的字节读取到现有的S,则相当于复制这些字节。

当你开始进入解释的杂草时,你会遇到技术问题。将标准解释为定义此类程序的行为是合理的,即使写入和读取发生在同一程序的不同调用中。

在以下两种情况之一出现问题:

1:写入数据的程序实际上与读取数据的程序不同。

2:当写入数据的程序实际上并没有写入类型的对象S,而是写入恰好可以合法解释为S.

该标准不管理两个程序之间的互操作性。但是,C++20 确实提供了一个工具,该工具有效地表示“如果此内存中的字节包含 a 的合法对象表示T,那么我将返回该对象外观的副本。” 它被称为std::bit_cast; 你可以给它传递一个字节数组sizeof(T),它会返回那个的副本T

如果你是骗子,你会得到未定义的行为。如果不是简单可复制的bit_cast,甚至都不会编译T

但是,将字节复制直接S从技术上不是S但完全可以是的源直接复制到现场S,是另一回事。标准中没有使这项工作起作用的措辞。

我们的朋友P0593提出了一种明确声明这种假设的机制,但它并没有完全融入 C++20。


推荐阅读