#defineの罠

#defineとは

#defineはCプリプロセッサのひとつで、 コンパイル前に記号定数がすべて定義したものに置き換わります。

これは例を見てもらったほうが分かりやすいでしょう。

 #define HOGE 3

 int main(void) {
     return HOGE;
 }

上のようにするとシステムへの戻り値として3が返ります。

Cプリプロセッサ

Cプリプロセッサ(以下CPP)はC++言語ではなく、 文法も独自のものです。 しかも#includeと多重インクルード防止法以外にはC++の文法で代替手段が存在するので、 CPPは使用すべきではありません。

CPPはC++コード上の異物以外の何者でもなく、 コードを複雑怪奇に見せるだけです(慣れればそうでもないが、C++の文法で実現できるのでわざわざなれる必要が無い)。

#defineの欠点

では#defineの欠点を見ていきましょう。

予想外の動作

まず、#defineはコンパイル前に展開されてしまい、 その動作はCPPを知らないプログラマの感覚とはかけ離れていると言うことです。

以下の例を見てください。

 #define BASE_SIZE 1 << 8

 int player[BASE_SIZE * 2];

これをCPPを知らないプログラマが読むとBASE_SIZEを2倍した要素数の配列を生成するように思うでしょうが、 BASE_SIZEはコンパイル前に展開されるので、 コンパイル時のコードは以下のようになっています。

 int player[1 << 8 * 2];

よーく見てみましょう。<<演算子は*演算子より優先度が低いので、 先に"8 * 2"が行われ、次に1のビットシフトが行われます。

つまり生成される配列の要素数ははBASE_SIZEの2倍の512個ではなく、 BASE_SIZEの2乗の65536個なのです。

これを避けるために、#defineではC++の文法から考えると過剰と思えるほどのカッコを付けます。 つまり上のような単純な場合にも"#define (1 << 8)" と記述します。

これでコンパイル時に"int player[(1 << 8) * 2];" となり、共通認識できる形になりました。

問題は#defineをマクロ関数として利用している場合です。 定数としてならカッコを大量に使用することで安全に使えるのですが、 マクロ関数としてはそれでも回避できない問題が残ります。

以下のプログラムを見てください。 上が展開前、下が展開後(コンパイル前)です。

 #define MIN(a, b) (((a) < (b)) ? (a) : (b))

 int hoge = 10;
 int piyo = 20;
 int min = MIN(++a, b);
 int hoge = 10;
 int piyo = 20;
 int min = ++hoge < piyo ? ++hoge : piyo;

展開後のコードを見ると、minに入る数は12であることが分かりますが、 展開前をみると11が入る錯覚を受けませんか?

この様に、#defineに常識的な動作を期待するのは不可能なので、 #defineでないと対処できない場合を除いてinlineを使用しましょう。

可視領域の広さ

第二に、可視領域の広さが挙げられます。

これはグローバル変数にも当てはまる欠点ですが、 #defineを記述したファイルをインクルードすると 当然そのファイルに定義されたシンボルは使用できなくなります。

グローバル変数の場合は名前空間に包むことで軽減できますが、 #defineは名前空間内部に記述しても意味がありません。

#defineはプリプロセッサなので当たり前なのですが、 この問題は初心者にはあまり直感的ではないかもしれません。

エラー時の混乱

#defineはコンパイル前に展開されるため、 シンボルテーブルには入りません。

これにより、#defineで設定した定数の使用方法に誤りがあると、 シンボル名(のつもりだが実はそうではない)の代わりに定数リテラルが出てきます。

つまり"#define HOGE 2"としてエラーになると、"HOGE"ではなく2がエラーとして報告されます。 いきなりそんな定数リテラルを報告されても、すぐに"HOGE"に行き着けるはずはなく、 解決のために無駄な時間を割かなければなりません。

まとめ

これらの理由から、#define(と言うかCPP全般)は使用しないほうがいいでしょう。 const定数やinline関数で置き換えられる部分は置き換えるべきです。

置き換えの手段がない方法(#includeや多重インクルード防止法、簡略化のための#define)も、 将来的には何らかの方法が考え出されるでしょう。

おそらく新しいキーワードが追加されるか、 C++の仕様が改定されると思います。