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

时间:2013-04-28 05:46:27

标签: c++ c++11 language-lawyer

现在我们有时必须使用二进制数据。在C ++中,我们使用字节序列,因为开始char是我们的构建块。定义为sizeof为1,它是字节。默认情况下,所有库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_t s作为二进制数据。或许,我们中的一些人不关心字节中的位数,并对此感到满意。

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

嗯,有一个有趣的Standardese就此。 char定义为只保存一个字节和字节是最低可寻址内存块,因此不能存在比特宽度小于unsigned char的类型。接下来,它被定义为能够保存UTF-8代码单元。这给了我们最小--8位。所以现在我们有一个typedef,它要求是8位宽,并且是一个至少8位宽的类型。但有其他选择吗?是的,char。请记住,std::uint8_t的签名是实现定义的。还有其他任何一种谢天谢地,没有。所有其他整数类型都需要超出8位的范围。

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

一旦我们掌握了这些知识,即我们有8位字节,char被实现为unsigned charreinterpret_cast,我们可以假设我们可以{{1} }从char*std::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已定义。

假设我们正在使用二进制数据且char*的潜在缺失无关紧要,来回展示std::uint8_t*char是否可移植且安全?

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

编辑:谢谢你,Jerry Coffin。我将添加标准引用([basic.lval],§3.10/ 10):

  

如果程序试图通过除了其中一个之外的glvalue访问对象的存储值   以下类型的行为未定义:

     

...

     

- char或unsigned char类型。

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

2 个答案:

答案 0 :(得分:22)

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

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

但是,从reinterpret_castchar*执行std::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的数组进行访问,因此安全可移植

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

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

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

答案 1 :(得分:19)

如果uint8_t存在,基本上唯一的选择是它是unsigned char的typedef(如果恰好是无符号,则为char)。没有(但是位域)可以表示比char更少的存储,并且唯一可以小到8位的其他类型是bool。下一个最小的正常整数类型是short,它必须至少为16位。

因此,如果存在uint8_t,您实际上只有两种可能性:您要么unsigned char投射到unsigned char,要么signed char投射到{{} 1}}。

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

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

编辑:就Luc提到的扩展整数类型而言,我不确定你是如何设法应用它来改变这种情况的。 C ++引用了unsigned char等定义的C99标准,因此其余部分的引用来自C99。

§6.2.6.1/ 3指定uint8_t将使用纯二进制表示,没有填充位。填充位仅在6.2.6.2/1中允许,具体排除unsigned char。然而,该部分详细描述了纯二进制表示 - 字面意思是位。因此,unsigned charunsigned char(如果存在)必须在位级别相同地表示。

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

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

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