比较结构指针、丢弃成员和 UB

Comparing struct pointers, casting away members, and UB

考虑以下代码:

int main()
{
    typedef struct { int first; float second; } type;

    type whole = { 1, 2.0 };
    void * vp = &whole;

    struct { int first; } * shorn = vp;
    printf("values: %d, %d\n", ((type *)vp)->first, shorn->first);
    if (vp == shorn)
        printf("ptrs compare the same\n");

    return 0;
}

两个问题:

  1. 指针相等比较是否UB?
  2. 关于初始化 shorn 行中 second 成员的“剪切”:像这样丢弃结构成员然后取消引用操纵指针以访问剩余成员?

当一个指针是 void * 时,将两个指针与 == 进行比较定义明确。

C standard 第 6.5.9 节关于相等运算符 == 的内容如下:

2 One of the following shall hold:

  • both operands have arithmetic type;
  • both operands are pointers to qualified or unqualified versions of compatible types;
  • one operand is a pointer to an object type and the other is a pointer to a qualified or unqualified version of void; or
  • one operand is a pointer and the other is a null pointer constant

...

5 Otherwise, at least one operand is a pointer. If one operand is a pointer and the other is a null pointer constant, the null pointer constant is converted to the type of the pointer. If one operand is a pointer to an object type and the other is a pointer to a qualified or unqualified version of void, the former is converted to the type of the latter.

shorn->first 的用法之所以有效,是因为指向结构的指针可以转换为指向其第一个成员的指针。对于 type 和未命名的结构类型,它们的第一个成员是 int 所以它可以解决。

在编写 C89 标准来描述的语言中,如果两个结构共享一个公共初始序列,则可以将指向其中一个的指针强制转换为另一个,并用于检查该公共初始序列的成员.依赖于此的代码是司空见惯的,甚至没有被认为是有争议的。

为了优化,C99 标准的作者故意允许编译器假设不同类型的结构不会别名在这种假设对他们的客户有用的情况下.因为有许多好的方法可以让实现识别这样的假设会不必要地破坏完美代码的情况,并且因为他们标准的作者期望编译器编写者会做出善意的努力以有用的方式行事对于使用其产品的程序员,该标准不强制要求任何特定 方法来进行此类区分。相反,它将支持已被普遍支持的构造的能力视为“实现质量”问题,如果编译器编写者做出真正的努力来对待它,这将是合理的。

不幸的是,一些对向付费客户销售他们的产品不感兴趣的编译器作者将标准未能强制要求有用的行为视为邀请以不必要的无用方式行事。因此,如果不使用非标准语法或完全禁用基于类型的别名,clang 或 gcc 无法对依赖于公共初始序列保证的代码进行有意义的处理。

C 标准的6.2.5 类型28 说:

[...] All pointers to structure types shall have the same representation and alignment requirements as each other. [...]

6.3.2.3 指针 1 说:

A pointer to void may be converted to or from a pointer to any object type. A pointer to any object type may be converted to a pointer to void and back again; the result shall compare equal to the original pointer.

7 段说:

A pointer to an object type may be converted to a pointer to a different object type. If the resulting pointer is not correctly aligned68) for the referenced type, the behavior is undefined. Otherwise, when converted back again, the result shall compare equal to the original pointer. [...]

脚注 68 说:

In general, the concept "correctly aligned" is transitive: if a pointer to type A is correctly aligned for a pointer to type B, which in turn is correctly aligned for a pointer to type C, then a pointer to type A is correctly aligned for a pointer to type C.

因为所有指向结构类型的指针都具有相同的表示形式,所以对于所有指向结构类型的指针,指向 void 的指针和指向结构类型的指针之间的转换必须相同。因此,指向结构类型 A 的指针似乎可以通过强制转换运算符直接转换为指向结构类型 B 的指针,而无需中间转换为指向 void 的指针,只要指针针对结构“正确对齐” B型。(这可能是一个弱论点。)

问题仍然存在,在两种结构类型 A 和 B 的情况下,结构类型 A 的初始序列由结构类型 B 的所有成员组成,指向结构类型 A 的指针保证正确对齐对于结构类型 B(反过来显然不能保证)。据我所知,C 标准没有这样的保证。所以严格来说,指向较大结构类型 A 的指针可能无法与较小结构类型 B 正确对齐,如果不是,则行为未定义。对于“理智”的编译器,较大的结构类型 A 的对齐不会比较小的结构类型 B 弱,但对于“理智”的编译器,情况可能并非如此。


关于使用从完整(较长)结构派生的指针访问截断(较短)结构的成员的第二个问题,那么只要指针针对较短结构正确对齐(参见上文了解为什么对于“疯狂的”编译器可能不是这样),只要避免严格的别名规则(例如,通过在跨编译单元边界的中间外部函数调用中通过中间指针指向 void),然后通过访问成员指向较短结构类型的指针应该完全没问题。当两种结构类型的对象作为同一联合类型的成员出现时,有一个特殊的保证。 6.3.2.3 结构和联合成员 6 说:

One special guarantee is made in order to simplify the use of unions: if a union contains several structures that share a common initial sequence (see below), and if the union object currently contains one of these structures, it is permitted to inspect the common initial part of any of them anywhere that a declaration of the completed type of the union is visible. Two structures share a common initial sequence if corresponding members have compatible types (and, for bit-fields, the same widths) for a sequence of one or more initial members.

然而,由于成员在结构类型中的偏移量不依赖于结构类型的对象是否出现在联合类型中,以上意味着任何具有共同初始成员序列的结构都将具有那些在各自结构类型中具有相同偏移量的公共成员。