8.4 条件付きコンパイル
8.4.1 コンパイルを制御する
C 言語は、多くのシステムで採用されている国際的なプログラミング言語であり、プロフェッショナルのプログラマの間では標準語のような存在です。これだけ多くのシステムで採用されている言語であれば、当然、プログラマは移植性の高いコードを作成しようと努めることでしょう。
しかし、コンパイル時の状況に応じてどうしても書き直さなければならない部分が生じてきます。OS のバージョンや開発環境が異なれば、わずかな仕様の違いがプログラムの動作に影響を与えることもありますし、コードを部分的に OS に特化させたい場合もあるでしょう。
こうした問題は、ソースを書き直すということを行うよりも、コンパイルするべきソースを分岐させるなどの、コンパイル処理を制御する条件付きコンパイルと呼ばれるテクニックを使うことで効率的に解決することができます。これも、コンパイル前にソースを整形するプリプロセッサが活躍します。
コンパイルの部分的な制御は #if ディレクティブと #endif ディレクティブの組み合わせで構成されます。#if ディレクティブは #endif に対応付けられていなければなりません。
#if 定数式 コンパイルコード #endif
定数式には、整数型の定数式をしていします。この定数式が偽であれば対象コードはコンパイルされません。この機能を利用すれば、デバッグ時の一時的なコードなどを記述することができます。リリース版をコンパイルする場合は定数式を 0 にすればよいのです。
定数式には #define ディレクティブで定義した名前などを使うことができますが、変数や sizeof 演算子、型キャストは使えません。そのほかの基本的な演算子は使用することができます。
#include <stdio.h> #define DEBUG_MODE 1 int main() { #if DEBUG_MODE printf("Debug mode\n"); #endif return 0; }
コード1では、main() 関数内で #if ディレクティブを用いています。このプログラムでは定数 DEBUG_MODE が真であれば printf() をコンパイルし、偽であればコンパイルを行わないことを表しています。DEBUG_MODE は 1 なので printf() 関数がコンパイルされますが、これを 0 に変更して再コンパイルすれば #if から #endif までのテキストがコンパイル対象から排除されることを確認することができます。
C 言語の構文である if-else 同様に、#if ディレクティブが偽であった場合にコンパイルするコードを指定したい場合は、#else ディレクティブを用います。#else ディレクティブは、基本的に if-else の関係と同じなので理解しやすいでしょう。
#if 定数式 真のコンパイルコード #else 偽のコンパイルコード #endif
この場合、定数式が 0 以外であれば #if の後のテキストが、0 であれば #else から #endif までのテキストがコンパイル対象となります。#else ディれ句ティイブは #if と組み合わせて用いられ、必ず #endif ディレクティブの前に記述されます。#else ディレクティブは #if ~ #endif までの間に最大で1つしか指定することができません。因みに、#if ディレクティブは入れ子にすることが可能です。
#if N #if M ... #else ... #endif #else ... #endif
この構文を用いることによって、コンパイルを行う国や開発環境、システムなどに応じて対象コードを選択させることができます。ただし、必要以上に複雑になってはいけません。あまりに複雑であれば、プログラムに委ねる方が無難でしょう。
#include <stdio.h> #define EN 1 #define JP 2 #define LANG JP int main() { #if LANG == EN printf("Kitty on your lap\n"); #else #if LANG == JP printf("あなたのひざの上の猫\n"); #endif #endif return 0; }
このプログラムは、国コードを表す EN と JP 定数を定義し、さらに #if 等で比較に用いるための定数 LANG を定義しています。プログラムは LANG が EN であれば英語で、JP であれば日本語で文字列を表示します。#else ディレクティブの後は、さらに #if ディレクティブを用いて入れ子構造になっているところにも注目してください。
コード2のように #if ディレクティブを入れ子構造にする場合、プリプロセッサディレクティブの構文上の問題から #else ディレクティブの後に指定する場合などでも改行を行わなければならなく、さらに #endif ディレクティブに対応させる必要があるためコードが複雑になりがちです。そこで #else ディレクティブと #if ディレクティブを組み合わせた #elif ディレクティブが用意されています。#elif ディレクティブは次のようにして利用することができます。
#if 定数式 コンパイルコード #elif 定数式 コンパイルコード #elif 定数式 ... #endif
#elif には #else と #if が一体となった効力があり、#if を #else の後に入れ子にする場合と同じ結果を得ることができるため、段階的に #if ディレクティブによって定数を評価したい場合に有効な手段となります。#elif は定数式が真であればそれ以降のテキストをコンパイル対象とします。#else と同様に #if~#endif の間に指定することができ、#else とは異なり複数指定することが可能です。
#include <stdio.h> #define EN 1 #define JP 2 #define LANG EN int main() { #if LANG == EN printf("Kitty on your lap\n"); #elif LANG == JP printf("あなたのひざの上の猫\n"); #endif return 0; }
コード3はコード2を #elif ディレクティブを用いて表現したものです。動作は同じですが #if ディレクティブを入れ子にするよりもソースは短くなるため可読性が向上します。
8.4.2 名前定義による制御
実は、#define で定義する名前は必ずしも何らかのトークン列に展開されるわけではありません。名前だけを定義して、トークン列を省略することが可能なのです。例えば次のような定義が許されます。
#define DEBUG
定数 DEBUG はトークン列を持ちませんが名前は定義されています。このような名前の定義は、コードに影響を与えることはありませんが、プリプロセッサレベルの処理では意味があります。
例えばコード1のように、デバッグとリリースでは異なるソースをコンパイルさせたい場合、定数 DEBUG の値を評価するよりも、DEBUG という名前が定義されているかどうかを評価するほうがスマートだと考えられています。そうすれば、#define をコメント化するか、#undef を用いて定義解除を行うことでリリース版のコンパイルを行うことができます。
名前の定義を評価するには defined プリプロセッサ演算子を用います。このプリプロセッサ演算子は #if ディレクティブまたは #elif ディレクティブの定数式でのみ使用することができます。
defined(識別子)
defined 識別子
defined 演算子は定義を評価する識別子を ( ) で囲むか、defined の後に空白をあけて指定します。指定した識別子が #define によって定義されている場合は真を、未定義であれば偽を返します。
#include <stdio.h> #define DEBUG int main() { #if defined(DEBUG) printf("Debug mode\n"); #else printf("Release mode\n"); #endif return 0; }
コード4は、DEBUG という名前が定義されていればデバッグモード用のコードを、そうでなければリリース用のコードをコンパイルするというプログラムの例です。#if ディレクティブで defined 演算子を用いて識別子を評価しています。
しかし、一般的には defined 演算子が用いられることはありません。C 言語プログラマの多くは、これよりも単純に記述できる #ifdef ディレクティブ及び #ifndef ディレクティブを用いることを好んでいるからです。これらは #if defined ... という形を省略したディレクティブです。
#ifdef 識別子
#ifndef 識別子
これら二つのディレクティブは #if defined(識別子) と #if !defined(識別子) というコードに等しいと考えることができます。#ifdef ディレクティブは識別子が定義されていれば、その後のコードをコンパイルします。逆に #ifndef は識別子が定義されていなければ、その後のコードをコンパイルします。これらのディレクティブは #if ディレクティブと同様に #endif に対応する必要があります。
#include <stdio.h> #define DEBUG int main() { #ifdef DEBUG printf("Debug mode\n"); #else printf("Release mode\n"); #endif return 0; }
コード5はコード4を #ifdef ディレクティブを用いて改良したものです。結果は同じですが #if defined(...) の代わりに #ifdef ディレクティブを用いています。多くの C 言語プログラマはこちらの書き方を好んでいます。
8.4.3 コンパイルエラー
#if ディレクティブなどを用いて定数や名前を調べた結果、コンパイルを行うには不十分な状態だとすれば、開発者はどのようにソースを記述するべきでしょう。十分な情報が設定されていない場合の処理方法のひとつは、デフォルトの動作というものをあらかじめ定め、それを設定するというような方法もありますが、それが好ましくない場合もあります。
こうした場合 #error ディレクティブを用いてコンパイルを強制的に中止させる方法があります。
#error 出力エラー文字列
プリプロセッサは、#error が評価されるとコンパイルを停止しエラー文字を出力します。コンパイルに必要な開発者の意思となる名前が定義されていない場合や、必要なヘッダファイルがインクルードされていない場合にこうしたエラーを出力させる方法が考えられます。
#include <stdio.h> /*#define BUFFERSIZE 0xFF*/ int main() { #ifdef BUFFERSIZE int iArray[BUFFERSIZE]; #else #error BUFFERSIZE 定数が定義されていません。 #endif return 0; }
コード6は、プログラムが割り当てる配列のサイズ BUFFERSIZE 定数が定義されているかどうかを #ifdef ディレクティブを用いて調べています。BUFFERSIZE が定義されていれば、この値を用いて配列を初期化しますが、そうでなければ配列を初期化することができないため、エラーを発生させます。エラー文字列がどのように表示されるかはコンパイル環境に依存する問題です。上の実行結果は Visual Studio 2010 でコンパイルした場合です。