現場では同じソースからデバッグ版や製品版、OS別のバイナリを作ることがよくあります。
こうした切り替えを可能にするのが条件コンパイルです。
本稿では#if と #ifdef の違いと使い分けを中心に、プリプロセッサの流れから実用的なサンプル、注意点までを丁寧に解説します。
初心者の方でも手を動かしながら理解できる構成です。
条件コンパイルの基本
条件コンパイルとは
条件コンパイルは、プリプロセッサ指令を使ってコンパイル時にソースコードの一部を有効・無効にする仕組みです。
実行時のif
文とは異なり、実行ファイルに含めるかどうかをビルド時点で決めます。
これにより以下のようなニーズに対応できます。
デバッグログのON/OFF、OSやCPUアーキテクチャによる分岐、機能の段階的導入(Feature Toggle)、試験的コードの一時無効化などです。
ポイントは「コードがそもそもコンパイルされない」ため、無効化した部分は実行ファイルのサイズや実行速度に影響しないことです。
プリプロセッサの流れ
C言語のビルドは大まかに次の段階で進みます。
プリプロセス → コンパイル → アセンブル → リンクです。
条件コンパイルは最初のプリプロセス段階で実行されます。
ここで#if
などの指令が評価され、該当しないコードは完全に取り除かれます。
つまり条件コンパイルは「ソースを作り替える」処理であり、コンパイラに渡る前に決着がつきます。
使う命令
条件コンパイルで頻出する主な指令と役割をまとめます。
どれも行頭で#
から始まり、プリプロセッサが解釈します。
指令 | 役割 | 代表的な使い方 |
---|---|---|
#if | 整数定数式(defined演算子を含む)で判定 | #if LOG_LEVEL >= 2 |
#ifdef | マクロが定義済みかを判定 | #ifdef DEBUG |
#ifndef | マクロが未定義かを判定 | #ifndef NDEBUG |
#elif | 追加の条件分岐 | #elif defined(OS_WIN) |
#else | どの条件にも合わない場合 | #else |
#endif | 条件ブロックの終端 | #endif |
#if は「値で判定」し、#ifdef は「定義の有無で判定」するのが最大の違いです。
詳細は次章で解説します。
#if と #ifdef の違いと使い分け
#if の基本
#if
は整数定数式で判定します。
式中ではdefined(MACRO)
を使って定義の有無を整数(1 or 0)として扱えます。
未定義の識別子は0として評価される点に注意します。
// #if の基本例
#if defined(LOG_LEVEL) && (LOG_LEVEL >= 2)
// ここは LOG_LEVEL が 2 以上のときだけ有効
#endif
数値レベルや複雑な条件を扱うときは#ifが適切です。
#ifdef の基本
#ifdef
はマクロの定義有無だけを判定します。
値の大小や式の評価はできません。
反対条件は#ifndef
です。
// #ifdef の基本例
#ifdef DEBUG
// DEBUG が定義されているときだけ有効
#endif
#ifndef NDEBUG
// NDEBUG が未定義のときだけ有効(典型的にはデバッグ有効時)
#endif
存在フラグ(スイッチ)のON/OFFには#ifdefが読みやすく便利です。
違いと選び方
ルールとしては次の指針が実務で扱いやすいです。
単純な有無だけなら#ifdef
、段階や等号不等号を含む判定なら#if
を使います。
また#if defined(M)
と#ifdef M
は等価ですが、式の一部として書けるのは前者です。
たとえば#if defined(A) && !defined(B)
のように複合条件が必要な場合は#if
を選びます。
#elif と #else の使い方
複数の候補から1つを選ぶ場合は#elif
を連ね、最後に#else
でフォールバックを書くと読みやすくなります。
#if defined(OS_WIN)
// Windows 向け
#elif defined(OS_MAC)
// macOS 向け
#elif defined(OS_LINUX)
// Linux 向け
#else
// 未対応環境
#endif
#endif の役割
#endif
は条件ブロックの終端を示します。
ネストが深くなると見落としやすいため、閉じタグにコメントを付ける習慣を持つと保守性が高まります。
#ifdef DEBUG
// デバッグ用のコード
#endif // DEBUG
#endif の入れ忘れはコンパイルエラーや意図せぬコード無効化の原因になります。
後述の注意点も参照してください。
基本の書き方とサンプル
ここでは単体でビルド・実行できるサンプルを示します。
同じソースでも与えるマクロ定義で出力が変わることを確認しましょう。
定数で切り替える
バージョンや段階的機能(フェーズ)を数値で管理し、#if
で分岐します。
// file: feature_version.c
#include <stdio.h>
// バージョン定数を定義(ビルド時に -DVERSION=... で上書きも可能)
#ifndef VERSION
#define VERSION 2
#endif
int main(void) {
printf("App version: %d\n", VERSION);
#if VERSION >= 3
// 先進機能
printf("Enable: new rendering pipeline\n");
#elif VERSION == 2
// 中間機能
printf("Enable: optimized cache\n");
#else
// 旧機能のみ
printf("Enable: legacy mode\n");
#endif
return 0;
}
出力例(デフォルトのVERSION=2
の場合):
App version: 2
Enable: optimized cache
ビルド時に定数を変える例(GCC/Clang):
# VERSION=1 としてビルド・実行
cc feature_version.c -o feature_version -DVERSION=1
./feature_version
App version: 1
Enable: legacy mode
フラグで切り替える
フラグの有無でON/OFFを切る場合は#ifdef
が簡潔です。
// file: debug_flag.c
#include <stdio.h>
// デバッグを有効にしたい場合はビルド時に -DDEBUG を付ける
int main(void) {
puts("Start processing");
#ifdef DEBUG
// DEBUG が定義されているときだけ出力される
puts("[DEBUG] intermediate value = 42");
#endif
puts("Done");
return 0;
}
出力例(フラグ無し):
Start processing
Done
コンパイル時にフラグを渡した場合:
cc debug_flag.c -o debug_flag -DDEBUG
./debug_flag
Start processing
[DEBUG] intermediate value = 42
Done
複数条件を分岐する
OSやプラットフォーム別にコンパイル内容を切り替える典型例です。
ここでは学習用に任意のフラグOS_WIN
などを自分で与える形にしています(実環境ではコンパイラが用意する_WIN32
や__linux__
などを使います)。
// file: platform_switch.c
#include <stdio.h>
int main(void) {
#if defined(OS_WIN)
puts("Platform: Windows");
#elif defined(OS_MAC)
puts("Platform: macOS");
#elif defined(OS_LINUX)
puts("Platform: Linux");
#else
puts("Platform: Unknown");
#endif
return 0;
}
ビルド例:
# Windowsとして
cc platform_switch.c -o platform_switch -DOS_WIN
./platform_switch
# Linuxとして
cc platform_switch.c -o platform_switch -DOS_LINUX
./platform_switch
出力例(Windowsとしてビルド):
Platform: Windows
出力例(Linuxとしてビルド):
Platform: Linux
一時的にコードを無効化する
#if 0
は「巨大なコメント」のように使えます。
レビュー中に一時無効化したいときに便利です。
// file: temporary_disable.c
#include <stdio.h>
int compute(void) {
#if 0
// 試験中のアルゴリズム(まだ遅いので無効化)
return 100; // TODO: 高速化ののち再有効化
#else
// 安定版
return 42;
#endif
}
int main(void) {
printf("result=%d\n", compute());
return 0;
}
result=42
#if 0 の内側は「コンパイルされない」ため、型チェックや未使用警告も届きません。
長期間放置は避け、必ずTODOや期限を記しておきます。
コンパイル時にフラグを渡す
ビルドコマンドでマクロを定義すると、ソースを書き換えずに振る舞いを切り替えられます。
GCC/Clangなら-D
、MSVCなら/D
です。
// file: build_flags.c
#include <stdio.h>
#ifndef LOG_LEVEL
#define LOG_LEVEL 1 // 既定のログレベル(1=INFO, 2=DEBUG)
#endif
int main(void) {
puts("Start");
#if LOG_LEVEL >= 2
puts("[DEBUG] detail A");
puts("[DEBUG] detail B");
#endif
puts("Finish");
return 0;
}
デフォルト(定義なし):
Start
Finish
ビルド時に与える例:
# 詳細ログを有効化
cc build_flags.c -o build_flags -DLOG_LEVEL=2
./build_flags
Start
[DEBUG] detail A
[DEBUG] detail B
Finish
ソースはそのままに、ビルド引数だけで動作を切り替えられるのが条件コンパイルの大きな利点です。
よくある用途と注意点
デバッグログのON/OFF
開発中は大量のログが役立ちますが、製品版では邪魔になることがあります。
#ifdef DEBUG
で絞り込み、さらにLOG_LEVEL
の数値で段階制御するのが定石です。
マクロを薄いラッパとして整備すると呼び出し側がすっきりします。
// 例: ログ用マクロ
#if defined(DEBUG) && (LOG_LEVEL >= 2)
#define DBG_PRINT(msg) puts(msg)
#else
#define DBG_PRINT(msg) ((void)0) // 空展開(副作用なし)
#endif
呼び出し側は DBG_PRINT(“…”) と書くだけで、ビルド種別に応じて自動的に有効・無効が切り替わります。
環境やOSで分岐する
実運用ではコンパイラが環境マクロを提供します。
例として_WIN32
(Windows)、__linux__
(Linux)、__APPLE__
(Appleプラットフォーム)などがあります。
可搬性のためOS専用APIの呼び出しは条件コンパイルで囲むと安全です。
代表的マクロ | 意味(代表例) |
---|---|
_WIN32 | Windows(32/64両方) |
_WIN64 | Windows 64bit |
__linux__ | Linux |
__APPLE__ | Appleプラットフォーム(macOS/iOS系) |
__GNUC__ | GCC系コンパイラ |
_MSC_VER | MSVCコンパイラ |
実コード例:
#if defined(_WIN32)
// Windows固有の処理
#elif defined(__APPLE__)
// Apple系固有の処理
#elif defined(__linux__)
// Linux固有の処理
#else
// 既定処理または未対応
#endif
#endif の入れ忘れに注意
#endif の抜けはビルドエラーや広範囲の無効化につながる致命的ミスです。
対策として次をおすすめします。
閉じ#endif
に短いコメントを付ける、IDEのプリプロセッサ可視化やフレーム表示を有効にする、ネストを浅く保つ、CIで-Werror
などの厳格な警告を有効化する、です。
条件は整数で判定
#if
が扱えるのは整数定数式のみです。
浮動小数点や文字列比較はできません。
どうしても文字列で切り替えたい場合は文字列を整数にマップするか、defined
と個別のフラグに分解します。
悪い例:
// NG: 文字列比較は不可
// #if MODE == "fast"
良い例:
// OK: 整数で比較
#if MODE == 2
未定義マクロは0扱い
#if の式中で未定義の識別子は 0 として扱われます。
これは便利な半面、スペルミスに気づきにくい原因にもなります。
// LOGLEVEL と書くべきところを LOG_LEVL と誤記
#if LOG_LEVL >= 2 // 未定義 -> 0 >= 2 -> 偽
// ここは実行されない(気づきにくい)
#endif
対策として原則 #if defined(MACRO) を併用し、綴りミスを早期に発見できるようにしましょう。
#if defined(LOG_LEVEL) && (LOG_LEVEL >= 2)
// 正しく判定
#endif
ネストを浅くする
条件コンパイルは強力ですが、ネストし過ぎると可読性が急落します。
可能ならガード節パターンで早めに枝刈りし、#if/#endif
の入れ子を避けます。
// ガード節: まず対象外を落とす
#if !defined(ENABLE_FEATURE)
// 何もしない(または互換実装)
#else
// 機能の本体
#endif
この形にすると有効経路のコードが左に寄り、読みやすくなります。
可読性を保つ書き方
可読性の秘訣は次の通りです。
マクロ名は役割が分かるよう接頭辞(例: OS_, FEAT_, CFG_)を付ける、ブロックの終わりに#endif // 名前
の形でコメントを入れる、プロジェクトの「ビルド時に与えるマクロ一覧」をドキュメント化する、値を持つマクロは範囲や意味をコメントで明記する、などです。
また条件式は短く・平易に保ち、可能なら関心ごとごとに#include
されたモジュールへ分離するのが保守性に優れます。
まとめ
本稿では#if と #ifdef の違いと使い分けを中心に、プリプロセッサの動作、よくある実装パターン、落とし穴までを解説しました。
要点は次の通りです。
単純な有無は#ifdef
、数値や複合条件は#if
を用いる、#elif/#else/#endif
で分岐を整理する、-D
や/D
でビルド時に柔軟に切り替える、未定義識別子は0扱いであることに注意する、です。
条件コンパイルは実行コストなしで振る舞いを変えられる強力な道具です。
過剰なネストを避け、意図と範囲をコメントで明確にしながら、堅牢で読みやすいコードベースを育てていきましょう。