char *和std :: uint8_t之间的reinterpret_cast * – 安全吗?

现在我们都有时需要使用二进制数据。 在C ++中,我们使用字节序列,并且从开始的char是我们的构build块。 定义为sizeof为1,它是字节。 所有的库I / O函数都默认使用char 。 一切都很好,但总是有一点担心,有点怪异的人,一些字节的位数是由实现定义的。

所以在C99中,决定引入几个typedef来让开发人员很容易expression自己的固定宽度的整数types。 可选,当然,因为我们从不想伤害可移植性。 其中, uint8_t作为std::uint8_t (一种固定宽度的8位无符号整数types)迁移到C ++ 11中,对于真正想要使用8位字节的人来说,是最佳select。

因此,开发人员接受了新的工具,并开始构build一些库,表示他们接受8位字节序列,如std::uint8_t*std::vector<std::uint8_t>或其他。

但是,也许有一个非常深刻的想法,标准化委员会决定不要求实施std::char_traits<std::uint8_t>因此禁止开发人员轻松地实例化std::basic_fstream<std::uint8_t>和轻松读取std::uint8_t s作为二进制数据。 或者,也许,我们中的一些人不关心一个字节的位数,并且对它感到满意。

但不幸的是,两个世界相互碰撞,有时你必须把数据作为char*传递给期望std::uint8_t*的库。 但是,等等,你说,不是charvariables位和std::uint8_t固定为8? 会导致数据丢失吗?

那么这个呢有一个有趣的Standardese。 被定义为char字节和字节是最小的可寻址内存块,所以不能有比char小的位宽的types。 接下来,它被定义为能够保存UTF-8代码单元。 这给我们至less8位。 所以现在我们有一个typedef,它需要8位宽,至less8位宽的types。 但是有其他的select吗? 是的, unsigned char 。 请记住, char签名是实现定义的。 任何其他types? 谢天谢地,不。 所有其他整数types的要求范围不在8位。

最后, std::uint8_t是可选的,这意味着使用这种types的库如果没有定义就不会编译。 但是如果编译呢? 我可以很有把握地说,这意味着我们在一个8位字节和CHAR_BIT == 8

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

这是我的Standardese阅读技能使我失望的地方。 我读了安全派生的指针( [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被定义。

假设我们正在处理二进制数据,并且可能缺lesschar符号并不重要,那么是否可移植和安全地来回转换char*std::uint8_t*

我将不胜感激参考标准与解释。

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

如果程序试图通过以下types之一的glvalue来访问对象的存储值,则行为是未定义的:

– 一个字符或无符号的字符types。

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

好吧,让我们真正的迂腐。 看完这个 , 这个和这个 ,我非常有信心,我理解这两个标准背后的意图。

所以,从std::uint8_t*char* reinterpret_cast ,然后解引用生成的指针是安全可移植的 ,明确允许[basic.lval] 。

但是,从char*执行reinterpret_caststd::uint8_t* ,然后取消引用结果指针违反了严格的别名规则 ,如果std::uint8_t作为扩展的无符号整数types实现,则是未定义的行为

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

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

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

第二:

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

Cppreference说std::memcpyunsigned char数组的forms访问对象,所以它是安全的可移植的

重申一下,为了能够在char*std::uint8_t*之间reinterpret_cast进行reinterpret_cast ,并且以100%符合标准的方式轻松安全地处理结果指针,必须满足以下条件:

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

实际上,上述情况在99%的平台上都是正确的,而前两种情况可能没有平台,而第三种情况是错误的。

如果uint8_t存在,本质上唯一的select就是它是unsigned char的一个typedef(或者如果它恰好是无符号的,则为char )。 没有任何东西(而是一个位域)可以表示比char更less的存储空间,唯一可以小到8位的其他types就是bool 。 下一个最小的正常整数types是一个short ,必须至less16位。

因此,如果uint8_t存在,则只有两种可能性:要么将unsigned charunsigned char ,要么将signed charunsigned char

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

既然包含了charunsigned char ,那么一个chartypes的转换也会给出定义的行为。

编辑:至于卢克提到的扩展整数types,我不知道你怎么会设法应用它在这种情况下有所作为。 C ++指的是uint8_t等定义的C99标准,所以其余部分的引用来自C99。

§6.2.6.1/ 3指定unsigned char应该使用一个纯粹的二进制表示,没有填充位。 填充位仅在6.2.6.2/1中允许,它特别排除了unsigned char 。 然而,该部分详细描述了一个纯粹的二进制表示forms – 从字面上看就是这个位。 因此, unsigned charuint8_t (如果存在的话)必须在位级相同地表示。

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

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

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

几星期前我问了一个问题 ,其中包含了答案。

看看R.的答案 ,这就解释了uint8_t不需要和unsigned char具有相同的表示forms。