使用 union 进行转换的可移植性

Que*_*rrr 3 c unions

我想使用 RGBA 值表示 32 位数字,使用联合生成该数字的值是否可移植?考虑这个 C 代码;

union pixel {
    uint32_t value;
    uint8_t RGBA[4];
};
Run Code Online (Sandbox Code Playgroud)

这编译得很好,并且我喜欢使用它而不是一堆函数。但这安全吗?

Gab*_*les 7

使用 Union 进行“类型双关”在 C 中很好,在 gcc 的 C++ 中也很好(作为 gcc [g++] 扩展)。但是,通过联合的“类型双关”具有硬件架构字节序考虑因素

\n

这称为“类型双关” ,并且由于字节顺序考虑,它不能直接移植。不过,除此之外,这样做就好了。C 标准并没有很好地表明这很好,但显然确实如此。阅读这些答案和来源:

\n
    \n
  1. 在 C99 中是否未指定通过联合进行类型双关,而在 C11 中已指定吗?
  2. \n
  3. 联合和类型双关
  4. \n
  5. https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html#Type%2Dpunning - gcc C 和 C++ 中允许类型双关
  6. \n
\n

此外,C18 草案N2176 ISO/IEC 9899:2017在“6.5.2.3 结构和联合成员”部分中规定,脚注 97 中如下:

\n
\n
    \n
  1. 如果用于读取联合对象内容的成员与最后用于在对象中存储值的成员不同,则该值的对象表示形式的相应部分将被重新解释为新对象表示形式中的对象表示形式ntype 如 6.2.6 中所述(该过程有时称为 \xe2\x80\x9ctype 双关语\xe2\x80\x9d)。这可能是一个陷阱表示。
  2. \n
\n
\n

在此屏幕截图中查看:

\n

在此输入图像描述

\n

所以,有

\n
typedef union my_union_u\n{\n    uint32_t value;\n    /// A byte array large enough to hold the largest of any value in the union.\n    uint8_t bytes[sizeof(uint32_t)];\n} my_union_t;\n
Run Code Online (Sandbox Code Playgroud)\n

在 C++ 中,它作为 GNU gcc 扩展(但不作为 C++ 标准的一部分)value工作。请参阅@Christoph 在他的回答中的解释bytes

\n
\n

GNU 对标准 C++(和 C90)的扩展明确允许使用 union 进行类型双关。其他不支持 GNU 扩展的编译器也可能支持联合类型双关,但它不是基本语言标准的一部分。

\n
\n
\n

下载代码:您可以从我的 eRCaGuy_hello_world存储库下载并运行以下所有代码:“type_punning.c”。CC++ 的 gcc 构建和运行命令可在文件最顶部的注释中找到。

\n
\n

因此,您可以执行以下操作来读取单个字节uint32_t value

\n

技术 1:基于联合的类型双关(这“类型双关”):

\n

这就是“类型双关”的意思:将一种类型写入联合体,然后读出另一种类型,从而利用联合体进行类型“转换”。

\n
my_union_t u;\n\n// write to uint32_t value\nu.value = 1234;\n\n// read individual bytes from uint32_t value\nprintf("1st byte = 0x%02X\\n", (u.bytes)[0]);\nprintf("2nd byte = 0x%02X\\n", (u.bytes)[1]);\nprintf("3rd byte = 0x%02X\\n", (u.bytes)[2]);\nprintf("4th byte = 0x%02X\\n", (u.bytes)[3]);\n
Run Code Online (Sandbox Code Playgroud)\n

示例输出:

\n
    \n
  1. 在小端架构上:\n
    \n
    1st byte = 0xD2\n2nd byte = 0x04\n3rd byte = 0x00\n4th byte = 0x00\n
    Run Code Online (Sandbox Code Playgroud)\n
    \n
  2. \n
  3. 大端架构上:\n
    \n
    1st byte = 0x00\n2nd byte = 0x00\n3rd byte = 0x04\n4th byte = 0xD2\n
    Run Code Online (Sandbox Code Playgroud)\n
    \n
  4. \n
\n

您也可以使用原始指针从变量中获取字节,但这种技术也存在硬件架构字节序问题。

\n

如果您也想使用原始指针,则可以在没有联合的情况下完成此操作,如下所示:

\n

技术 2:读取原始指针(这不是类型双关语”):

\n
1st byte = 0xD2\n2nd byte = 0x04\n3rd byte = 0x00\n4th byte = 0x00\n
Run Code Online (Sandbox Code Playgroud)\n

示例输出:

\n
    \n
  1. 在小端架构上:\n
    \n
    1st byte = 0xD2\n2nd byte = 0x04\n3rd byte = 0x00\n4th byte = 0x00\n
    Run Code Online (Sandbox Code Playgroud)\n
    \n
  2. \n
  3. 大端架构上:\n
    \n
    1st byte = 0x00\n2nd byte = 0x00\n3rd byte = 0x04\n4th byte = 0xD2\n
    Run Code Online (Sandbox Code Playgroud)\n
    \n
  4. \n
\n

您可以使用位掩码和位移位来避免硬件架构字节序可移植性问题。

\n

为了避免上述联合类型双关原始指针方法都存在的字节顺序问题,您可以使用类似以下内容的方法。这避免了硬件架构之间的字节顺序差异:

\n

技术 3.1:使用位掩码和位移位(这不是类型双关”):

\n
1st byte = 0x00\n2nd byte = 0x00\n3rd byte = 0x04\n4th byte = 0xD2\n
Run Code Online (Sandbox Code Playgroud)\n

示例输出(上述技术与字节序无关!):

\n
    \n
  1. 所有架构上:大端小端:\n
    \n
    1st byte = 0xD2\n2nd byte = 0x04\n3rd byte = 0x00\n4th byte = 0x00\n
    Run Code Online (Sandbox Code Playgroud)\n
    \n
  2. \n
\n

或者:

\n

技术 3.2:使用便捷宏进行位掩码和位移位:

\n
uint32_t value = 1234;\nuint8_t *bytes = (uint8_t *)&value;\n\n// read individual bytes from uint32_t value\nprintf("1st byte = 0x%02X\\n", bytes[0]);\nprintf("2nd byte = 0x%02X\\n", bytes[1]);\nprintf("3rd byte = 0x%02X\\n", bytes[2]);\nprintf("4th byte = 0x%02X\\n", bytes[3]);\n
Run Code Online (Sandbox Code Playgroud)\n

示例输出(上述技术与字节序无关!):

\n
    \n
  1. 所有架构上:大端小端:\n
    \n
    1st byte = 0xD2\n2nd byte = 0x04\n3rd byte = 0x00\n4th byte = 0x00\n---------------\n1st byte = 0xD2\n2nd byte = 0x04\n3rd byte = 0x00\n4th byte = 0x00\n
    Run Code Online (Sandbox Code Playgroud)\n
    \n
  2. \n
\n

否则,如果体系结构是Little-endian ,(my_pixel.RGBA)[0]则 、 或(u.bytes)[0]可能等于byte0(如我上面所定义) ,或者如果体系结构是Big-endian ,则可能等于。byte3

\n

请参阅下面的字节顺序图: https: //en.wikipedia.org/wiki/Endianness。请注意,在 big-endian 中,任何给定变量的最高有效字节首先存储在内存中(意味着:在较低地址中),但在 Little-endian 中,首先存储的是最低有效字节(在较低地址中)。地址)在内存中。另请记住,字节顺序描述的是字节顺序,而不是顺序(字节内的位顺序与字节顺序无关),并且每个字节是 2 个十六进制字符,或“半字节”,其中半字节是 4 位。

\n

在此输入图像描述

\n

根据上面的维基百科文章,网络协议通常使用大端字节顺序,而大多数处理器(x86、大多数 ARM 等)通常是小端字节顺序(强调):

\n
\n

大尾数法是网络协议中的主导顺序,例如在互联网协议套件中,它被称为网络顺序,首先传输最高有效字节。相反,小尾数法是处理器架构(x86、大多数 ARM 实现、基本 RISC-V 实现)及其相关内存的主要顺序。

\n
\n
\n

关于标准是否支持“类型双关”的更多注释

\n

根据维基百科的“类型双关”文章,向工会成员写入value但从中读取RGBA[4]是“未指定的行为”。然而,@Eric Postpischil 在这个答案下面的评论中指出,维基百科是错误的。该答案顶部的其他参考文献也与现在编写的维基百科答案不一致。

\n

我现在理解并同意Eric Postpischil 的评论,该评论指出(强调):

\n
\n

引用的文本涉及与除最后存储的联合成员之外的联合成员相对应的字节,不适用于这种情况。例如,适用于写入2字节成员、读出short4字节成员的情况。int额外的两个字节未指定。这提供了一个 C 实现许可证,可以将存储实现为short两字节存储(保留联合的剩余字节不变)或四字节存储(可能是因为它对处理器来说是高效的)。在本例中,我们有一个四字节uint32_t成员和一个四字节uint8_t [4]成员。

\n
\n

维基百科声称(截至 2021 年 4 月 22 日):

\n

对于工会:

\n
1st byte = 0xD2\n2nd byte = 0x04\n3rd byte = 0x00\n4th byte = 0x00\n
Run Code Online (Sandbox Code Playgroud)\n
\n

my_union.ui在初始化另一个成员 后进行访问my_union.d,仍然是 C 中类型双关[4]的一种形式,结果是未指定的行为 [5](以及C++ 中的未定义行为[6])。

\n
\n

来自上面的参考文献[5]:“未指定的行为”包括:

\n
\n

与除最后存储到 (6.2.6.1) 的联合成员之外的联合成员相对应的字节值。

\n
\n

这意味着,如果您将数据存储到 union 的一个成员中,但从另一个成员中读取数据(这正是您想要使用该 union 的目的) ,则根据 C 标准,这是“未指定的行为”。

\n

在此输入图像描述

\n

我认为 gcc 允许类型双关(写入联合的一个成员,但从联合中的另一个成员读取,作为“翻译”的一种形式)作为“gcc 扩展”,但是 C 和 C++ 标准,如果在您的中-Wpedantic使用建立标志,否则禁止它。

\n

也可以看看:

\n
    \n
  1. 从我的存储库下载并运行上述所有代码:https://github.com/ElectricRCAircraftGuy/eRCaGuy_hello_world/blob/master/c/type_punning.c
  2. \n
  3. 实践中的联合、别名和类型双关:什么有效,什么无效?
  4. \n
  5. 联合和类型双关
  6. \n
  7. [我的存储库] 我将READ_BYTE()宏添加到我的eRCaGuy_hello_world存储库中的utilities.h文件中。
  8. \n
  9. 在哪里可以找到当前的 C 或 C++ 标准文档?
  10. \n
  11. https://news.ycombinator.com/item?id=17263328 \n
      \n
    1. 在 C99 中是否未指定通过联合进行类型双关,而在 C11 中已指定吗?<== 特别请参阅此处。显然,C 标准并没有很好地阐明这一点。
    2. \n
    \n
  12. \n
  13. 我的更多答案:\n
      \n
    1. 答案 1/3:使用 union 和 Packed struct
    2. \n
    3. 答案 2/3:通过手动位移位将结构体转换为字节数组
    4. \n
    5. 答案 3/3:使用压缩结构和指向它的原始 uint8_t 指针
    6. \n
    \n
  14. \n
\n

  • 我花了足够的时间在互联网争论上,真的不想处理来自持不同观点的人的竞争性编辑:)无论如何,解决方案是链接到规范的SO问题,我认为 (2认同)
  • 引用的文本涉及与除最后存储的联合成员之外的联合成员相对应的字节,不适用于这种情况。例如,它适用于写入两字节“short”成员并读取四字节“int”成员的情况。额外的两个字节未指定。这提供了一个 C 实现许可证,可以将“short”的存储实现为两字节存储(保留联合的剩余字节不变)或四字节存储(可能是因为它对处理器来说是高效的)。在当前的情况下,我们有一个四字节的“uint32_t”成员和一个四字节的“uint8_t [4]”成员。 (2认同)