monoの開発ブログ

C99の配列初期化に潜む罠

C99では配列初期化が拡張されています。一見便利そうなのですが、特大の罠が仕込まれていてかなりの時間を無駄にしたのでとても残念な気持ちです。

ここで話すC99の拡張とは、配列の初期化に要素指示子を利用できるようになったことを指しています。

int a[] = {
  [3] = 1,
  [7] = 2,
};

こんな感じで記述すると、a[3] が 1、a[7] が 2 で初期化されます。

では、以下のように配列を初期化したとき、a[3] の値はどうなるでしょうか。

int a[] = {
  [3] = 1,
  [2] = 2,
  3,
};

1、と思うかもしれませんが、実は 3 です。このコードでポイントになるのは、最後に要素指示を使わずに 3 とだけ記述した部分です。実はC99では要素指示子を使った初期化と従来の初期化を混ぜて使えます。その場合、最後に要素指示子を使って初期化した要素の続きを初期化することになります。従って、上のコードは以下のようなイメージで解釈されます。

int a[] = {
  [3] = 1,
  [2] = 2,
  [3] = 3,
};

a[3] に対する初期化が2回記述されていることになり、この場合は後から書いたものが優先されるため、3 で初期化されるというわけです。

この時点で性質の異なる2つの記法を混ぜると悲劇が起こるというのはなんとなく感じていただけたかと思いますが、ここからは私が某ソフトウェアのコードを読んでいて遭遇したパターンの例を紹介します。

.hではこんな感じの定数が定義してあります。

#define DATA_APPLE 0
#define DATA_MICROSOFT 1
#define DATA_GOOGLE 2
#define DATA_AMAZON 3

.cでは先ほどの定数を使って配列を初期化しています。

extern int apple;
extern int amazon;
extern int microsoft;
extern int google;

int *data[] = {
  [DATA_APPLE]     = &apple,
  [DATA_AMAZON]    = &amazon,
  [DATA_MICROSOFT] = &microsoft,
  [DATA_GOOGLE]    = &google,
  0,
};

一見問題なさそうに見えるのが本当に厄介。知らない状態でなんとなく見ると「末尾にダミーの 0 を入れておいてループの終了条件に使うのかな?」などという先入観にとらわれてしまい、問題に気付くことができません。というか丸一日くらいできませんでした。

もうおわかりだと思いますが、上のコードの配列初期化部分は以下のような感じで解釈されます。

int *data[] = {
  [0] = &apple,
  [3] = &amazon,
  [1] = &microsoft,
  [2] = &google,
  [3] = 0,
};

data[3]&amazon ではなく 0 になってしまっていますし、 data[4] はなくなってしまいました。#defineでの並びと初期化の並びがしれっと入れ替わっているのが原因です。

この例のように問題の部分だけを見ればなんとかわかるとしても、実際のコードに紛れ込んでいたらなかなか気付けないと思いますし、分かっていてもミスしてはまる可能性が十分にあります。この記事を読んだ皆さんにはこのようなコードを書いて欲しくないですし、もし読んだりメンテナンスする羽目になったときには十分に注意していただきたいと思います。

余談ですが、実際に読んでいたコードではこのバグに加えて amazon 相当の変数の定義がどこにも存在しないという謎の状態になっていたので、配列に要素を追加しようとするとリンクエラーになったりならなかったりしてつらかったです。

#define DATA_APPLE 0
#define DATA_MICROSOFT 1
#define DATA_GOOGLE 2
#define DATA_AMAZON 3
#define DATA_FACEBOOK 4

int *data[] = {
  [DATA_APPLE]     = &apple,
  [DATA_AMAZON]    = &amazon,
  [DATA_MICROSOFT] = &microsoft,
  [DATA_GOOGLE]    = &google,
  [DATA_FACEBOOK]  = &facebook,  /* ここに追加すると */
  0,                             /* [5] = 0 になるのでamazonへの参照が復活 => 存在しないのでリンクエラー */
};

int *data[] = {
  [DATA_APPLE]     = &apple,
  [DATA_AMAZON]    = &amazon,
  [DATA_MICROSOFT] = &microsoft,
  [DATA_FACEBOOK]  = &facebook,  /* ここに追加すると */
  [DATA_GOOGLE]    = &google,
  0,                             /* [3] = 0 になるのでリンクエラーにはならないけど実行時にバグる */
};