将一个结构体强制转换为包含它的联合体是合法的吗?

9

这是对于一个有关打印不同结构体中的共同成员问题的后续。

我认为联合体可以允许检查它们两个元素的共同初始序列。因此,我最终用了以下代码:

#include <stdio.h>

struct foo {
    const char *desc;
    float foo;
};
struct bar {
    const char *desc;
    int bar;
};
union foobar {
    struct foo foo;
    struct bar bar;
};

void printdesc(const union foobar * fb) {
    printf("%s\n", fb->foo.desc);          // allowed per 6.5.2.3 Structure and union members
}

int main() {

    struct bar bb = {"desc bar", 2};

    union foobar fb = { .bar=bb};

    printdesc((union foobar *) &(fb.bar)); // allowed per 6.7.2.1 Structure and union specifiers
    printdesc((union foobar *) &bb);       // legal?

    return 0;
}

它可以在没有任何警告的情况下编译,并且给出了预期的结果。

desc bar
desc bar

这里关键的是带有// legal?注释的那一行。我已经将一个bar *转换为了foobar *。当barfoobar联合体的成员时,根据6.7.2.1结构和联合体说明符,这是允许的。但在这里,我不确定。
如果bar没有声明为foobar的成员,那么将指向bar对象的指针转换为指向foobar对象的指针是合法的吗?
问题并不是关于它是否可以在特定编译器中工作。我相当确定,在它们当前的版本中,所有常用的编译器都能够处理。问题是关于它是否是合法的C代码。
以下是我的研究结果。
来自C11草案n1570的参考文献:
6.5.2.3 结构体和联合体成员§6

... 如果一个联合包含多个共享公共初始序列(见下文)的结构体,并且如果该联合对象当前包含其中之一,则允许检查它们中任何一个的公共初始部分...

6.7.2.1 结构体和联合体说明符§16

... 适当转换后,指向联合对象的指针指向其每个成员......反之亦然...


根据注释88所述,“此列表的目的是指定对象可能或不可能被别名化的情况”,因此您可能需要考虑6.5p7中的列表。 - user3386109
@user3386109:我认为6.5p7中包含上述类型之一的聚合或联合类型可以满足我的使用情况。 - Serge Ballesta
1
@SergeBallesta 我的经验是6.5p7写得非常糟糕,你永远不可能让任何两个人对它的含义达成一致。你可能想要查看C17规范,看看这段话是否有任何变化。 - user3386109
1
@user3386109 指针转换规则在C标准中也没有明确规定。比如,6.7.2.1p16中的“适当转换”是什么意思?因此,回答这样的问题真的是一项毫无回报的工作。 - Language Lawyer
1
@LanguageLawyer — 我很惊讶“适当转换”的短语会引起麻烦。假设您有union U { Member1 m1; Member2 m2; }。进一步地,给定union U *up;,'suitably converted'的注释意味着Member1 *mp1 = (Member1 *)up;是良好定义的,Member2 *mp2 = (Member2 *)up;也是如此。尝试OtherType *otp = (OtherType *)up;不是“适当转换”,Member1 *mp1 = (Member2 *)up;也不是——尽管后者的结果可能相同,即使它不是“适当转换”。 - Jonathan Leffler
显示剩余6条评论
2个回答

1

我脑海中首先想到的一个严重问题是:

void copydesc(const union foobar * fb, union foobar * fb_copy) {
  static_assert(sizeof(*fb) == sizeof(union foobar), "broken compiler");
  // NOTE: Buffer-overflow when struct cast smaller than `union foobar`.
  memcpy(fb_copy, fb, sizeof(*fb));
}

大多数程序员不会注意到这一点,而那些注意到的人都太珍贵了,不值得浪费时间。而编译器/静态分析工具可能不会警告您的技术。
一个更安全的方法,但仍然不一定是完美的,是添加一个具有相同共享字段的结构体,添加构造函数测试,并像您在示例中精心打包它们一样将常见的结构体列在首位,以使其更易读。
当您发现自己在思考某些不太常见的构造是否正确时,这是一个不好的迹象。可以说,如果您真的知道自己在做什么(这是C的一个特性),这些构造是有用的,但这会引入重大风险,即独立正确部分(如上面的缓冲区溢出)的意外连锁反应会导致严重问题。

0
一般来说,不行
假设您有以下定义:
struct foo {
    int flag;
    double foo;
};
struct bar {
    int flag;
    int bar;
};
union foobar {
    struct foo foo;
    struct bar bar;
};

struct foo的对齐方式可能是8,而struct bar的对齐方式可能是4。所以如果你这样做:

struct bar b;
union foobar *fb = (union foobar *)&b;

您可能会遇到对齐问题。C11的6.3.2.3p7节规定:

对象类型的指针可以转换为不同对象类型的指针。如果结果指针未正确对齐于引用类型,则行为是未定义的。否则,当再次转换时,结果应与原始指针相等。将对象的指针转换为字符类型的指针时,结果指向对象的最低地址字节。结果的连续增量,直到对象的大小,产生指向对象其余字节的指针。

因此,如果b未对齐于8字节边界,则行为是未定义的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接