我可以安全地将 std::string 用于 C++11 中的二进制数据吗?

Can I safely use std::string for binary data in C++11?

本文关键字:二进制 数据 C++11 用于 安全 std string 我可以      更新时间:2023-10-16

互联网上有几篇帖子建议你应该对二进制数据使用std::vector<unsigned char>或类似的东西。

但我更喜欢std::basic_string变体,因为它提供了许多方便的字符串操作函数。而 AFAIK,自 C++11 以来,该标准保证了每个已知的 C++03 实现已经做到的事情:std::basic_string将其内容连续存储在内存中。

乍一看,std::basic_string<unsigned char>可能是一个不错的选择。

但是,我不想使用std::basic_string<unsigned char>,因为几乎所有操作系统功能都只接受char*,因此需要显式强制转换。此外,字符串文字是const char*的,因此每次将字符串文字分配给二进制字符串时,我都需要显式强制const unsigned char*,我也想避免这种情况。此外,用于读取和写入文件或网络缓冲区的函数同样接受char*const char*指针。

这就剩下std::string,这基本上是std::basic_string<char>的typedef。

对二进制数据使用 std::string 的唯一潜在剩余问题(我可以看到)是std::string使用char(可以签名)。

charsigned charunsigned char 是三种不同的类型,char可以是无符号的,也可以是无符号的。

因此,当实际字节值 11111111b 作为 char 从std::string:operator[]返回时,并且您想要检查其值,其值可以是255(如果char是无符号的),也可以是"负数"(如果char有符号,具体取决于您的数字表示形式)。

同样,如果要显式地将实际字节值11111111b附加到std::string,如果char是有符号的,并且int char会话会导致溢出,则只需附加(char) (255)可能是实现定义的(甚至会引发信号)。

那么,有没有一种安全的方法可以解决这个问题,使std::string再次成为二进制安全?

§3.10/15 指出:

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

  • [...]
  • 一种类型,该类型是对应于对象的动态类型的有符号或无符号类型,
  • [...]
  • 字符或无符号字符类型。

如果我理解正确的话,它似乎允许使用 unsigned char* 指针来访问和操作std::string的内容,并使其也定义良好。它只是将位模式重新解释unsigned char,没有任何变化或信息丢失,后者是因为charsigned charunsigned char中的所有位都必须用于值表示。

然后,我可以使用这种对std::string内容的unsigned char*解释作为访问和更改[0, 255]范围内的字节值的方法,以定义明确且可移植的方式,而不管char本身的符号性如何。

这应该可以解决由潜在签名char引起的任何问题。

我的假设和结论是否正确?

此外,也是对相同位模式的unsigned char*解释(即 11111111b10101010b ) 保证在所有实现上都相同?换句话说,标准是否保证"通过unsigned char的眼睛看",相同的位模式总是导致相同的数值(假设字节中的位数相同)?

因此,我是否可以安全地(即,没有任何未定义或实现定义的行为)使用 std::string 在 C++11 中存储和操作二进制数据?

uc类型为 unsigned char 的转换static_cast<char>(uc)始终有效:根据 3.9.1 [basic.fundamental],charsigned charunsigned char 的表示形式与char与另外两种类型之一相同

声明为字符 (char) 的对象应足够大,以存储实现的基本字符集的任何成员。如果此集中的字符存储在字符对象中,则该字符对象的整数值等于该字符的单个字符文本形式的值。char 对象是否可以保存负值是实现定义的。字符可以显式声明为无符号或有符号。纯字符、有符号字符和无符号字符是三种不同的类型,统称为窄字符类型。字符、有符号字符和无符号字符占用相同的存储量,并具有相同的对齐要求 (3.11);也就是说,它们具有相同的对象表示形式。对于窄字符类型,对象表示形式的所有位都参与值表示形式。对于无符号窄字符类型,值表示形式的所有可能的位模式都表示数字。这些要求不适用于其他类型的类型。在任何特定的实现中,纯字符对象可以采用相同的 值为有符号字符或无符号字符;哪一个是实现定义的。

当然,将unsigned char范围之外的值转换为char值会带来问题,并可能导致未定义的行为。也就是说,只要你不试图将有趣的值存储到std::string中,你就没问题了。关于位模式,您可以依靠第 n 位转换为 2 n。仔细处理时,将二进制数据存储在std::string中应该没有问题。

也就是说,我不相信你的前提:处理二进制数据主要需要处理最好使用unsigned值操作的字节。您需要在 char*unsigned char* 之间转换的少数情况在没有明确处理时创建方便的错误,同时意外地弄乱了char的使用,这将是沉默的!也就是说,处理unsigned char将防止错误。我也不相信你得到所有这些不错的字符串函数的前提:首先,无论如何,你通常最好使用这些算法,但二进制数据也不是字符串数据。总之:对std::vector<unsigned char>的建议不是凭空而来的!故意避免在设计中建造难以找到的陷阱!

支持使用 char 的唯一温和合理的论点可能是关于字符串文字的论点,但即使是这样,在 C++11 中引入的用户定义的字符串文字中也站不住脚:

#include <cstddef>
unsigned char const* operator""_u (char const* s, size_t) 
{
    return reinterpret_cast<unsigned char const*>(s);
}
unsigned char const* hello = "hello"_u;

是的,你的假设是正确的。将二进制数据存储为 std::string 中的无符号字符序列。

我在使用 std::string 处理 Visual Studio 中的二进制数据时遇到了麻烦Microsoft。我已经看到字符串被莫名其妙地截断了,所以无论标准文档怎么说,我都不会这样做。