31

C++11 中是未定义的行为,但在 C 中while(1);是未定义的行为吗?

4

4 回答 4

32

这是定义明确的行为。在 C11 中添加了新的第 6.8.5 条广告 6

控制表达式不是常量表达式的迭代语句,156)不执行输入/输出操作,不访问易失性对象,并且在其主体、控制表达式或(在 for 的情况下)不执行同步或原子操作statement) 其表达式 3,可被实现假定为终止。157)


157)这旨在允许编译器转换,例如即使在无法证明终止时也可以删除空循环。

由于循环的控制表达式是一个常量,编译器可能不会假定循环终止。这适用于应该永远运行的反应程序,例如操作系统。

但是对于以下循环,行为尚不清楚

a = 1; while(a);

实际上,编译器可能会或可能不会删除此循环,从而导致程序可能终止或可能不终止。这并不是真正的未定义,因为它不允许擦除您的硬盘,但这是一种避免的结构。

然而,还有另一个障碍,请考虑以下代码:

a = 1; while(a) while(1);

现在,由于编译器可能假设外循环终止,内循环也应该终止,否则外循环如何终止。所以如果你有一个非常聪明的编译器,那么一个while(1);不应该终止的循环必须有这样的非终止循环,直到main. 如果你真的想要无限循环,你最好在其中读取或写入一些volatile变量。

为什么这个条款不实用

我们的编译器公司不太可能使用这个子句,主要是因为它是一个非常符合语法的属性。在中间表示(IR)中,上述示例中的常量和变量之间的差异很容易通过常量传播而丢失。

该子句的目的是允许编译器编写者应用所需的转换,如下所示。考虑一个不常见的循环:

int f(unsigned int n, int *a)
{       unsigned int i;
        int s;
        
        s = 0;
        for (i = 10U; i <= n; i++)
        {
                s += a[i];
        }
        return s;
}

出于架构原因(例如硬件循环),我们希望将此代码转换为:

int f(unsigned int n, int *a)
{       unsigned int i;
        int s;
        
        s = 0;
        for (i = 0; i < n-9; i++)
        {
                s += a[i+10];
        }
        return s;
}

如果没有第 6.8.5 ad 6 条,这是不可能的,因为如果nequals UINT_MAX,循环可能不会终止。然而,人类很清楚这不是该代码编写者的意图。条款 6.8.5 ad 6 现在允许这种转换。然而,实现这一点的方式对于编译器编写者来说不是很实用,因为无限循环的语法要求很难在 IR 上维护。

请注意,n和as overflow on给出未定义的行为i是必不可少的,因此可以证明转换是合理的。然而,除了更大的正范围之外,高效的代码也受益于使用。unsignedsigned intunsigned

另一种方法

我们的方法是代码编写者必须通过例如assert(n < UINT_MAX)在循环之前插入一个类似 Frama-C 的保证来表达他的意图。这样,编译器可以“证明”终止,而不必依赖第 6.8.5 和第 6 条。

PS:我正在查看 2011 年 4 月 12 日的草稿,因为 paxdiablo 显然正在查看不同的版本,也许他的版本更新。在他的引文中没有提到常量表达式的元素。

于 2013-05-08T08:57:11.197 回答
5

在检查C99 标准草案后,我会说“不”,它不是未定义的。我在草稿中找不到任何提及迭代结束要求的语言。

描述迭代语句语义的段落的全文是:

迭代语句会导致重复执行称为循环体的语句,直到控制表达式比较等于 0。

如果适用,我希望出现任何限制,例如为 C++11 指定的限制。还有一个名为“约束”的部分,它也没有提到任何这样的约束。

当然,实际标准可能会说别的,尽管我对此表示怀疑。

于 2013-05-08T08:51:07.337 回答
1

以下语句出现在C11 6.8.5 Iteration statements /6

一个迭代语句,其控制表达式不是常量表达式,不执行输入/输出操作,不访问 volatile 对象,并且在其主体、控制表达式或(在 for 语句的情况下)不执行同步或原子操作它的表达式 3 可以被实现假定为终止。

由于while(1); 使用了一个常量表达式,因此不允许该实现假定它将终止。

如果表达式是非常量并且所有其他条件都同样满足,编译器可以完全删除这样的循环,即使不能最终证明循环会终止。

于 2013-05-08T08:57:32.127 回答
1

最简单的答案涉及 §5.1.2.3p6 中的引用,其中说明了符合实现的最低要求:

对一致性实现的最低要求是:

— 对 volatile 对象的访问严格按照抽象机的规则进行评估。

— 在程序终止时,写入文件的所有数据应与根据抽象语义执行程序所产生的结果相同。

— 交互设备的输入和输出动态应按照 7.21.3 的规定进行。这些要求的目的是尽快出现无缓冲或行缓冲的输出,以确保在程序等待输入之前实际出现提示消息。

这是程序的可观察行为。

如果机器代码由于执行优化而无法产生可观察的行为,则编译器不是 C 编译器。在终止点只包含这样一个无限循环的程序的可观察行为是什么?这样一个循环可以结束的唯一方法是通过一个信号导致它过早结束。在 的情况下SIGTERM,程序终止。这将导致没有可观察到的行为。因此,该程序的唯一有效优化是编译器抢占系统关闭程序并生成立即结束的程序。

/* unoptimised version */
int main() {
    for (;;);
    puts("The loop has ended");
}

/* optimised version */
int main() { }

一种可能性是发出信号并调用 longjmp 以使执行跳转到不同的位置。似乎唯一可以跳转到的地方是在循环之前的执行期间到达的某个地方,因此如果编译器足够智能,可以注意到引发了一个信号导致执行跳转到其他地方,它可能会优化循环(和信号升高)远离,有利于立即跳跃。

当多个线程进入等式时,有效的实现可能能够将程序的所有权从主线程转移到不同的线程,并结束主线程。无论优化如何,程序的可观察行为必须仍然是可观察的。

于 2013-05-08T10:32:44.487 回答