【问题标题】:Is it legal to alias a struct and an array?给结构和数组取别名是否合法?
【发布时间】:2018-01-08 09:29:17
【问题描述】:

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

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

#include <stdio.h>
#include <stddef.h>
#include <assert.h>

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", q.x, q.y, q.z, q.t);
    return 0;
}

这里没有真正的意义,但我已经看到了现实世界的例子,其中在结构成员之间进行迭代允许更简单的代码,减少拼写错误的风险。

问题:

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


(注 1)由于结构 描述了一个顺序分配的非空成员对象集,后面的成员必须具有递增的地址。简单地说,编译器可以在它们之间包含填充。所以最后一个成员的偏移量(这里是t)如果3倍sizeof(int)加上它之前的总填充。如果偏移量正好是3 * sizeof(int),那么结构中没有填充


作为重复提出的问题既包含一个可以接受的答案,让我们认为它是 UB,也包含一个 +1 answer,让我们认为它可能是合法的,因为我可以确保不存在任何填充

【问题讨论】:

  • 不,这是未定义的行为。 int x; 是一个大小为 1 的数组。将联合与数组和你的结构一起使用......也许。
  • 几天前我知道了这个并且它是UB(如果我没记错的话)。
  • @P.P:副本确实解决了类似的问题,但在这里我可以证明(在编译时)该结构不包含填充。副本中一个赞成的答案(仅+1)表示填充是问题所在。所以我接受要关闭的问题,但我想知道它是否是UB 当结构不包含填充时
  • 只有一个+1的答案,说在struct有only一个成员并且没有填充的问题的情况下,这不会是UB.. .这不是你的情况,也不是你的代码的相同上下文,关键是编译器允许假设你永远不会使用&amp;q.x的一个元素,因为它是“一个大小为一的数组”,所以在你的您触发的代码越界。不应该是 UB 的联合示例,rextester.com/OLCE62527
  • @SergeBallesta 问题不是,但问题的答案会回答你。这是同样的问题。并且对您的问题的回答将引用该标准的同一句话。

标签: c arrays struct language-lawyer


【解决方案1】:

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

union something {
  struct quad {
    int x;
    int y;
    int z;
    int t;
  };

  int array [4];
};

这避免了严格的别名违规,但您可能仍然有填充字节。您可以使用静态断言检测到。

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

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

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

【讨论】:

    【解决方案2】:

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

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

    您的结构成员是具有自动存储持续时间的单独变量,按照特定顺序使用或不使用填充,具体取决于您控制编译器的方式,仅此而已。因此,在给定另一个成员的地址的情况下,您不能使用指针算术来访问一个成员,并且这样做的行为是未定义的。

    【讨论】:

    • 我试图在我的编辑中解决 有或没有填充 部分:如果有填充,断言应该会引发错误。我认为您的句子that is a corollary ...[but] doesn't define the property 更接近我所需要的,但我的英语还不够好,无法让我完全理解您的意思.
    【解决方案3】:

    我要争论 UB。首先,来自6.5.6 加法运算符的强制引用:

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

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

    我会说不。需要显式创建对象。

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

    【讨论】:

    • 但是数组只定义在6.2.5.20。获取指向动态存储的指针并将其用作指向数组的指针以在该数组中创建对象是完全合法的。这就是我的观点:C 没有 new 指令来创建动态数组
    • @SergeBallesta - 如果你一直在玩分配的对象,这个问题会更难回答,IMO。但是您声明了一个很明显不是数组的自动对象。因此,我会说这个问题。
    • @StoryTeller 我认为他确实创建了一个数组(一个大小为 1 的数组,即参见我的回答中的第二点)。这里的问题是,这个数组太小了。
    • 另外,请问如何分配内存给一个有效的类型?分配器只采用大小而不是任何类型。假设我分配了void* ptr = malloc(sizeof(struct quad));,我可以将它用作int[4](当然假设断言成立)?
    • @StoryTeller:我接受了你的回答,因为你对分配对象的评论帮助我理解了真正的问题。
    【解决方案4】:

    开始 -

    引用C11,章节§6.5.2.1p2

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

    这意味着ix[i] 的计算结果为*(ix + i)。这里的子表达式是ix + iix 的类型为 pointer to integer

    现在,

    引用C11,章节§6.5.6p7

    就这些运算符而言,指向不是数组元素的对象的指针与指向长度为 1 且对象类型作为其元素类型的数组的第一个元素的指针的行为相同。

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

    这使我认为这确实是不允许的。

    【讨论】:

      【解决方案5】:

      应该是 UB。正如在that other question 中建立的那样,static_assert 可以以一致的方式测试可能的填充。所以是的,结构的 4 个成员确实是连续分配的。

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

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

      根据Lundin's answer,通过数组和结构之间的联合进行类型双关应该可以工作,因为(非规范)注释说

      如果用于读取联合对象内容的成员与上次用于读取的成员不同 在对象中存储一个值,该值的对象表示的适当部分被重新解释 作为新类型中的对象表示

      两种类型都有相同的表示: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,因为结构的各个成员不能同时是数组的成员。

      【讨论】:

      • 非常感谢其他回答者给了我提示。
      • C 标准明确指出,访问联合的成员而不是最后存储的成员会将存储重新解释为新类型。因此,即使在某种形式上没有新类型的“对象”,字节也会被解释为存在。这是一个非规范的脚注,但它有长期历史和支持材料的支持。规范文本说使用成员的 .-> 表达式的值是成员的值——没有限定要求它是最后存储的成员。
      • “甚至结构和数组之间的联合也不允许为结构和数组起别名,因为联合的一个成员可以同时处于活动状态。”这不是真的,那是 C++。 C 语言允许在不同的联合类型之间进行“类型双关”,如 6.5.2.3/3 加(非规范性)脚注 95 中所述。这就是为什么我提出的联合解决方案可行的原因。
      • @Lundin:帖子已编辑,感谢您的反馈。我已经对你的回答投了赞成票,你的评论帮助我更好地理解了它背后的理由。
      猜你喜欢
      • 2020-09-26
      • 2019-03-22
      • 1970-01-01
      • 2018-07-01
      • 1970-01-01
      • 2016-07-23
      • 1970-01-01
      • 2016-02-16
      • 1970-01-01
      相关资源
      最近更新 更多