C言語の#ifに潜む罠-未定義マクロの参照-
さっそくですが問題です。
以下のCコードをビルドして実行すると、どうなるでしょう?
#include <stdio.h> #define ON (0U) #define OFF (1U) #define PRINT_HELLO_WORLD (OFF) int main(void) { #if ( PRlNT_HELLO_WORLD == ON ) printf("Hello World!"); #endif return 0; }
- 「Hello World!」と表示される
- 何も表示されない
- ビルドエラーとなる
- 不定動作となる
正解は
・
・
・
1. 「Hello World!」と表示されるです。※3のこともある(後述)
どうでしょう?正解しましたか?
このコードについて、以下で解説していきます。
「Hello World!」と表示される理由
お気づきの方もいるでしょうが、先のコードの #if ( PRlNT_HELLO_WORLD == ON )
この部分、「PRINT」の 大文字のI が 小文字のL になっています。
つまり、#if で未定義のマクロを参照しています。
「じゃあ、ビルドエラーでは?」と思うかもしれませんが、意外なことにほとんどの場合ビルドは通ります。警告すら出ないことが多いです。
実はC言語の仕様(※)上、#if で未定義マクロを参照した場合、"0" として評価することになっているのです。
そのため、マクロ「ON」と一致し、printfの部分が有効になるのです。
これ、結構バグの原因となるので注意が必要です。
※「プログラミング言語C 第2版」の「A12.5 条件付きコンパイル」に以下の記述があります。
#ifおよび#elifにおける定数式は普通のマクロ置換を受けるものである。
<中略>
またマクロ展開後に残る識別子は 0L に置き換えられる。
未定義マクロを参照してしまうありがちなケース
まず一つは、マクロ名の誤りです。先ほどのコードのようなやつですね。
まあこれは気を付ければそんなに起きないかなーとおもいます。
もう一つは、インクルード漏れです。これは結構注意が必要です。
特にインクルード関係が複雑になってくると、"ビルドエラーが出たらインクルードを追加する"という作業をやりがちです。
このようなとき、運が悪いとこの事象にハマることになります。
未定義マクロを参照しないようにするには
一番いい解決方法は、未定義マクロの参照を検出するコンパイルオプションを付けることです。
最近のコンパイラであれば、大体備わっているんじゃないかと思います。
例えばgccであれば、-Wundef
を付けることで以下のように警告を出すことができます。
なお、-Wall
では検出されない(何故!)ので気を付けてください。
冒頭でビルドエラーとなることもあると言ったのは、このようなオプションを指定している場合には検出される可能性があるためでした。
また、他の解決方法として 静的解析ツール を使用する方法も考えられます。
静的解析ツールであれば、未定義マクロの参照は警告を出してくれるはずです。
ただし、静的解析ツールでは注意が必要なことがあります。
それは、アセンブラファイルで #if を使用する場合です。
おそらく多くの静的解析ツールはCソースファイルが解析対象ですが、コンパイラによってはアセンブラファイルにプリプロセスをかけられることがあります。
このようなとき、静的解析ツールでは検出されないことがあります。
まとめ
- #if で未定義マクロを参照すると、0として解釈される
- 未定義マクロの参照は、コンパイルオプションを使用して検出しよう