首页 > 解决方案 > 使用 union 进行转换的可移植性

问题描述

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

union pixel {
    uint32_t value;
    uint8_t RGBA[4];
};

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

标签: cunion

解决方案


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

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

  1. 是否通过 C99 中未指定的联合进行类型双关,并且它是否已在 C11 中指定?
  2. 工会和类型双关语
  3. https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html#Type%2Dpunning - gcc C 和 C++ 中允许类型双关

此外,C18 草案N2176 ISO/IEC 9899:2017在“6.5.2.3 结构和工会成员”部分中声明,脚注 97 如下:

  1. 如果用于读取联合对象内容的成员与上次用于在对象中存储值的成员不同,则将值的对象表示的适当部分重新解释为新类型中的对象表示在 6.2.6 中描述(有时称为“类型双关语”的过程)。这可能是一个陷阱表示。

在此屏幕截图中查看它:

在此处输入图像描述

所以,有

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

作为一种翻译方式,valuebytesC 中就很好了。在 C++ 中,它作为 GNU gcc 扩展(但不是 C++ 标准的一部分)工作。请参阅@Christoph 在他的回答中的解释

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


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


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

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

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

my_union_t u;

// write to uint32_t value
u.value = 1234;

// read individual bytes from uint32_t value
printf("1st byte = 0x%02X\n", (u.bytes)[0]);
printf("2nd byte = 0x%02X\n", (u.bytes)[1]);
printf("3rd byte = 0x%02X\n", (u.bytes)[2]);
printf("4th byte = 0x%02X\n", (u.bytes)[3]);

样本输出:

  1. 小端架构上:
    1st byte = 0xD2
    2nd byte = 0x04
    3rd byte = 0x00
    4th byte = 0x00
    
  2. 大端架构上:
    1st byte = 0x00
    2nd byte = 0x00
    3rd byte = 0x04
    4th byte = 0xD2
    

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

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

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

uint32_t value = 1234;
uint8_t *bytes = (uint8_t *)&value;

// read individual bytes from uint32_t value
printf("1st byte = 0x%02X\n", bytes[0]);
printf("2nd byte = 0x%02X\n", bytes[1]);
printf("3rd byte = 0x%02X\n", bytes[2]);
printf("4th byte = 0x%02X\n", bytes[3]);

样本输出:

  1. 小端架构上:
    1st byte = 0xD2
    2nd byte = 0x04
    3rd byte = 0x00
    4th byte = 0x00
    
  2. 大端架构上:
    1st byte = 0x00
    2nd byte = 0x00
    3rd byte = 0x04
    4th byte = 0xD2
    

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

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

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

uint32_t value = 1234;

uint8_t byte0 = (value >> 0)  & 0xff;
uint8_t byte1 = (value >> 8)  & 0xff;
uint8_t byte2 = (value >> 16) & 0xff;
uint8_t byte3 = (value >> 24) & 0xff;

printf("1st byte = 0x%02X\n", byte0);
printf("2nd byte = 0x%02X\n", byte1);
printf("3rd byte = 0x%02X\n", byte2);
printf("4th byte = 0x%02X\n", byte3);

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

  1. 所有架构上:big-endianlittle-endian
    1st byte = 0xD2
    2nd byte = 0x04
    3rd byte = 0x00
    4th byte = 0x00
    

或者:

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

#define BYTE(value, byte_num) ((uint8_t)(((value) >> (8*(byte_num))) & 0xff))

uint32_t value = 1234;

uint8_t byte0 = BYTE(value, 0);
uint8_t byte1 = BYTE(value, 1);
uint8_t byte2 = BYTE(value, 2);
uint8_t byte3 = BYTE(value, 3);

// OR

uint8_t bytes[] = {
    BYTE(value, 0), 
    BYTE(value, 1), 
    BYTE(value, 2), 
    BYTE(value, 3), 
};

printf("1st byte = 0x%02X\n", byte0);
printf("2nd byte = 0x%02X\n", byte1);
printf("3rd byte = 0x%02X\n", byte2);
printf("4th byte = 0x%02X\n", byte3);
printf("---------------\n");
printf("1st byte = 0x%02X\n", bytes[0]);
printf("2nd byte = 0x%02X\n", bytes[1]);
printf("3rd byte = 0x%02X\n", bytes[2]);
printf("4th byte = 0x%02X\n", bytes[3]);

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

  1. 所有架构上:big-endianlittle-endian
    1st byte = 0xD2
    2nd byte = 0x04
    3rd byte = 0x00
    4th byte = 0x00
    ---------------
    1st byte = 0xD2
    2nd byte = 0x04
    3rd byte = 0x00
    4th byte = 0x00
    

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

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

在此处输入图像描述

根据上面的 Wikipedia 文章,网络协议通常使用big-endian字节顺序,而大多数处理器(x86、大多数 ARM 等)通常是little-endian(强调添加):

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


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

根据维基百科的“类型双关语”文章,写信给工会成员value但阅读RGBA[4]是“未指定的行为”。但是,@Eric Postpischil 在此答案下方的评论中指出,维基百科是错误的。该答案顶部的其他参考资料也与现在编写的维基百科答案不一致。

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

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

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

对于工会:

union {
    unsigned int ui;
    float d;
} my_union = { .d = x };

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

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

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

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

在此处输入图像描述

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

也可以看看:

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

关键字:C 中的类型双关语,将类型和结构转换为 C 中的字节


推荐阅读