条件付きコンパイルは、1つのソースファイルを複数の異なるプログラムに変えます。プリプロセッサがコンパイラの実行前に、どのテキストのブロックが生き残るかを決めます - なので成立しない分岐の内側のコードは、実行時に単にスキップされるのではなく、削除されます。解析すらされないので実行ファイルには何も寄与せず、現在のコンパイラが拒否するような構文を含んでいても構いません。
これが普通の if との違いです。どちらも経路を選びますが、プログラムが存在する前に選ぶのは一方だけです。
#ifdef と #ifndef
最もシンプルな問いは、マクロが定義されているかどうかです:
#define DEBUG の行を削除すると、すべてのデバッグ出力がビルドから消えます - 無効になるのではなく、なくなります。まずそのまま実行し、それからその行を消してもう一度実行してみてください。
#ifndef は否定です。マクロが定義されていない場合にのみブロックを残します。最もよくある用途はヘッダーファイルで扱うインクルードガードですが、呼び出し側が上書きできるデフォルト値を用意する方法でもあります:
すべての #ifdef と #ifndef は #endif で閉じなければなりません。長いファイルでは閉じ側に注釈を付けておくと - #endif /* DEBUG */ - 後で本当に時間の節約になります。
#if、#elif、#else
#if は整数定数式を取り、それが非ゼロのときブロックを残します。これによりバージョンやレベルの比較が可能になります:
LOG_LEVEL を 0 や 3 に変えて再実行すると、プログラム自体が形を変えるのが分かります。
この式はプリプロセッサが評価します。つまり使えるのは整数定数、算術演算子と比較演算子、そしてそれらに展開されるマクロだけです。sizeof、enum の値、変数、その他コンパイラを必要とするものは見えません。#if の中の未定義のマクロは、エラーになるのではなく 0 と評価されます。これは便利で、ときどき驚かされます:
#if FEATURE_X /* FEATURE_X はどこにも定義されていない -> 0 -> ブロックは削除される */
defined(NAME) は #ifdef の問いを #if の中で使える形にするので、テストを組み合わせられます:
#if defined(DEBUG) && !defined(NDEBUG)
/* デバッグビルドで、アサーションが有効 */
#endif
#if defined(LINUX) || defined(BSD)
/* どちらかの unix 系システム */
#endif
#if defined(X) と #ifdef X は同じ意味です。&&、||、! が必要なときに前者を使いましょう。
コマンドラインからマクロを定義する
こうしたスイッチの本当の力は、ソースをまったく変えなくてよいことです。gcc -D はコンパイル全体に対してマクロを定義します:
gcc -DDEBUG program.c -o program # DEBUG は 1 として定義される
gcc -DLOG_LEVEL=3 program.c -o program # 特定の値
gcc -DDEBUG -DBUFFER_SIZE=512 a.c b.c -o app
-DNAME だけなら #define NAME 1 と同等です。-U NAME は定義を取り消します。これはヘッダーが定義したものをオフにしたいときに効いてきます。
そこで通常の構成はこうなります: ソースには #ifndef で守られたデフォルトを置き、ビルドコマンドが構成を選ぶのです。
/* config.h */
#ifndef LOG_LEVEL
#define LOG_LEVEL 1 /* デフォルトは静かに */
#endif
#ifndef MAX_CONNECTIONS
#define MAX_CONNECTIONS 64
#endif
# 開発ビルド
gcc -DDEBUG -DLOG_LEVEL=3 -Wall -Wextra -g src/*.c -o app-dev
# リリースビルド
gcc -DNDEBUG -O2 src/*.c -o app
NDEBUG は標準化されています。これを定義するとプログラム内のすべての assert() が無効になります。<assert.h> 自体が条件付きコンパイルで書かれているからです。これはこのパターンの縮図です - ビルドが何を定義したかによって、違うものにコンパイルされるヘッダーなのです。
プラットフォームの切り替え
コンパイラはターゲットシステムを識別するマクロをあらかじめ定義しているので、1つのソースファイルでそれぞれ正しいAPIを呼べます:
#if defined(_WIN32)
#include <windows.h>
#define CLEAR_SCREEN "cls"
#elif defined(__APPLE__)
#include <unistd.h>
#define CLEAR_SCREEN "clear"
#elif defined(__linux__)
#include <unistd.h>
#define CLEAR_SCREEN "clear"
#else
#error "Unsupported platform"
#endif
よく使うもの: _WIN32(32ビットと64ビットの両方の Windows で定義される)、_WIN64、__linux__、__APPLE__、__unix__、__ANDROID__。コンパイラの識別には別のセットがあり - __GNUC__、__clang__、_MSC_VER - アーキテクチャにもあります - __x86_64__、__aarch64__。
何の上でビルドされたかを報告する、実行可能な版はこちらです:
#error はそれ自体知っておく価値があります。あなたのメッセージとともにコンパイルを止めます。プラットフォームの連鎖を #else / #error "Unsupported platform" で締めくくれば、静かな誤ビルドがビルドログの先頭に現れる明確な失敗に変わります。
コンパイラがあらかじめ定義しているマクロをすべて列挙するには:
gcc -dM -E - < /dev/null
この出力が「このマシンで何をテストできるか」に対する決定的な答えです。
#if 0 でコードを無効にする
ブロックを /* ... */ でコメントアウトすると、そのブロック自身がコメントを含んだ瞬間に失敗します。C言語のコメントはネストできないので、内側の最初の */ が外側のコメントを終わらせ、その後がすべて浮いたコードになるからです。#if 0 にはそんな問題はありません:
#if 0
/* この領域全体が、コメントも含めて取り除かれます。 */
legacy_init();
int n = old_calculation(42); /* このコメントも問題ありません */
report(n);
#endif
#if 1 にひっくり返せばコードが戻ります。エディタは引き続きCとしてハイライトしますし、#if 0 を別の #if の中にネストすることもできます。
これはデバッグの道具であって、保管システムではありません。#if 0 の中に置かれたコードは決してコンパイルされないので、静かに腐っていきます - 誰かがスイッチを戻すころには、もうビルドできません。問題を二分探索している間に使い、それからブロックは削除してバージョン管理に覚えておいてもらいましょう。
条件付きコンパイルが失敗するところ
3つの失敗パターンが、苦痛の大半を占めます。
コンパイルされないコードは検査もされません。 非アクティブな #ifdef 分岐の中のタイプミスは、誰かがその構成をビルドするまで見えません - 何か月も後かもしれませんし、あなたが持っていないプラットフォームのCIかもしれません。プロジェクトに重要なプラットフォーム分岐があるなら、それらすべてを定期的にビルドしましょう。
#ifdef を制御フローと交互に挟むと、あっという間に読めなくなります。 こういうものは理解するのが大変です:
if (ready) {
#ifdef FAST_PATH
fast_send(buf);
} else {
#endif
slow_send(buf);
}
ある分岐で開いた波かっこを別の分岐で閉じるのは合法ですが、ひどいものです。関数まるごとを包む条件分岐にして、その間で選ぶほうが良いでしょう:
#ifdef FAST_PATH
static void send_data(const char *buf) { fast_send(buf); }
#else
static void send_data(const char *buf) { slow_send(buf); }
#endif
実行時の if のほうが良いことが多い。 どちらの分岐もどこでもコンパイルできるなら、普通の if (debug_enabled) のほうが両方の経路が型検査され、テスト可能で、再ビルドなしに切り替えられます。プリプロセッサは、それにしかできないこと - 他のプラットフォームでは本当にコンパイルできないコード - のために取っておきましょう。
これらすべてを組み合わせるパターン - デフォルトを持つヘッダー、ビルドごとの -D、インクルードガードに包まれたプラットフォーム分岐 - についてはヘッダーファイルを、#define 自体についてはマクロを参照してください。
よくある質問
C言語の条件付きコンパイルとは何ですか?
コンパイラが動く前に、プリプロセッサ指令を使ってソースコードのブロックを残すか捨てるかを決めることです。成立しない #ifdef の内側のコードはテキストから完全に削除されます - 解析もされず、コンパイルもされず、実行ファイルにも現れません。1つのソースファイルで複数のプラットフォームやビルド種別をサポートする方法です。
#ifdef と #if の違いは何ですか?
#ifdef NAME は値に関係なく、そのマクロが存在するかどうかだけを尋ねます。#if 式 は整数定数式を評価するので、#if VERSION >= 3 のように値を比較できます。単に有無だけのフラグには #ifdef、値が重要なときは #if を使いましょう。#if defined(NAME) は両方を兼ね、&& や || と組み合わせられます。
gcc のコマンドラインからマクロを定義するにはどうしますか?
-D を使います: gcc -DDEBUG program.c は、ファイルの先頭に #define DEBUG 1 があるかのように DEBUG を定義し、-DMAX=50 は特定の値を与えます。ビルドシステムがソースを編集せずに機能を切り替えるのはこの方法で、-U NAME は定義を取り消します。
コードをコメントアウトする代わりに #if 0 を使うのはなぜですか?
/* ... */ コメントはネストできないからです - 対象領域の中の最初の */ がコメントを早く終わらせ、残りが壊れたコードになります。#if 0 ... #endif は、いくつコメントを含んでいても、どれだけの量のコードでも取り除けますし、シンタックスハイライトも生きたままで、#if 1 に戻すのも簡単です。