现在我们有时必须使用二进制数据。在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 char
或reinterpret_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中。现在怎么办?
答案 0 :(得分:22)
好吧,让我们变得真正迂腐。在阅读this,this和this之后,我非常有信心理解这两个标准背后的意图。
因此,从reinterpret_cast
到std::uint8_t*
执行char*
然后取消引用结果指针是安全和可移植并且是明确的[basic.lval]允许。
但是,从reinterpret_cast
到char*
执行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_cast
和char*
之间std::uint8_t*
并使用结果指针便携和安全以100%符合标准的方式,必须满足以下条件:
CHAR_BIT == 8
。std::uint8_t
已定义。std::uint8_t
已实施为char
或unsigned 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 char
和char
,因此将其作为char序列访问的强制转换也会给出已定义的行为。
编辑:就Luc提到的扩展整数类型而言,我不确定你是如何设法应用它来改变这种情况的。 C ++引用了unsigned char
等定义的C99标准,因此其余部分的引用来自C99。
§6.2.6.1/ 3指定uint8_t
将使用纯二进制表示,没有填充位。填充位仅在6.2.6.2/1中允许,具体排除unsigned char
。然而,该部分详细描述了纯二进制表示 - 字面意思是位。因此,unsigned char
和unsigned char
(如果存在)必须在位级别相同地表示。
为了看到两者之间的差异,我们必须断言,当被视为一个特定位时,某些特定位将产生与另一个视图不同时的结果 - 尽管两者必须在位级别具有相同的表示。
更直接地说:两者之间的结果差异要求他们以不同方式解释比特 - 尽管他们直接要求他们相同地解释比特。
即使在纯粹的理论层面,这似乎很难实现。在接近实际水平的任何事情上,这显然是荒谬的。