C言語の条件コンパイルは、ビルド時の条件に応じてコンパイルするコードを切り替える仕組みです。
OSやビルドモードの違いに合わせて処理を分岐したいときに役立ちます。
本記事では#ifと#ifdefを中心に、違いと正しい使い方、ありがちな落とし穴、ビルド指定の方法を初心者向けに丁寧に解説します。
条件コンパイル入門
何ができるか
条件コンパイルを使うと、コンパイル時の条件に応じてソースコードの一部を有効化または無効化できます。
これは実行時のif
分岐とは違い、不要なコードはそもそもバイナリに含まれません。
デバッグ出力の有無やOSごとの処理切り替え、試験的機能のオンオフなどに向いています。
基本の流れ
Cのビルドはおおむね次の順で進みます。
プリプロセッサ→コンパイラ→リンカです。
条件コンパイルは最初のプリプロセス段階で評価されます。
つまり#if
や#ifdef
で囲まれたコードは、この段階で「残す」「捨てる」が確定します。
マクロは#define
でソース内に書くか、ビルド時に-D
で与えます。
よくある用途
代表的には次のような使い方があります。
デバッグビルドでは詳細ログを出すがリリースでは消す、WindowsとLinuxでAPIが違う部分だけ切り替える、軽量版とフル機能版で機能を限定するなどです。
不要なコードをビルド対象から外せるため、サイズや依存関係を抑えられます。
#ifと#ifdefの違い
#if
#if
は定数式を評価して真偽を決めます。
式の中ではマクロが展開され、数値の大小比較や論理演算が使えます。
未定義の識別子は0として扱われますが、コンパイラの警告対象になることがあるため安全に書くにはdefined
を併用します。
例えば#if LOG_LEVEL >= 2
のように数値で段階を切ることができます。
#ifdef
#ifdef
はそのマクロが定義されているかどうかだけを判定します。
値が0であっても「定義されていれば真」です。
0に定義しても#ifdef
は有効とみなされるため注意してください。
値ではなく存在の有無で分岐したいときに使います。
#ifndef
#ifndef
は未定義なら真になる逆バージョンです。
ヘッダの二重インクルードを防ぐインクルードガードでよく使われますが、インクルードガードの詳細は別記事で扱います。
defined(MACRO)の基本
defined(MACRO)
は#ifや#elifの式の中で、マクロの定義有無を0または1で返します。
#if defined(DEBUG)
は#ifdef DEBUG
と同等で、複合条件が書けます。
例えば#if defined(DEBUG) && LOG_LEVEL > 1
のように組み合わせられます。
以下の表で違いをまとめます。
ディレクティブ/書き方 | 主な判定 | 真になる条件 | 0に定義した場合 | 典型例 |
---|---|---|---|---|
#ifdef MAC | 定義の有無 | MACが定義済み | 真 | #ifdef DEBUG |
#ifndef MAC | 定義の有無 | MACが未定義 | 偽 | #ifndef INCLUDED_H |
#if EXPR | 定数式 | EXPRが非0 | 値による | #if LOG_LEVEL >= 2 |
#if defined(MAC) | 定義の有無 | MACが定義済み | 真 | #if defined(_WIN32) |
条件コンパイルの書き方と例
#ifの例
数値マクロで段階的にログ出力を切り替える例です。
デフォルト値を#ifndef
で補い、#if
/#elif
で分岐します。
// log_level.c
#include <stdio.h>
// LOG_LEVELが未定義ならデフォルトの1にする
#ifndef LOG_LEVEL
#define LOG_LEVEL 1
#endif
int main(void) {
// マクロの値はコンパイル時に決まり、ここでは文字列に展開される
printf("LOG_LEVEL=%d\n", LOG_LEVEL);
// 段階的にログを切り替える
#if LOG_LEVEL >= 2
printf("[VERBOSE] 詳細なログを出力します。\n");
#elif LOG_LEVEL == 1
printf("[INFO] 通常のログを出力します。\n");
#else
printf("[SILENT] ログを出力しません。\n");
#endif
// 非0かどうかの単純判定
#if LOG_LEVEL
printf("LOG_LEVELが非0なので、この行は出力されます。\n");
#endif
return 0;
}
LOG_LEVEL=1
[INFO] 通常のログを出力します。
LOG_LEVELが非0なので、この行は出力されます。
LOG_LEVEL=2
[VERBOSE] 詳細なログを出力します。
LOG_LEVELが非0なので、この行は出力されます。
LOG_LEVEL=0
[SILENT] ログを出力しません。
コンパイル方法の例は後述の「-Dでマクロを定義する」を参照してください。
#ifdefの例
存在有無で機能を有効化する例です。
#ifdef
は値が0でも「定義されていれば真」になることを確認します。
// feature_toggle.c
#include <stdio.h>
// FEATURE_Xが定義されているときだけ関数を用意する
#ifdef FEATURE_X
static void feature_x(void) {
printf("Feature X が有効です!\n");
}
#endif
int main(void) {
#ifdef FEATURE_X
feature_x(); // 関数が存在するのはFEATURE_Xが定義されるビルドだけ
#else
printf("Feature X は無効です。(-DFEATURE_X で有効化)\n");
#endif
return 0;
}
Feature X は無効です。(-DFEATURE_X で有効化)
Feature X が有効です!
#define FEATURE_X 0
としても#ifdef FEATURE_X
は真です。
値で切り替えるなら#if FEATURE_X
を使うか、#if defined(FEATURE_X) && FEATURE_X
のように書きます。
#elif/#elseの使い方
複数の候補からいずれか1つだけを選ぶには#elif
と#else
を連ねます。
以下はプラットフォームごとにメッセージを切り替える例です。
// platform_message.c
#include <stdio.h>
int main(void) {
#if defined(_WIN32)
printf("Windows向けの処理を実行します。\n");
#elif defined(__APPLE__)
printf("Apple/macOS向けの処理を実行します。\n");
#elif defined(__linux__)
printf("Linux向けの処理を実行します。\n");
#else
printf("未知のプラットフォームです。\n");
#endif
return 0;
}
Linux向けの処理を実行します。
Windows向けの処理を実行します。
これらのOSマクロは多くの場合コンパイラが自動で定義します。
通常は自分で-D__linux__
のように指定しません。
プラットフォーム切替
プラットフォームで使える代表的なマクロは次のとおりです。
自動定義されるマクロを前提にし、必要であれば自前の抽象化マクロに集約して使うのが保守しやすいです。
目的 | よく使うマクロの例 |
---|---|
Windows | _WIN32 |
Linux | __linux__ |
macOS | __APPLE__ |
Unix系全般 | __unix__ |
推奨パターンとして、自前の統一マクロを定義し、以降のコードではそれだけを見る方法があります。
// os_detect.h (抜粋)
// ここでは「検出」と「抽象化」を一箇所に集約する
#if defined(_WIN32)
#define OS_WINDOWS 1
#elif defined(__APPLE__)
#define OS_MAC 1
#elif defined(__linux__)
#define OS_LINUX 1
#else
#define OS_UNKNOWN 1
#endif
以降の各所では#if defined(OS_WINDOWS)
のように分岐すれば、将来別OSを足す際も変更点が限定されます。
ビルド指定と注意点
-Dでマクロを定義する
ビルド時の-D
指定は最も手軽な外部設定手段です。
GCC/ClangとMSVCでの指定方法は次のとおりです。
// GCC/Clang
cc -DDEBUG main.c -o app // DEBUGを定義
cc -DLOG_LEVEL=2 log_level.c -o app // 数値で定義
cc -DFEATURE_X feature_toggle.c -o app // 存在で切り替え
// MSVC
cl /DDEBUG main.c
cl /DLOG_LEVEL=2 log_level.c
cl /DFEATURE_X feature_toggle.c
ビルドツールを使う場合の例です。
# CMake
target_compile_definitions(myapp PRIVATE DEBUG LOG_LEVEL=2)
文字列を渡す場合はシェルのクォートに注意します。
-DNAME="Alice"
。
#endifの閉じ忘れ対策
プリプロセッサのブロックは必ず閉じる必要があります。
閉じ忘れは見つけづらいので、対応する条件を#endif
の行末コメントに書く習慣がおすすめです。
#if defined(DEBUG)
// デバッグ用コード...
#endif // defined(DEBUG)
長いファイルではセクションごとに空行やコメントで区切って視認性を上げます。
ネストしすぎない
条件コンパイルを深くネストすると読みづらくなります。
次のような入れ子は避けましょう。
// 悪い例: 条件が入り組んでいて読みづらい
#if defined(_WIN32)
#if defined(DEBUG)
// ...
#else
// ...
#endif
#else
#if LOG_LEVEL > 1
// ...
#endif
#endif
より良い書き方は、中間の「意味のあるマクロ」を導入して平坦化することです。
// まず集約マクロを作る
#if defined(_WIN32)
#define PLATFORM_WINDOWS 1
#endif
#if defined(DEBUG) || LOG_LEVEL > 1
#define NEED_VERBOSE_LOG 1
#endif
// 以降は意味論に基づく分岐だけにする
#if defined(PLATFORM_WINDOWS)
// ...
#endif
#if defined(NEED_VERBOSE_LOG)
// ...
#endif
条件はシンプルに
式が複雑だと誤解や警告を招きます。
特に未定義マクロを#if
式で直接使うと、多くのコンパイラは警告します。
安全な書き方を心がけましょう。
// 悪い例: FOOやBARが未定義だと警告や意図しない0扱い
#if FOO && (BAR > 1)
#endif
// 良い例1: defined()で存在を確認してから値を見る
#if defined(FOO) && (FOO) && defined(BAR) && (BAR > 1)
#endif
// 良い例2: 事前にデフォルト値を与えておく
#ifndef FOO
#define FOO 0
#endif
#ifndef BAR
#define BAR 0
#endif
#if FOO && (BAR > 1)
#endif
また、値で切り替えたい場合は#if
を、存在有無なら#ifdef
/#ifndef
を使うと読み手に意図が伝わりやすくなります。
まとめ
条件コンパイルは、コンパイル時にコードを選別して最終バイナリから不要な部分を取り除ける強力な仕組みです。
数値判定ができる#if
、存在有無を見る#ifdef
/#ifndef
、そしてdefined(MACRO)
を適所で使い分けることで、デバッグ切り替えやプラットフォーム分岐を明瞭に記述できます。
ビルド時の-D
指定も活用しつつ、ネストを浅く保ち、条件をシンプルにするのが保守のコツです。
なお、定数マクロや関数風マクロ、インクルードガードは本記事の範囲外ですので、別の記事で詳しく解説します。