别名结构和数组是否合法?

在结构中相同类型的连续成员之间的指针算术过去常常是一种常见的做法,而指针算术仅在数组内有效。 在C ++中,它将是明确的Undefined Behavior,因为数组只能由声明或新表达式创建。 但是C语言将数组定义为具有特定成员对象类型的连续分配的非空对象集,称为元素类型。 (n1570 C11草案,6.2.5类型§20)。 因此,如果我们可以确保成员是连续的(意味着它们之间没有填充),那么将其视为数组是合法的。

这是一个简化的示例,它在没有警告的情况下编译,并在运行时给出预期的结果:

#include  #include  #include  struct quad { int x; int y; int z; int t; }; int main() { // ensure members are consecutive (note 1) static_assert(offsetof(struct quad, t) == 3 * sizeof(int), "unexpected padding in quad struct"); struct quad q; int *ix = &q.x; for(int i=0; i<4; i++) { ix[i] = i; } printf("Quad: %d %d %d %d\n", qx, qy, qz, qt); return 0; } 

它在这里没有任何意义,但我已经看到了真实世界的例子,在结构的成员之间进行迭代允许更简单的代码,而错误的风险更低。

题:

在上面的例子中, static_assert足以使结构的别名与数组合法?


(注释1)由于结构描述了顺序分配的非空成员对象集 ,后面的成员必须具有增加的地址。 只需编译器可以在它们之间包含填充。 因此,如果sizeof(int) 3 sizeof(int)之前的总填充,则最后一个成员(此处为t )的偏移量。 如果偏移正好是3 * sizeof(int)则struct中没有填充


提出作为副本的问题包含一个可接受的答案,让我们认为它将是UB,以及+1答案 ,让我们认为它可能是合法的,因为我可以确保不存在填充

我要争论UB。 首先, 6.5.6 Additive运算符的强制引用:

当一个具有整数类型的表达式被添加到指针或从指针中减去时,结果具有指针操作数的类型。 如果指针操作数指向数组对象的元素,并且数组足够大,则结果指向与原始元素偏移的元素,使得结果元素和原始数组元素的下标的差异等于整数表达式。 换句话说,如果表达式P指向数组对象的第i个元素,则表达式(P)+ N(等效地,N +(P))和(P)-N(其中N具有值n)指向分别为数组对象的第i + n个和第n个元素,只要它们存在。 此外,如果表达式P指向数组对象的最后一个元素,则表达式(P)+1指向一个超过数组对象的最后一个元素,如果表达式Q指向一个超过数组对象的最后一个元素,表达式(Q)-1指向数组对象的最后一个元素。 如果指针操作数和结果都指向同一个数组对象的元素,或者指向数组对象的最后一个元素,则评估不应产生溢出; 否则,行为未定义。 如果结果指向数组对象的最后一个元素之后,则不应将其用作已计算的一元*运算符的操作数。

我强调了我认为问题的症结所在。 当你说数组对象是“一个连续分配的非空对象集合,具有特定的成员对象类型,称为元素类型”时,你是对的 。 但反过来又是真的吗? 连续分配的对象集是否构成数组对象?

我要说不。 需要明确创建对象。

因此,对于您的示例,没有数组对象。 通常有两种方法在C中创建对象。使用自动,静态或线程本地持续时间声明它们。 或者分配它们并为存储提供有效的类型。 你既没有创建一个数组。 这使得算术正式未定义。

这里的问题是你对连续分配的定义:“我们可以确保成员是连续的(意味着它们之间没有填充)”。

虽然这是连续分配的必然结果,但它没有定义属性。

您的结构成员是具有自动存储持续时间的单独变量,具有或不具有填充的特定顺序,具体取决于您能够如何控制编译器,这就是全部。 因此,您不能使用指针算法来获取给定另一个成员的地址的一个成员,并且这样做的行为是未定义的。

不,像这样对struct和数组进行别名是不合法的,它违反了严格的别名。 解决方法是将结构包装在一个联合中,该联合包含一个数组和各个成员:

 union something { struct quad { int x; int y; int z; int t; }; int array [4]; }; 

这会避免严格的别名冲突,但您仍可能有填充字节。 您可以使用静态断言检测到哪个。

另一个问题仍然存在,那就是你不能在指向结构的第一个成员的int*上使用指针算法,因为在加法运算符的指定行为中概述了各种晦涩的原因 – 它们要求指针指向数组类型。

避开所有这些的最好方法是简单地使用上面联合的数组成员。 这与静态断言一起产生定义明确,坚固耐用且可移植的代码。

(理论上,您也可以使用指向字符类型的指针来遍历结构 – 与int*不同,这将根据6.3.2.3/7允许。但如果您对单个字节不感兴趣,这是一个更麻烦的解决方案。)

首先 –

引用C11 ,章节§6.5.2.1p2

后缀表达式后跟方括号[]的表达式是数组对象元素的下标名称。 下标运算符[]的定义是E1[E2](*((E1)+(E2)))...

这意味着ix[i]评估为*(ix + i) 。 这里的子表达式是ix + iix具有pointer to integer类型pointer to integer

现在,

引用C11 ,章节§6.5.6p7

出于这些运算符的目的,指向不是数组元素的对象的指针与指向长度为1的数组的第一个元素的指针的行为相同,其中对象的类型为其元素类型。

因此我们知道ix指向一个大小为1的数组。 甚至构造一个超出长度的指针(除了一个除外)是未定义的行为,更不用说取消引用它了。

这导致我解释这确实是不允许的。

这将是UB。 正如在其他问题中所建立的那样 ,static_assert可以以一致的方式测试可能的填充。 所以是的,结构的4个成员确实是连续分配的。

但真正的问题是连续分配是必要的,但不足以构成一个arrays。 即使我在C标准中找不到它的明确参考,对象在其生命周期内也不能重叠 – 这在C ++标准中更明确地说明了。 它们可以是聚合(结构或数组)的成员,但不允许聚合重叠。 这与1992年12月10日Antti Haapala在回答提议的副本时引用的C89的缺陷报告#017的回应是一致的。

即使C没有new语句,分配的存储也具有没有声明类型的特定属性。 这允许在该存储中动态创建对象,但是当在其地址创建不同类型的对象时,已分配对象的生命周期结束。 因此,即使在分配的内存中,我们也不能同时拥有数组和结构。

根据Lundin的回答 ,通过数组和结构之间的并集来打字,应该有效,因为(非规范性)注释说

如果用于读取union对象内容的成员与上次用于在对象中存储值的成员不同,则该值的对象表示的相应部分将被重新解释为新类型中的对象表示forms

并且两种类型将具有相同的表示:4个连续的整数

没有联合,迭代数组成员的方法是在字节级别,因为6.3.2.3转换/指针说:

7 …当指向对象的指针转换为指向字符类型的指针时,结果指向对象的最低寻址字节。 结果的连续递增(直到对象的大小)产生指向对象的剩余字节的指针。

 char *p = q; for (i=0; i<4; i++) { int *ix = (int *) (p + i * sizeof(int)); // Ok: points to the expected int member *ix = i; } 

但是在非字符类型上迭代结构成员的指针算术是UB,因为结构的各个成员不能同时是数组的成员。