【问题标题】:C standard addressing simplification inconsistencyC标准寻址简化不一致
【发布时间】:2011-02-05 06:34:51
【问题描述】:

第 6.5.3.2 节“地址和间接运算符”¶3 说(仅相关部分):

一元 & 运算符返回其操作数的地址。 ... 如果操作数是一元 * 运算符的结果,则该运算符和 & 运算符都不会被计算,结果就好像两者都被省略了,除了对运算符的约束仍然适用并且结果不是左值。类似地,如果操作数是 [] 运算符的结果,则不会计算 & 运算符和 [] 隐含的一元 *,结果就像删除了 & 运算符一样并且 [] 运算符更改为 + 运算符。 ...

这意味着:

#define NUM 10
int tmp[NUM];
int *i = tmp;
printf("%ti\n", (ptrdiff_t) (&*i - i) );
printf("%ti\n", (ptrdiff_t) (&i[NUM] - i) );

应该完全合法,打印 0 和 NUM (10)。标准似乎很明确,这两种情况都需要优化。

但是,它似乎不需要优化以下内容:

struct { int a; short b; } tmp, *s = tmp;
printf("%ti\n", (ptrdiff_t) (&s->b - s) );

这似乎非常不一致。我看不出上面的代码没有理由不打印sizeof(int) 加上(不太可能)填充(可能是4)。

简化&-> 表达式在概念上(恕我直言)与&[] 相同,即简单的地址加偏移量。它甚至是一个可以在编译时确定的偏移量,而不是可能在运行时使用 [] 运算符确定。

为什么这看起来如此不一致,有什么理由吗?

【问题讨论】:

  • 我见过大量难的 C 和 C++ 标准问题,除了点赞、收藏和等待阅读答案,我不知道该怎么做。真的写了一个感觉很奇怪。
  • 有趣 ... MSVC++ 打印 4!
  • @Abhi Rao - 带有 -Wall -Wextra -Werror 的 GCC (4.0) 编译并打印 4 没有任何抱怨。
  • 现在我们已经消除了格式错误的空指针示例,我真的没有看到一个令人信服的用例来扩展此功能以涵盖&->。显然,编译器可以(并且可能会)优化实际的取消引用并获取地址,但我只是没有看到一个令人信服的例子,其中&-> 的某些使用在当前语言规则下是未定义的但在给予&->&* 相同待遇的新规则下会得到很好的定义。
  • @James McNellis - 您可以使用非空指针创建 offsetof hack 词。据推测,如果标准要求对其进行优化,那么格式错误的背部更有可能起作用。

标签: c standards memory-address simplification


【解决方案1】:

在您的示例中,&i[10] 实际上是不合法的:它变成了i + 10,然后变成了NULL + 10,并且您不能对空指针执行算术运算。 (6.5.6/8列出了可以进行指针运算的条件)

反正这条规则是在C99中加入的;它在 C89 中不存在。我的理解是,它在很大程度上是为了使代码像下面这样定义明确:

int* begin, * end;
int v[10];

begin = &v[0];
end = &v[10];

最后一行在 C89(和 C++)中在技术上是无效的,但由于这条规则,在 C99 中是允许的。这是一个相对较小的更改,使常用的构造得到良好定义。

因为您不能对空指针执行算术运算,所以您的示例 (&s->b) 无论如何都是无效的。

至于为什么会出现这种“不一致”,我只能猜测。很可能没有人想过让它保持一致,或者没有人看到一个令人信服的用例。这有可能被考虑并最终被拒绝。在the Rationale 中没有关于&* 减少的评论。您或许可以在 the WG14 papers 中找到一些确定的信息,但不幸的是,它们似乎组织得非常糟糕,因此浏览它们可能很乏味。

【讨论】:

  • 我从示例中删除了空指针,因为它们从来都不是我真正关心的问题。
  • 我看不出NULL 是如何在这里发挥作用的。此外,对于指针算术(只要您不评估不存在的对象),可以使用数组之后的元素。 AFAIR,这在标准的几个地方都提到了。
  • @Jens:问题中的原始示例使用了NULL 指针,并且不能对空指针执行明确定义的算术。您可以获得指向过去的“元素”的指针,但不能取消引用它。对于int v[10];,只有在C99中使用&v[10]&*(v + 10)是合法的;在 C++ 和 C90 中,此类代码形式上会产生未定义的行为。
  • 我接受这一点是因为“没有人认为这很重要”可能是我在不寻找理由的情况下得到的最佳答案。如果我确实想搜索理由,这个答案有一个很好的链接。
【解决方案2】:

我认为该规则不是为了优化目的而添加的(它带来了什么而不是 as-if 规则?)但是允许 &t[sizeof(t)/sizeof(*t)]&*(t+sizeof(t)/sizeof(*t)) 如果没有它,这将是未定义的行为(直接写这样的东西可能看起来很傻,但是添加一层或两层宏就会有意义)。我没有看到特殊套管 &p->m 会带来这种好处的情况。请注意,正如詹姆斯指出的那样,&p[10] 带有 p 一个空指针仍然是未定义的行为; &p->m 带有 p 的空指针同样会保持无效(我必须承认,当 p 是空指针时我看不到任何用处)。

【讨论】:

  • p = NULL 的明显(恕我直言)使用是 offsetof 宏的 hacky 实现,它依赖于 &((struct t *)0)->m 工作。但是,它可以很容易地更改为1(或依赖于编译器的有效指针值,比如堆栈)而不是0,虽然它可能不太可能给你带来好的struct值它应该给你正确的偏移值。
  • @Chris:很久以前,我有一个早期的标准 C 编译器,它根据地址 0 定义 offsetof(),然后给出核心转储或编译错误(我现在忘记了)被使用了。我最终破解了系统标头并使用 1024 作为地址而不是 0;效果很好。它 (1024) 充分对齐不会产生问题 - 不像 1.
  • 除了字符数组,&t[sizeof(t)] 远远超出了分配对象的末尾。
  • @Jonathan - 哦,对,对齐存在。我现在觉得很傻。
  • @JOnathan,谢谢,已在帖子中修复。
【解决方案3】:

我相信编译器可以选择以不同的方式打包,可能会在结构成员之间添加填充以提高内存访问速度。这意味着您不能肯定地说b总是 是 4 的偏移量。单值不存在同样的问题。

此外,编译器在优化阶段可能不知道结构在内存中的布局,从而阻止了任何与结构成员访问和后续指针转换有关的优化。


编辑:

我有另一个理论......

很多时候编译器会在词法分析和解析之后优化抽象语法树。这意味着它将找到诸如抵消的运算符和评估为常数并将树的这些部分减少为一个节点的表达式之类的东西。这也意味着关于结构的信息不可用。在某些代码生成之后发生的后续优化过程可能会考虑到这一点,因为它们有额外的信息,但对于诸如修剪 AST 之类的事情,这些信息尚不存在。

【讨论】:

  • 你不能确定它总是偏移量 4,但是为了使 struct 有用,你可以确定它是一个恒定的偏移量。我使用了int,后跟short,所以我怀疑是否有编译器需要在它们之间添加填充。
  • “另外,在优化阶段,编译器可能不知道内存中结构的布局......”这似乎是优化器需要掌握的一些非常重要的信息。
  • 我认为这也取决于编译器的编写方式。该标准规定了它应该如何正常工作的规则,但设置优化标志可能确实如你所说。我的猜测是标准的编写者不想强加太多优化。
  • 看来&s->b 的优化不如&i[x]&s->b(typeof_b *)((char *)s)[offsetof(typeof_s, b)] 相同,并且由于 offsetof 是一个整数常量表达式(17.7 ¶3),这似乎更有可能是可优化的。
  • 我同意 Scott 的回答,即单个值没有相同的问题,但原因不同。从概念上讲,您是对的,但是您正在考虑不人道的术语,这些术语并不总是映射到编译器功能。从语义上讲,您正在尝试解决什么都没有的偏移量。
猜你喜欢
  • 2019-08-02
  • 2014-07-15
  • 1970-01-01
  • 1970-01-01
  • 2011-06-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多