最適化なしだと期待通りに動くけど、最適化すると期待と異なる動作する例をいくつか。最適化機能のバグ? と思いきや、実は未定義の挙動となるパターンを踏んでいるだけという。
どれも有名なものだけど、筆者周囲ではハマる人が相変わらず多いので投稿。
実行例はすべて x86-64 な Linux で行った結果です。
符号付き整数をオーバーフローを検出しようと思ったのに
コンパイラ: gcc-4.9.3, clang-3.6.2
最適化オプション: -O2
プログラムの第一引数に INT_MAX
の値を指定して実行してみましょう。
表示される整数は、 n + 1 < n となっているのに、n + 1 > n のルートを通ります。
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
if(argc <= 1)
{
fprintf(stderr, "usage: %s <number>\n", argv[0]);
return 1;
}
int n = strtol(argv[1], NULL, 0);
if(n + 1 > n)
{
printf("%d > %d\n", n + 1, n);
}
else
{
printf("%d < %d [Overflow!]\n", n + 1, n);
}
return 0;
}
実行例:
$ ./a.out 0x7fffffff -2147483648 > 2147483647
符号付き整数の演算がオーバーフローした場合の結果は未定義です。
なお、 n
の型を unsigned
にして、第一引数を UINT_MAX
の値にした場合はオーバーフローが検出されます。(符号なし整数のオーバーフローについては既定があります)
その参照先、変更したハズでは?
コンパイラ: gcc-4.9.3, clang-3.6.2
最適化オプション: -O2
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
void f(uint16_t *x, uint32_t *y)
{
x[0] = 0x1111;
x[1] = 0x2222;
*y = 0x33333333;
printf("x[0] = %"PRIx16", x[1] = %"PRIx16", *y = %"PRIx32"\n",
x[0], x[1], *y);
}
int main(int argc, char *argv[])
{
uint32_t a = 0x55555555;
f((uint16_t *)&a, &a);
return 0;
}
実行例:
$ ./a.out x[0] = 1111, x[1] = 2222, *y = 33333333
x[0]
と x[1]
、上書きされて0x3333になったんじゃないの?
同じ領域を全く異なる型でアクセスするのは未定義です。
正確に説明するとややこしいけど、大雑把に言うとそんな感じ。「Cは高級アセンブラだ!」とか言ってるとこれにハマること多し。
なおchar
、signed char
、unsigned char
でのアクセスは例外的にOK。
ヌルポで落ちないの?
ちょいと毛色を変えて、あからさまにダメなコードだけど、どのようにダメになるかが斜め上になるパターン。
コンパイラ: clang-3.6.2
最適化オプション: -O2
#include <stdio.h>
int n;
static int *p = NULL;
void f(void)
{
p = &n;
}
int main(int argc, char *argv[])
{
n = argc;
printf("p = %p, *p = %d\n", p, *p);
return 0;
}
実行例:
$ ./a.out p = (nil), *p = 1 $ echo $? 0 $ ./a.out a p = (nil), *p = 2 $ echo $? 0
NULL
のくせに n
の内容を表示しつつ正常終了しちゃった。
ヌルポインターに対するアクセス結果はあくまで未定義です。メモリ保護機能があるOSであってもアクセス違反例外が起こるなんて保証はありません。1
こいつらでハマる例はないかな?
割とやらかすけど、規格を読んでみると未定義。でも実際にハマるパターン (処理系、ソース) が思い付かないもの。何か面白い例はないかな?