我想遍历signed int
的所有可能值,从INT_MIN
到INT_MAX
。显而易见的代码
for (int x = INT_MIN; x <= INT_MAX; x++)
do_something(x);
由于有符号整数溢出导致的未定义行为而中断。值得注意的是,它显然很聪明的变体也是如此
int x = INT_MIN;
do {
do_something(x);
} while (x++ < INT_MAX);
有趣的是,在第二个示例中,clang -O2 -fsanitize=undefined
正确地报告了runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
,而gcc -O2 -fsanitize=undefined
则没有。我想gcc
的-fsanitize=signed-integer-overflow
和-ftrapv
一样破碎。
我能找到表达此迭代的唯一方法是使用goto
:
int x = INT_MIN;
loop: {
do_something(x);
if (x < INT_MAX) {
x++;
goto loop;
}
}
使用无限循环并手动break
:
int x = INT_MIN;
while (1) {
do_something(x);
if (x < INT_MAX)
x++;
else
break;
}
并利用短路评估仅在安全的情况下增加变量:
int x = INT_MIN;
do {
do_something(x);
} while (x < INT_MAX && ++x != INT_MIN);
正如史蒂夫·杰索普(Steve Jessop(所指出的那样,在do-while
解决方案中,人们可以使用while (x < INT_MAX && (++x, 1));
。
在三个版本中,我并不是特别反对goto
版本,我不太喜欢do-while
版本(尤其是++x != INT_MIN
位......(,但总而言之,我更喜欢while (1)
版本。我的问题如下。
C编译器是否允许完全消除无限while (1)
循环,以防do_something
不执行输入/输出操作,也不访问易失性对象?
我知道它不能用于 C11,但我不太确定以前的版本。
编辑
我将尝试更详细地描述我所担心的事情。假设我正在使用此循环来验证其他一些代码的正确性。例如,我可能会做
#include <stdio.h>
#include <limits.h>
int add_wrap_unsigned(int x, int y) {
return (unsigned) x + (unsigned) y;
}
int add_wrap_builtin(int x, int y) {
int res;
__builtin_add_overflow(x, y, &res);
return res;
}
int check() {
int x = INT_MIN;
while (1) {
if (add_wrap_unsigned(x, 1) != add_wrap_builtin(x, 1))
return 1;
if (x < INT_MAX)
x++;
else
break;
}
return 0;
}
int main() {
if (check())
printf("counterexample found");
else
printf("no counterexample");
return 0;
}
这正确地报告了带有叮当声的no counterexample
。check
被编译为一个简单的return 0
。但是,更改单行(第 22 行从break;
到x = INT_MIN;
(会完全改变行为:check
变成了一个 noop 无限循环,但main
现在打印counterexample found
。即使有-std=c11
,所有这一切都会发生.
我担心的是我不能信任第一个版本,因为编译器可能没有做正确的事情,就像第二种情况一样。
问题是是否允许 C 编译器删除无限循环。不,不允许删除控制表达式为常量(for (;;) {}
或while (1) { }
(的无限循环,但 C 标准在 C11/C17 6.8.5p6 中明确指出
一个迭代语句,其控制表达式不是常量表达式,156(不执行
- 输入/输出操作,不访问易失性对象,并且在其主体中不执行同步或原子操作,控制表达式,或者(在 for 语句的情况下(其表达式-3,可以由实现假定终止.157(
即,允许编译器优化一个没有任何副作用并且具有非常量控制表达式的循环。 也就是说,当涉及到假设规则时,它是一个完全无用的循环。该标准的C11修订版已授予此例外。这在 C11 之前不适用,因此如果存在该行为,那么它就不符合标准。
您不需要保护增量,只需break
:
for (int x = INT_MIN; ; x++) {
do_something(x);
if (x == INT_MAX) break;
}
在这种情况下,控制表达式是一个常量表达式(隐式 true 值(,因此不得按照 C11/C17 6.8.5p6 进行优化,但编译器必须显式证明它才能终止。
现在优化的内容取决于编译器和标志 --O3
Clang 9.0 将其编译为等效于
for (int x = INT_MIN; x != INT_MIN; x++)
do_something(x)
如果溢出是用环绕定义的 - 但正如我们所知,x86 机器代码确实有明确定义的环绕。
GCC 9.2 生成的机器代码的行为类似于
int x = INT_MIN;
do_something(x);
do {
do_something(++x);
} while (x < INT_MAX);
在 C 中这样写,将没有未定义的行为,并且在性能上与 Clang 的性能几乎没有区别(但不是在大小上(。