63

现在我们有时都必须使用二进制数据。在 C++ 中,我们使用字节序列,从一开始char就是我们的构建块。定义为sizeof1,它是字节。char并且默认使用所有库 I/O 函数。一切都很好,但总是有一点点担心,一些奇怪的东西会困扰一些人——一个字节中的位数是实现定义的。

所以在 C99 中,决定引入几个 typedef 让开发人员可以轻松地表达自己,固定宽度的整数类型。当然是可选的,因为我们不想损害可移植性。其中uint8_t, 迁移到 C++11 作为std::uint8_t, 一个固定宽度的 8 位无符号整数类型,对于真正想要使用 8 位字节的人来说是完美的选择。

因此,开发人员接受了新工具并开始构建库,这些库明确地声明他们接受 8 位字节序列,asstd::uint8_t*std::vector<std::uint8_t>其他。

但是,也许经过深思熟虑,标准化委员会决定不要求实现,std::char_traits<std::uint8_t>因此禁止开发人员轻松便携地将 s 实例化,例如,std::basic_fstream<std::uint8_t>轻松地std::uint8_t将 s 读取为二进制数据。或者也许,我们中的一些人不关心字节中的位数并且对此感到满意。

但不幸的是,两个世界发生冲突,有时您必须将数据作为char*并将其传递给期望的库std::uint8_t*。但是等等,你说,不是char可变位并且std::uint8_t固定为8吗?会不会导致数据丢失?

好吧,这有一个有趣的标准语。定义为恰好保存一个字节并且字节是内存的char最低可寻址块,因此不能有位宽小于char. 接下来,它被定义为能够保存 UTF-8 代码单元。这给了我们最小值 - 8 位。所以现在我们有一个需要 8 位宽的 typedef 和一个至少 8 位宽的类型。但是有替代品吗?是的,unsigned char。请记住,签名char是实现定义的。还有其他类型吗?谢天谢地,没有。所有其他整数类型的所需范围都在 8 位之外。

最后,std::uint8_t是可选的,这意味着如果未定义使用此类型的库将不会编译。但是如果它编译呢?我可以非常自信地说,这意味着我们处于一个具有 8 位字节和CHAR_BIT == 8.

一旦我们知道我们有 8 位字节,即std::uint8_t实现为charor unsigned char,我们是否可以假设我们可以执行reinterpret_castfrom char*to std::uint8_t*,反之亦然?它是便携式的吗?

这就是我的标准阅读技巧让我失望的地方。我阅读了有关安全派生指针 ( [basic.stc.dynamic.safety]) 的内容,据我所知,以下内容:

std::uint8_t* buffer = /* ... */ ;
char* buffer2 = reinterpret_cast<char*>(buffer);
std::uint8_t buffer3 = reinterpret_cast<std::uint8_t*>(buffer2);

如果我们不碰它是安全的buffer2。如我错了请纠正我。

因此,鉴于以下先决条件:

  • CHAR_BIT == 8
  • std::uint8_t被定义为。

假设我们正在处理二进制数据并且可能缺少符号无关紧要,那么它是否可移植且安全char*std::uint8_t*char

我将不胜感激对标准的参考和解释。

编辑:谢谢,杰里·科芬。我将添加来自标准的引用([basic.lval],§3.10/10):

如果程序尝试通过非下列类型之一的泛左值访问对象的存储值,则行为未定义:

...

— char 或 unsigned char 类型。

EDIT2:好的,更深入。std::uint8_t不保证是unsigned char. 它可以实现为扩展无符号整数类型,扩展无符号整数类型不包含在 §3.10/10 中。现在怎么办?

4

2 回答 2

33

好吧,让我们变得真正迂腐。在阅读了 thisthisthis之后,我非常有信心理解这两个标准背后的意图。

因此,执行reinterpret_castfrom std::uint8_t*tochar*然后取消引用结果指针是安全可移植的,并且[basic.lval]明确允许。

但是,执行reinterpret_castfrom char*tostd::uint8_t*然后解除对结果指针的引用违反了严格的别名规则,并且如果实现为扩展无符号整数类型,则是未定义的行为std::uint8_t

但是,有两种可能的解决方法,首先:

static_assert(std::is_same_v<std::uint8_t, char> ||
    std::is_same_v<std::uint8_t, unsigned char>,
    "This library requires std::uint8_t to be implemented as char or unsigned char.");

有了这个断言,你的代码将不会在平台上编译,否则会导致未定义的行为。

第二:

std::memcpy(uint8buffer, charbuffer, size);

Cppreference表示将std::memcpy对象作为数组访问,unsigned char因此它是安全可移植的。

重申一下,为了能够以100% 符合标准的方式可移植安全地在结果指针reinterpret_cast之间进行操作,必须满足以下条件:char*std::uint8_t*

  • CHAR_BIT == 8.
  • std::uint8_t被定义为。
  • std::uint8_t被实现为charor unsigned char

实际上,上述条件在 99% 的平台上都是正确的,并且很可能没有平台上前 2 个条件为真而第 3 个条件为假。

于 2013-04-28T10:06:53.573 回答
20

如果uint8_t存在,本质上唯一的选择是它是一个类型定义unsigned char(或者char如果它恰好是无符号的)。没有什么(除了位域)可以表示比 a 更少的存储空间char,唯一可以小到 8 位的其他类型是 a bool。下一个最小的普通整数类型是 a short,它必须至少为 16 位。

因此,如果uint8_t确实存在,那么您实际上只有两种可能性:您要么转换unsigned charunsigned char,要么转换signed charunsigned char

前者是身份转换,所以显然是安全的。后者属于 §3.10/10 中为访问任何其他类型作为 char 或 unsigned char 序列而给出的“特殊分配”,因此它也给出了定义的行为。

由于这包括charand unsigned char,因此将其作为 char 序列访问的强制转换也给出了定义的行为。

编辑:就 Luc 提到的扩展整数类型而言,我不确定您将如何设法应用它以在这种情况下有所作为。C++ 引用了 C99 标准来定义uint8_t等,因此其余部分的引号来自 C99。

§6.2.6.1/3 规定unsigned char应使用纯二进制表示,没有填充位。仅在 6.2.6.2/1 中允许使用填充位,其中明确排除了unsigned char. 然而,该部分详细描述了纯二进制表示 - 从字面上看。因此,unsigned charand uint8_t(如果存在)必须在位级别以相同的方式表示。

要看到两者之间的差异,我们必须断言,某些特定位在被视为一个时会产生与另一个不同的结果——尽管事实上两者在位级别上必须具有相同的表示。

更直接地说:两者之间的结果差异要求他们以不同的方式解释位 - 尽管直接要求他们以相同的方式解释位。

即使在纯理论层面上,这似乎也很难实现。在任何接近实际水平的东西上,这显然是荒谬的。

于 2013-04-28T06:06:29.057 回答