字符* 和 std::uint8_t* 之间的reinterpret_cast - 安全

reinterpret_cast between char* and std::uint8_t* - safe?

本文关键字:reinterpret cast 安全 std uint8 字符 之间      更新时间:2023-10-16

现在我们有时都必须使用二进制数据。在C++我们使用字节序列,从一开始,char就是我们的构建块。定义为sizeof为 1,它是字节。默认情况下,所有库 I/O 函数都使用 char。一切都很好,但总是有一点担忧,一点奇怪的事情困扰着一些人 - 字节中的位数是实现定义的。

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

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

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

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

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

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

一旦我们有了这个知识,我们有 8 位字节,std::uint8_t实现为 charunsigned char ,我们是否可以假设我们可以从 char*std::uint8_t* 进行reinterpret_cast,反之亦然?它是便携式的吗?

这就是我的标准阅读技巧让我失败的地方。我读过关于安全派生的指针([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迹象并不重要,那么来回投char*std::uint8_t*是否便携且安全?

我希望参考该标准并附上解释。

编辑:谢谢,杰里棺材。我将添加标准([basic.lval],§3.10/10(中的引用:

如果程序尝试通过 glvalue 而不是 以下类型的行为未定义:

— 字符或无符号字符类型。

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

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

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

但是,从char*std::uint8_t*执行reinterpret_cast,然后取消引用生成的指针违反了严格的别名规则,如果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::memcpyunsigned char数组的形式访问对象,因此它是安全和可移植的。

重申一下,为了能够在char*std::uint8_t*之间reinterpret_cast并以 100% 符合标准的方式可移植安全地处理生成的指针,必须满足以下条件:

  • CHAR_BIT == 8 .
  • std::uint8_t已定义。
  • std::uint8_tcharunsigned char实现。

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

如果uint8_t存在,基本上唯一的选择是它是unsigned char的typedef(如果它碰巧是无符号的,则char(。没有什么(除了位域(可以表示比char更少的存储,而唯一可以小至8位的另一种类型是bool。下一个最小的普通整数类型是short,它必须至少为16位。

因此,如果uint8_t存在,你实际上只有两种可能性:要么将unsigned char投射到unsigned char,要么将signed char投射到unsigned char

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

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

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

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

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

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

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