Edited at

最適化バグですか? いいえ未定義の挙動です。

More than 3 years have passed since last update.

最適化なしだと期待通りに動くけど、最適化すると期待と異なる動作する例をいくつか。最適化機能のバグ? と思いきや、実は未定義の挙動となるパターンを踏んでいるだけという。

どれも有名なものだけど、筆者周囲ではハマる人が相変わらず多いので投稿。

実行例はすべて 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は高級アセンブラだ!」とか言ってるとこれにハマること多し。

なおcharsigned charunsigned 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


こいつらでハマる例はないかな?

割とやらかすけど、規格を読んでみると未定義。でも実際にハマるパターン (処理系、ソース) が思い付かないもの。何か面白い例はないかな?



  • void * と関数ポインタの相互変換。2

  • ひとつの集成体に含まれないふたつのオブジェクトのアドレスの大小比較 3

  • 配列の範囲 (ひとつ後ろを含む) を超えるポインタ加減算。





  1. もうちょっと正確に言うと、ヌルポインターに対するアクセスが発生するようなコードをコンパイル・実行した結果が未定義なのであって、本当にヌルポインターに対するアクセスが発生したらしっかりアクセス違反例外が起こるのでその点はご安心を。 



  2. POSIX では、 void * → 関数ポインタの変換が特例で認められています。 



  3. C では未定義ですが、 C++ では未既定です。