【问题标题】:Casting small fields in a structure to a larger variable将结构中的小字段转换为更大的变量
【发布时间】:2014-04-02 11:19:54
【问题描述】:

我有一个遗留代码的情况,结构的一个大字段被分成两个子字段。比如一个uint32被拆分成两个uint16的:

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

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

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

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

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

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

【问题讨论】:

    标签: c struct c89 strict-aliasing


    【解决方案1】:

    您的“优化”解决方案存在一些问题:

    1. 违反了strict aliasing rule
    2. 它不适用于 little-endian 机器(当今绝大多数计算机),因为您没有正确重新排序 high 和 low 字段。
    3. 不保证myVar_HmyVar_L 在内存中是连续的:允许编译器在两个字段之间添加填充。

    实现您想要的合法且独立于平台的方法是保持您之前的解决方案不断变化。它不应该带来任何性能问题。

    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;
    

    编辑以下评论:你说你在大端机器上工作,所以内存顺序不是问题。在这种情况下,您可能会做的“最合法”的事情是使用匿名结构使您的结构成为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
    

    【讨论】:

    • 顺便说一句,我使用的是大端 MCU(ColdFire),所以没关系。当然,填充是一种风险,可以通过强制填充大小为 1(通过 pragma)来解决。
    • @presiuslitelsnoflek 在阅读了有关严格别名的更多内容后,我了解到别名问题是指uint16uint32,对吗?并不是说我将演员阵容中的两个字段都包含在 uint32 中 - 这是演员阵容的副作用,不是吗?
    • @Eli 没错,它们不是兼容的类型。包含这两个字段的事实确实“只是”一种副作用,但正是导致发明严格别名规则的那种副作用。
    • union 版本打破了别名规则,就像 OP 的原始代码一样。我看不出它比 OP 的版本有什么好处。 (当然,您最初使用算术的建议是最好的,编译器无论如何都应该从中生成最佳代码)
    • @Matt 没错,理论上union 也打破了别名,根据标准是UB。但是,由于 OP 愿意承担其他风险(假设大字节序和没有填充),并且由于 the union trick is well defined under GCC,我认为这在 OP 的约束下或多或少是可以接受的解决方案。
    【解决方案2】:

    关于您的具体问题:

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

    答案是:它违反了严格的别名规则。见What is the strict aliasing rule?。编译器可以为所欲为,包括完全忽略语句:

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

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

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

    当然,在本例中,&amp;s.myVar_H&amp;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 的目标是用 minimal 数量的更改来更新 工作的旧 代码,而不是进行完全重写。

    另见:


    更新 #2:也许我可以提出一个更有说服力的案例?对于任何构想,都可以从以下方面争论:

    • 合法性
    • 效率
    • 优雅

    对于第三个标准,优雅,与例如工会双关语。

    对于第二个标准,效率,John Regehr 在Embedded in Academia, "Type Punning, Strict Aliasing, and Optimization"(2013 年 6 月)中就类似问题提出:

    诸如 GCC 4.8.1 和 Clang ~3.3 之类的编译器(都适用于 x86-64 Linux)无法为 c2 [union 的类型双关语功能的示例] 生成好的代码 ... [并给出] 糟糕的目标代码

    而在他的示例c5 中使用memcpy

    两个编译器对memcpy 的理解都足够深入,我们可以得到所需的[最佳] 目标代码。

    在我看来,c5 是这小批函数中最容易理解的代码,因为它不会进行混乱的转换,而且它完全、完全、显然没有混乱规则可能引起的并发症用于联合和严格的别名。几年前,当我发现编译器可以看穿memcpy 并生成正确的代码时,它成为我首选的类型双关语。

    最后,让我们看看第一个标准,合法性。 Regehr 关于工会双关语的陈述(强调添加):

    不幸的是,这段代码 [c2,使用联合双关语] 也是现代 C/C++ 方言未定义(或者可能只是未指定,我不完全确定)。

    我不愿意不同意 Regehr,但关于 Stack Overflow 的共识似乎是工会双关语自 C99 以来一直有效;见:

    但这是 OP 专门询问的 C89 中定义的行为。

    最后请注意 OP 要求:

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

    memcpy 解决方案保持头文件不变,如果您与一个敏感的程序员团队一起工作,这是有利的。您可以通过将宏 LEGACY_CPY32 重新定义为之前的“不正确”但可以正常工作的功能,轻松撤销所有更改:

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

    【讨论】:

    • 您如何考虑跨不同编译器和硬件平台的结构中的填充?请注意,我不赞成您使用“最安全”一词。如果你不包括这个,我就不会投反对票。就目前而言,这是完全不正确的。请参阅 presius 的回答。
    • @SanJacinto:谢谢你的建议;我已经编辑了我的答案。而且我相信普雷修斯的回答是倒退的。
    • @JosephQuinsey:为什么这可以避免抗锯齿问题?行为似乎相似 - 基本上将 &amp;s.myVar_H 视为指向 uint32 或 4 个字节的指针。将两个连续的struct 字段称为单个字段的“危险”是相同的。
    • @JosephQuinsey 同意,他把它倒过来了,但语义是正确的。我还是不喜欢你的解决方案。除了人为错误之外,移位和字节算术本质上没有什么“不安全”的。您的解决方案包含内置问题,并不是真正的解决方案。
    • 您的memcpy 变体仍然存在同样的别名问题,只是形式不同。您通过不同类型的左值读取存储为一种类型的字节。它没有比原版更好或更差。 (在存在对齐问题的情况下,memcpy 版本“更好”,因为它避免了对齐问题,但是两个版本仍然违反别名规则)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-11-17
    • 1970-01-01
    • 2010-10-25
    • 2021-06-28
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多