【问题标题】:Struct pointer compatibility结构指针兼容性
【发布时间】:2012-01-31 22:17:24
【问题描述】:

假设我们有两个结构体:

typedef struct Struct1
{
    short a_short;
    int id;
} Struct1;

typedef struct Struct2
{
    short a_short;
    int id;
    short another_short;
} Struct2;

Struct2 * 转换为Struct1 * 是否安全? ANSI 规范对此有何评论? 我知道一些编译器可以选择重新排序结构字段以优化内存使用,这可能会导致两个结构不兼容。无论编译器标志如何,有什么方法可以确保此代码有效?

谢谢!

【问题讨论】:

  • 重新排序标准 AFAIK 不允许成员。我相信虽然插入不同数量的填充是允许的。
  • @delnan 哦,那么 struct 'packing' 只会禁用对齐?谢谢,我不知道!

标签: c casting struct ansi


【解决方案1】:

语言规范包含以下保证

6.5.2.3 结构和联合成员
6 为了简化联合的使用,提供了一项特殊保证:如果联合包含 几个结构共享一个共同的初始序列(见下文),如果联合 对象当前包含这些结构之一,允许检查常见的 它们中任何一个的初始部分,任何地方的完整类型的声明 可见。如果对应的成员,两个结构共享一个共同的初始序列 对于一个或多个序列具有兼容的类型(并且对于位域,具有相同的宽度) 初始成员。

这仅适用于通过联合的类型双关语。但是,这基本上保证了这些结构类型的初始部分将具有相同的内存布局,包括填充。

上述内容并不一定允许通过强制转换不相关的指针类型来做同样的事情。这样做可能会违反别名规则

6.5 表达式
7 对象的存储值只能由具有以下之一的左值表达式访问 以下类型:
— 与对象的有效类型兼容的类型,
— 与对象的有效类型兼容的类型的限定版本,
— 对应于有效类型的有符号或无符号类型 对象,
— 一种类型,它是有符号或无符号类型,对应于 对象的有效类型,
— 聚合或联合类型,其中包括上述类型之一 成员(递归地包括子聚合或包含联合的成员),或
— 一种字符类型。

这里唯一的问题是是否访问

((Struct1 *) struct2_ptr)->a_short

构成对整个 Struct2 对象的访问(在这种情况下,它违反了 6.5/7 并且它是未定义的),或者只是对 short 对象的访问(在这种情况下,它可能是完美定义的)。

一般来说,坚持以下规则可能是个好主意:类型双关语允许通过联合但不允许通过指针。不要通过指针来实现,即使您正在处理具有共同初始成员子序列的两个 struct 类型。

【讨论】:

  • 在这些规则下,即使struct s {int x;} foo; ... foo.x=1; 也会调用 UB,因为这些规则不允许通过成员类型的左值访问聚合。无论标准是否要求,质量编译器在这种情况下都应该表现得明智,但在代码使用新派生的指向联合成员的指针的情况下也是如此。标准允许编译器在这种情况下做傻事的事实并不意味着这样做的编译器不应该被认为质量低劣。
  • @supercat:我不是这个意思。绝对不能将访问成员x 本身解释为访问整个foo。那没有任何意义。我关心的是foo.x 表达式的初始部分。 . 之前的 foo 本身是否构成对整个 foo 对象的访问? IE。 . 运算符对其左操作数的应用是否正式构成对整个左操作数的访问?
  • 如果是这样,那么 (*(Struct1 *) struct2_ptr).a_short 已经未定义,因为 (*(Struct1 *) struct2_ptr). 单独部分。甚至在我们到达a_short 部分之前。
  • 使用从整体中新派生的指针或左值访问聚合成员访问整体以及——在联合的情况下——其他成员也是。通过两个细微的变化,6.5p7 将消除 90% 的 -fno-strict-aliasing 需求以及适得其反的“有效类型规则”和“字符类型例外”。简单地说,从访问中使用的左值必须具有正确的类型,或从其他具有正确类型的新派生,并将重构限制为在函数或循环的特定执行期间访问的对象(包括嵌套的)。
  • 如果一个人将动词“读/写地址一个字节”当作一个指针或左值,在未来的任何时候,无需清洗,就可以访问或寻址一个字节,那么为了访问每个字节,指针或左值 L 将保持新鲜,直到在不使用从 L 派生的指针或左值的情况下访问或寻址该字节,或者直到代码进入发生这种情况的函数或循环。在这些情况下识别推导是很容易的,我怀疑 C89 的作者曾经想过编译器作者会强烈拒绝这样做。
【解决方案2】:

不,标准不允许这样做;通过 Struct1 指针访问 Struct2 对象的元素是未定义的行为。 Struct1 和 Struct2 是不兼容的类型(如 6.2.7 中所定义),并且可能填充不同,并且通过错误的指针访问它们也违反了别名规则。

保证这样的事情的唯一方法是当 Struct1 作为其初始成员包含在 Struct2 中时(标准中为 6.7.2.1.15),如 unwind's answer

【讨论】:

  • 这是一个好点。然而,语言规范在谈到 trap 表示 时提供了一些相当相关的推理过程。它说结构类型的对象(作为一个整体)永远不会被视为陷阱表示,即使它们的字段当前包含陷阱表示。这意味着该语言在访问整个结构对象和访问特定字段之间做出了明确的区分。
  • 这让人怀疑((Struct1 *) struct2_ptr)->a_short 是否真的未定义。 *(Struct1 *) struct2_ptr 绝对是未定义的(严格混叠违规)。但是从标准中的当前措辞来看,((Struct1 *) struct2_ptr)->a_short 是否可以在我看来并不明确。
  • 标准也不清楚p->x是访问*p还是只访问x,不幸的是这一点对于应用严格的别名规则至关重要。常见的编译器似乎将其视为为此目的访问*p
  • 对非字符类型的聚合成员的访问属于标准作者未强制要求的行为类别,因为他们希望编译器在可行时支持它们。 请注意,即使使用普通成员访问顺序直接访问聚合成员也属于同一类别
  • @M.M:实际上,标准非常明确。左值p->x 的类型是成员的类型。标准应该认识到,除了被读取或写入之外,左值也可以用于派生另一个左值,并认识到对“新派生”左值的操作是父操作,但它实际上并没有说任何这样的事情,将这种识别作为实施质量问题留在会导致行为被定义的情况下,否则不会被定义,但不允许在它的情况下进行优化会做相反的事情。
【解决方案3】:

是的,可以这样做!

示例程序如下。

#include <stdio.h>

typedef struct Struct1
{
    short a_short;
    int id; 
} Struct1;

typedef struct Struct2
{
    short a_short;
    int id; 
    short another_short;
} Struct2;

int main(void) 
{

    Struct2 s2 = {1, 2, 3}; 
    Struct1 *ptr = &s2;
    void *vp = &s2;
    Struct1 *s1ptr = (Struct1 *)vp;

    printf("%d, %d \n", ptr->a_short, ptr->id);
    printf("%d, %d \n", s1ptr->a_short, s1ptr->id);

    return 0;
}

【讨论】:

  • “示例程序”并没有完全证明此类行为的合法性。
【解决方案4】:

据我所知,这是安全的。

但如果可能的话,这样做要好得多:

typedef struct {
    Struct1 struct1;
    short another_short;
} Struct2;

然后你甚至告诉编译器Struct2Struct1 的实例开头,并且由于指向结构的指针总是指向它的第一个成员,你可以安全地将Struct2 * 视为Struct1 *.

【讨论】:

  • 好吧,如果发现某一天 offsetof( Struct1.a_short ) 不等于 offsetof( Struct2.a_short ) 的可能性很小,那么发现某一天 offsetof( Struct2.struct1 ) 的可能性是相等的不等于零。 (这意味着&amp;struct2 != (Struct2*)&amp;struct2.struct1)。
  • 确实,这种方式好多了! :) 谢谢!
  • 如果 struct1 和 struct2 都将“int”放在首位,而“int”需要 32 位对齐,那么这两种结构类型都可以是 8 个字节,但您的 Struct2 的替代形式需要 12 个字节。如果编译器遵守通用初始序列规则,则任何一种形式都应该是有效的(并且 8 字节形式会更有效),但即使在 C89 模式下调用,gcc 也不再支持 C89 的保证,除非使用 -fno-strict-aliasing 标志.
【解决方案5】:

它很可能会起作用。但是您非常正确地询问如何确保此代码有效。所以:在你的程序的某个地方(可能在启动时)嵌入了一堆 ASSERT 语句,确保 offsetof( Struct1.a_short ) 等于 offsetof( Struct2.a_short ) 等。此外,除了你之外的一些程序员可能有一天会修改其中一个结构,但不是其他,安全总比抱歉好。

【讨论】:

  • 谢谢 Mike,我一定会添加一些断言来确保这一点!
  • @R.. 静态断言?我不知道它们存在。 I looked it up and found out。你是对的,谢谢。
  • #define static_assert(p) struct{char dummy[2*!!(p)-1];}
【解决方案6】:

结构指针类型在 C 中始终具有相同的表示形式。

(C99, 6.2.5p27) "所有指向结构类型的指针都应具有相同的 表示和对齐要求。”

并且结构类型中的成员在 C 中总是按顺序排列的。

(C99, 6.7.2.1p5) “结构是由一系列 成员,其存储按顺序分配"

【讨论】:

  • 这不能回答问题;即使有这些限制,它仍然可能是混叠违规。但是,在某些条件下,C 标准确实明确允许 OP 想要的。
  • 非常感谢 ANSI 规范中的这些引用。这对我来说清楚地表明这是安全的!
  • @R.. 这是一个很好的观点。如果强制转换的指针被取消引用,这仍然可能违反 C 别名规则。如果实现利用严格的别名规则,这可能被认为是不安全的。
  • 即使我们忽略严格的别名要求,这些引号也不够使其安全。保证相同填充量的报价在哪里?
  • @AnT:如果填充可能不同,编译器很难支持通用初始序列保证。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-07-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-05-31
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多