【问题标题】:Defined behaviour for expressions表达式的定义行为
【发布时间】:2012-02-21 18:52:55
【问题描述】:

C99 标准的价格为 6.5.2 美元。

在上一个和下一个序列点之间,一个对象应该有它的存储值 通过表达式的评估最多修改一次。此外,先验值 应为只读以确定要存储的值

(我强调)

接着要注意,以下示例是有效的(乍一看似乎很明显)

a[i] = i;

虽然它没有明确说明 ai 是什么。

虽然我相信不会,但我想知道这个例子是否涵盖以下情况:

int i = 0, *a = &i;
a[i] = i;

不会更改i 的值,而是访问i 的值以确定放置值的地址。或者我们为i 分配一个已经存储在i 中的值是否无关紧要?请提供一些启示。


奖金问题; a[i]++a[i] = 1 呢?

【问题讨论】:

  • 当然,你也可以很愚蠢——而 C 可以让你这样做。但为什么呢?
  • 保持代码简单。简单。下一个人可能需要阅读这些内容。而那个人可能就是你。好吧,去上吊自己,否则麻烦另一个可怜的人。你的选择

标签: c undefined-behavior standards-compliance language-lawyer


【解决方案1】:

第一句话:

在前一个序列点和下一个序列点之间,一个对象应该有它的 表达式的求值最多修改一次存储的值。

够清楚了。该语言不会对子表达式施加求值顺序,除非它们之间存在序列点,并且它不需要 some 未指定的求值顺序,它表示修改对象两次会产生未定义的行为。这允许积极优化,同时仍然可以编写遵循规则的代码。

下一句:

此外,先读值应只读以确定要存储的值

乍一看(和第二眼)确实不直观;为什么读取值的目的会影响表达式是否具有已定义的行为?

但它反映的是,如果子表达式 B 依赖于子表达式 A 的结果,则必须先计算 A,然后才能计算 B。 C90 和 C99 标准没有明确说明这一点。

在脚注的一个例子中给出的一个更明显的违反该句子的例子是:

a[i++] = i; /* undefined behavior */

假设a是一个声明的数组对象,i是一个声明的整数对象(没有指针或宏诡计),没有对象被修改超过一次,所以它不违反第一句。但是 LHS 上i++ 的评估决定了要修改哪个对象,RHS 上i 的评估决定了要存储在该对象中的值——以及 RHS 上读取操作的相对顺序并且没有定义 LHS 上的写操作。同样,该语言可能要求以某种未指定的顺序对子表达式进行求值,但相反,它使整个行为未定义,以允许更积极的优化。

在你的例子中:

int i = 0, *a = &i;
a[i] = i; /* undefined behavior (I think) */

读取i 的前一个值以确定要存储的值以确定它将被存储在哪个对象中。因为a[i] 指的是i(但是只是因为i==0),修改i的值会改变左值a[i]所指的对象。在这种情况下,i 中存储的值与已存储在那里的值 (0) 相同,但标准不会对碰巧存储相同值的存储进行例外处理。我相信这种行为是不确定的。 (当然,标准中的示例并不打算涵盖这种情况;它隐含地假设 a 是一个声明的数组对象,与 i 无关。)

至于标准所说的允许的例子:

int a[10], i = 0; /* implicit, not stated in standard */
a[i] = i;

一个可以解释标准说它是未定义的。但我认为第二句话,指的是“先验值”,仅适用于被表达式修改的对象的值。 i 永远不会被表达式修改,因此不存在冲突。 i 的值既用于确定要通过赋值修改的对象,也用于确定要存储在那里的值,但这没关系,因为i 的值本身永远不会改变。 i 的值不是“先验值”,它只是值。

C11 标准为这种表达式评估提供了一个新模型——或者更确切地说,它用不同的词来表达相同的模型。它不是“序列点”,而是谈论在彼此之前或之后排序的副作用,或者相对于彼此不排序的副作用。它明确表示如果子表达式 B 依赖于子表达式 A 的结果,则必须在计算 B 之前计算 A。

the N1570 draft,第 6.5 节说:

1 表达式是一系列运算符和操作数 指定一个值的计算,或指定一个对象 或功能,或产生副作用,或执行 它们的组合。操作数的值计算 运算符的值在计算值之前排序 运算符的结果。

2 如果标量对象的副作用相对于 对同一标量对象的不同副作用或 使用相同标量对象的值计算值, 行为未定义。如果有多个允许的排序 在表达式的子表达式中,行为未定义 如果在任何排序中出现这种未排序的副作用。

3 运算符和操作数的分组由语法指示。 除非稍后指定,副作用和价值计算 的子表达式是未排序的。

【讨论】:

  • @hvd:评估子表达式i++ 的结果(连同评估a 的结果)确定通过赋值修改哪个对象。该结果恰好是 i 的先前值。
  • 行为的不确定性可以追溯到现代优化思想之前。我怀疑它未定义的主要原因是(1)在许多情况下,意外混叠的“自然平台后果”可能会导致任意乱码值,尤其是在“就地”对多字值进行操作时,几乎没有察觉有利于将乱码值与“任何事情都可能发生”区分开来; (2) 捕获此类用法的功能可能很有用,但由于 C 标准没有说明陷阱可以做什么和不能做什么,因此允许陷阱意味着允许未定义的行为。
【解决方案2】:

读取对象的值来确定在哪里存储它不计入“确定要存储的值”。这意味着唯一的争论点可能是我们是否正在“修改”对象i:如果是,它是未定义的;如果我们不是,那没关系。

将值0 存储到已经包含值0 的对象中是否算作“修改存储的值”?根据“修改”的简单英语定义,我不得不说不;保持不变与修改它相反。

然而,很明显这是未定义的行为:

int i = 0, *a = &i;
a[i] = 1;

这里毫无疑问,读取存储值的目的不是确定要存储的值(要存储的值是常数),并且修改了i的值。

【讨论】:

    猜你喜欢
    • 2019-12-02
    • 2018-09-29
    • 1970-01-01
    • 2022-08-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多