【问题标题】:reinterpret_cast between char* and std::uint8_t* - safe?char* 和 std::uint8_t* 之间的 reinterpret_cast - 安全吗?
【发布时间】:2013-04-22 00:29:36
【问题描述】:

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

所以在 C99 中,决定引入几个 typedef 来让开发人员轻松表达自己,固定宽度的整数类型。当然是可选的,因为我们不想损害可移植性。其中uint8_t作为std::uint8_t迁移到C++11,是一个固定宽度的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_ts 读取为二进制数据。或者,我们中的一些人不关心字节中的位数并且对此感到满意。

但不幸的是,两个世界发生冲突,有时您必须将数据作为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 被实现为 charunsigned char,我们是否可以假设我们可以从 reinterpret_cast 执行 char* 到 @987654348 @ 反之亦然?便携吗?

这就是我的标准阅读能力让我失望的地方。我阅读了有关安全派生指针 ([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 访问一个对象的存储值,而不是其中一个 以下类型的行为未定义:

...

— char 或 unsigned char 类型。

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

【问题讨论】:

  • 老实说,你很难找到一个平台,其中 char 不是在过去 20 多年左右制造的八位。跨度>
  • 我相信只有当你碰巧遇到一台没有 2 的补码表示的机器时才会不安全,在这种情况下你可能会得到意想不到的行为。但我希望看到真正知道答案的人。
  • 那里有很多现代机器,比如只进行 32 位访问的 DSP,Joachim。
  • @FaTony:我很抱歉重复我自己,(请参阅我对 Jerry Coffin 的回答和其中的引用的评论),但你的句子“std::uint8_t 不能保证是@ 的typedef 987654362@" 表明它可能是。其实不能。 C++11, 18.4.1 说 uint8_t 是一个 typedef 用于无符号整数类型。此外,3.9.1/3 定义了无符号整数类型,char 不是其中之一,无论它是否可以容纳负值。 C99 声明相同。
  • asked a question a couple weeks ago 包含您的答案。看看R.'s answer,它解释了uint8_t确实不需要需要与unsigned char具有相同的表示。

标签: c++ c++11 language-lawyer strict-aliasing uint8t


【解决方案1】:

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

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

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

但是,有两种可能的解决方法,第一种:

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 的数组访问,因此它是安全便携

重申一下,为了能够在 char*std::uint8_t* 之间进行 reinterpret_cast 并以 100% 标准便携安全使用生成的指针-符合方式,必须满足以下条件:

  • CHAR_BIT == 8
  • std::uint8_t 已定义。
  • std::uint8_t 实现为 charunsigned char

实际上,上述条件在 99% 的平台上都为真,并且很可能没有平台可以满足前 2 个条件为真而第 3 个条件为假的情况。

【讨论】:

  • 这引出了一个问题:为什么要使用整数类型来完成字符类型的工作?
  • 最好向标准化委员会提出什么问题。我希望有一个单独的整数类型,它保证大小为 1 个字节并且没有字符语义。
  • std::uint8_t 不必是 unsigned char 以使其可移植:它可能是 char,如果在给定实现上将 char 定义为未签名。 (别忘了char 有 3 种类型,而不是其他整数类型的 2 种。)
  • 正确。更新了我的答案。
  • 关于"std::uint8_t是实现为char还是unsigned char":如果实现为signed char呢?有区别吗?
【解决方案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 或 unsigned 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(如果存在)必须在位级别以相同的方式表示。

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

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

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

【讨论】:

  • 那个“分配”只在一个方向上起作用。指向字符类型的指针 -> 指向任何非字符类型的指针很容易导致代码违反所谓的别名规则。
  • 好的,取消将其标记为答案。这需要更深入的调查。
  • 如果一个实现提供(比如)__u8 扩展整数类型,并将其用于uint8_t,即使它具有与unsigned char 完全相同的表示,访问任何unsigned char 的对象不会扩展到 __u8。实现可能会特别地提供这种类型,以便优化器可以通过假设没有别名来做得更好。
  • 您说:“如果 uint8_t 存在 [...] 它是 [...] 的 typedef(或者 char,如果它恰好是无符号的)”我认为这是不正确的。 uint8_t 必须是无符号整数类型的 typedef,而 char 不是这种类型,无论它是否有符号。同样,char 不是有符号整数类型。我在herehere 之前讨论过这个问题(阅读里面的cmets)。 uint8_t 可以(并且可能会但不一定)是 unsigned char 的 typedef。
  • @JerryCoffin:我的立场是正确的。您的参考起到了作用:C++ 标准不要求该大小,但它在 §3.9.1.3 中引用了 C99 标准的 §5.2.4.2.1 并要求满足这些要求。
猜你喜欢
  • 2021-11-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-04-01
  • 1970-01-01
相关资源
最近更新 更多