将结构中的小字段转换为更大的变量

时间:2014-04-02 11:19:54

标签: c struct c89 strict-aliasing

我在遗留代码中遇到一种情况,其中一个结构的大字段被分成两个子字段。例如,uint32被分为两个uint16

typedef struct
{
    uint16 myVar_H;
    uint16 myVar_L;
} MyStruct;

到目前为止,这些字段是通过转移到每个uint32部分来从uint16变量中读取的。现在我们要对此进行优化,以便通过将第一个字段的地址转换为uint32来读取整个uint32

MyStruct s;
*((uint32*)(&s.myVar_H)) = myValue;

这适用于我的编译器。问题是这是否合法并在C89(或其他C标准)中定义?

注意:更改结构是另一种选择,但由于这是一个基于专有协议的古老遗留代码,我宁愿不触及结构定义。

编辑:我使用的是大端MCU(ColdFire),所以这在字节方面可以正常使用。

2 个答案:

答案 0 :(得分:4)

您的"优化"存在一些问题。溶液:

  1. 违反了strict aliasing rule
  2. 它不适用于小端机器(现在绝大多数计算机),因为你没有正确地重新排序高低字段。
  3. 无法保证myVar_HmyVar_L在内存中是连续的:允许编译器在两个字段之间添加填充。
  4. 实现目标的法律和平台独立方式是保持以前的解决方案不变。它不应该带来任何性能问题。

    MyStruct s;
    // Reading
    uint32 u = (uint32)s.myVar_H << 16 | s.myVar_L; // The cast is important
    // Writing
    s.myVar_H = myValue >> 16;
    s.myVar_L = myValue % 0xFFFF;
    

    编辑以下评论:您说您在大端计算机上工作,因此内存顺序不是问题。在这种情况下,最合法的&#34;您可能要做的事情是使您的结构union具有匿名结构(不是实际C89,它是gcc扩展名。这样您就不会破坏严格的别名,新定义也不会破坏任何现有代码。

    typedef union // Your type is now a union
    {
        struct
        {
             uint16 myVar_H;
             uint16 myVar_L;
        }; // Anonymous struct, legal C11 but gcc extension in C89
    
        uint32 myVar;
    } MyStruct;
    
    s.myVar = myValue; // Safe
    

答案 1 :(得分:-2)

关于您的具体问题:

  

问题是这是否合法并在C89中定义?

答案是 :它违反了严格的别名规则。见What is the strict aliasing rule?。编译器可以随意做任何事情,包括完全忽略语句:

   *((uint32*)(&s.myVar_H)) = myValue;

更安全的方法是使用memcpy;您的代码段变为:

   MyStruct s;
   memcpy(&s.myVar_H, &myValue, sizeof myValue);

当然,在此特定情况下,&s.myVar_H&s相同。

这假定myValue必须具有正确的尺寸和格式(例如,它不能是charfloat。事实并非如此,如果你有C99的复合文字,你可以写:

   memcpy(&s.myVar_H, (uint32[]){myValue}, sizeof(uint32));

这很难看,但你可以在宏中隐藏它的丑陋:

   #define LEGACY_CPY32(dst, src) memcpy(&(dst), (uint32[]){src}, sizeof(uint32))

但是这些建议会避免 严格别名问题。 Litte-endian问题和结构填充问题仍然存在,如果您切换机器和/或编译器,甚至只是更改编译器开关,您的代码可能会中断。


更新#1 :当然没有人会像这样写新代码。但我假设OP的目标是使用 最小 更改次数来更新正常运行的代码,而不是完全重写。< / p>

另见:


更新#2 :也许我可以提出一个更有说服力的案例?对于任何构造,人们都可以争论:

  • 合法性
  • 效率
  • 优雅

对于第三个标准,优雅,我的建议失去了手,与例如工会双关语。

对于第二个标准,效率Embedded in Academia, "Type Punning, Strict Aliasing, and Optimization"(2013年6月)中的John Regehr对类似问题进行了陈述:

  

GCC 4.8.1和Clang~3.3(两者都适用于x86-64 Linux)的编译器无法为c2生成良好的代码[工会类型惩罚功能的一个例子] ...... [和蹩脚的目标代码

而在他的示例c5中使用memcpy

  

两个编译器都非常了解memcpy,我们得到了所需的[最佳]目标代码。

     

在我看来,c5是最容易理解这一小部分功能的代码,因为它不会造成混乱的转移,而且它完全显然没有可能由于工会的混乱规则和严格的别名。几年前,当我发现编译器可以看到memcpy并生成正确的代码时,它成为了我最喜欢的打字习惯。

最后,让我们看看第一个标准,合法性。 Regehr声明关于工会双关语(强调补充):

  

不幸的是,这个代码[c2,使用union punning]也是 undefined 的现代C / C ++方言(或者只是未指定,我不是完全肯定)。

我不愿意与Regehr意见不一致,但Stack Overflow的共识似乎是因为C99之后工会投注已经有效;见:

但是在C89中它是不是定义的行为,OP特别询问。

最后注意OP请求:

  

由于这是一个基于专有协议的古老遗留代码,我宁愿不触及结构定义

memcpy解决方案使头文件不受影响,如果您与一群敏感的程序员合作,这是有利的。您可以通过将宏LEGACY_CPY32重新定义为之前的“不正确”但工作功能轻松退出所有更改:

 #define LEGACY_CPY32(dst, src) (*((uint32*)(&(dst))) = (src))