CMakeのadd_definitions()から卒業!安全で効率的な定義追加方法を徹底解説

2025-05-27

add_definitions()の基本的な使い方

add_definitions()は、引数として-Dまたは/Dで始まる定義を渡します。例えば、以下のように使います。

add_definitions(-DFOO -DBAR=1 -DVERSION_STRING="1.0.0")

これは、コンパイラに以下のオプションを渡すこととほぼ同等です。

  • MSVCの場合:/DFOO /DBAR=1 /DVERSION_STRING="1.0.0"
  • GCC/Clangの場合:-DFOO -DBAR=1 -DVERSION_STRING="1.0.0"

これにより、ソースコード内でこれらの定義を使用できるようになります。

#ifdef FOO
    // FOOが定義されている場合のコード
#endif

#if BAR == 1
    // BARが1の場合のコード
#endif

const char* version = VERSION_STRING; // "1.0.0"が設定される

add_definitions()のスコープ

add_definitions()で追加された定義は、そのコマンドが呼び出されたCMakeLists.txtファイルとそのサブディレクトリにあるすべてのターゲット(ライブラリや実行ファイルなど)に適用されます。これは、その定義が呼び出される前後に定義されたターゲットにも適用されるという、比較的広いスコープを持ちます。

重要な注意点として、add_definitions()はCMakeの比較的新しいバージョンでは非推奨(deprecated)とされています。 以下の理由から、通常はより新しいコマンドを使用することが推奨されています。

  1. 非推奨であること
    CMakeのバージョン3.12以降では、add_definitions()ではなく、より明確な目的を持つコマンドが推奨されています。
  2. 曖昧なスコープ
    add_definitions()は現在のディレクトリとそのサブディレクトリのすべてのターゲットに影響を与えるため、特定のターゲットにのみ定義を適用したい場合に不便です。また、プロジェクト全体に予期せぬ影響を与える可能性があります。
  3. 汎用性の欠如
    プリプロセッサ定義以外のコンパイラオプション(例えば警告レベルなど)を追加するのには適していません。
  • target_compile_options()
    特定のターゲットに一般的なコンパイラオプションを追加するために使用されます。

    add_executable(my_lib my_lib.cpp)
    target_compile_options(my_lib PRIVATE -O3)
    
  • add_compile_options()
    プリプロセッサ定義以外の一般的なコンパイラオプションを追加するために使用されます。例えば、警告レベルの設定などです。

    add_compile_options(-Wall -Wextra)
    
  • target_compile_definitions()
    特定のターゲットに対してのみプリプロセッサ定義を追加したい場合に最適です。これにより、定義が適用される範囲を細かく制御できます。

    add_executable(my_app main.cpp)
    target_compile_definitions(my_app PRIVATE MY_APP_DEBUG) # my_appのみに適用
    

    PRIVATEキーワードは、その定義がターゲットのソースファイルのみに適用され、そのターゲットに依存する他のターゲットには適用されないことを意味します。PUBLICINTERFACEも指定できます。

  • add_compile_definitions()
    これはadd_definitions()の直接的な後継であり、プリプロセッサ定義を追加するために特化しています。こちらも現在のディレクトリとそのサブディレクトリに影響しますが、add_definitions()よりも目的が明確です。

    add_compile_definitions(FOO BAR=1 VERSION_STRING="1.0.0")
    

    -Dを記述する必要がない点に注意してください。



しかし、もし既存のプロジェクトでadd_definitions()を使用している場合、またはその動作を理解するために、よくあるエラーとトラブルシューティングについて説明します。

意図しない広範なスコープによる問題

エラー/症状

  • 特定の定義を変更しただけで、関係ないはずのファイルまで再コンパイルが走る。
  • ある特定のターゲットにのみ定義を適用したかったのに、プロジェクト全体の他のターゲットにも定義が適用されてしまい、予期せぬコンパイルエラーや動作変更が発生する。

原因
add_definitions()は、それが呼び出されたCMakeLists.txtファイルとそのサブディレクトリにあるすべてのターゲットに適用されます。これは、そのコマンドが呼び出される前後に定義されたターゲットにも影響するため、意図しない広範なスコープになります。

トラブルシューティング/解決策

  • add_compile_definitions()も検討する。これはadd_definitions()の直接的な後継ですが、依然として現在のディレクトリとそのサブディレクトリ全体に影響を与えるため、スコープの問題は限定的です。

  • 最も推奨される解決策
    add_definitions()の使用を避け、代わりにtarget_compile_definitions()を使用する。これにより、定義を特定のターゲットに限定して適用できます。

    # 古い方法(広範なスコープ)
    # add_definitions(-DMY_DEFINE)
    
    # 新しい推奨方法(特定のターゲットに限定)
    add_executable(MyTarget main.cpp)
    target_compile_definitions(MyTarget PRIVATE MY_DEFINE)
    

    PRIVATEキーワードは、そのターゲット自身のコンパイルにのみ定義が適用され、そのターゲットに依存する他のターゲットには適用されないことを意味します。

定義の値にスペースや特殊文字が含まれる場合の問題

エラー/症状

  • エスケープ文字の扱いが複雑で、期待通りに定義されない。
  • -DMESSAGE="Hello World"のようにスペースを含む文字列を定義しようとしたが、コンパイラが正しく認識しない。

原因
add_definitions()は、引数をそのままコンパイラに渡すため、シェルやコンパイラの引数解析ルールに依存します。スペースや特殊文字(例: "\)が含まれる場合、適切にエスケープしないと問題が発生します。

トラブルシューティング/解決策

  • 推奨される代替

    • target_compile_definitions()またはadd_compile_definitions()を使用する。 これらのコマンドは、定義の値を自動的に適切にエスケープしてくれます。

      add_compile_definitions(MESSAGE="Hello World") # -DMESSAGE="Hello World" となる
      # または
      target_compile_definitions(MyTarget PRIVATE MESSAGE="Hello World")
      
    • configure_file()を使用してヘッダファイルを生成する。 これが最も安全で推奨される方法です。CMake変数をC/C++の#defineに変換したヘッダファイルを生成し、それをソースコードでインクルードします。

      CMakeLists.txt

      set(MY_VERSION_STRING "1.0.0")
      set(IS_DEBUG_BUILD ON) # 真偽値も設定可能
      
      # config.h.in テンプレートを config.h に生成
      configure_file(
          "${CMAKE_CURRENT_SOURCE_DIR}/config.h.in"
          "${CMAKE_CURRENT_BINARY_DIR}/config.h"
          @ONLY
      )
      
      # 生成されたヘッダファイルへのインクルードパスを追加
      include_directories("${CMAKE_CURRENT_BINARY_DIR}")
      

      config.h.in (テンプレートファイル)

      #pragma once
      #define MY_VERSION_STRING "@MY_VERSION_STRING@"
      #cmakedefine IS_DEBUG_BUILD
      

      @VARIABLE@はCMake変数で置き換えられ、#cmakedefineはCMake変数がONの場合に#defineに、OFFの場合にコメントアウトされます。

      main.cpp

      #include "config.h" // 生成されたヘッダファイルをインクルード
      
      #include <iostream>
      
      int main() {
          std::cout << "Version: " << MY_VERSION_STRING << std::endl;
          #ifdef IS_DEBUG_BUILD
              std::cout << "Debug build enabled." << std::endl;
          #else
              std::cout << "Release build." << std::endl;
          #endif
          return 0;
      }
      

      この方法は、CMakeの変数をC/C++コードに渡すための最も堅牢な方法です。

  • 引用符とエスケープ
    文字列全体を二重引用符で囲み、さらに内部の二重引用符やバックスラッシュをエスケープする必要があります。これは非常に煩雑で間違いやすいです。

    # スペースを含む文字列の例(非常に複雑)
    add_definitions(-DMESSAGE="\\\"Hello World\\\"")
    

    上記の場合、C++コードではconst char* msg = MESSAGE; とすると、"Hello World" という文字列リテラルとして展開されることを期待しますが、エスケープの組み合わせが複雑でデバッグが困難です。

add_definitions()がスクリプトモードで動作しない

エラー/症状

  • cmake -P script.cmakeのようにスクリプトモードでCMakeを実行した際に、add_definitions()コマンドでエラーが発生する。 CMake Error: Command add_definitions() is not scriptable

原因
add_definitions()(およびターゲットやビルドシステム生成に関連する多くのコマンド)は、CMakeのスクリプトモード(-Pオプション)では使用できません。スクリプトモードは、CMakeの実行環境内で一般的なスクリプトを実行するために設計されており、ビルドシステムの生成には関与しません。

トラブルシューティング/解決策

  • もしビルドシステムを生成したいのであれば、通常のCMake実行フロー(cmake . -> cmake --build .)を使用してください。
  • スクリプトモードでプリプロセッサ定義が必要な場合は、CMake変数として定義し、それらを直接利用するか、configure_file()のようなスクリプトモードで動作するコマンドを使用してファイルに出力することを検討してください。

定義の変更がビルドに反映されない/不完全な再コンパイル

エラー/症状

  • 一部のファイルは再コンパイルされるが、他のファイルはされないため、ビルドエラーやリンクエラーが発生する。
  • add_definitions()で定義を変更したが、再ビルドしてもC/C++コードに変更が反映されない。

原因
ビルドシステム(MakefilesやNinjaなど)は、ファイルの依存関係を追跡して、必要なファイルのみを再コンパイルします。しかし、add_definitions()のように広範なスコープで定義を変更した場合、ビルドシステムがすべての関連する依存関係を正確に認識できないことがあります。特に、定義がヘッダファイル内で使用されている場合や、条件付きコンパイルに影響する場合に発生しやすいです。

トラブルシューティング/解決策

  • やはりtarget_compile_definitions()configure_file()の使用を検討する。 これらの方法は、CMakeがビルドシステムに依存関係をより正確に伝えるのに役立ち、不必要な再コンパイルや不完全な再コンパイルのリスクを減らします。特にconfigure_file()で生成されたヘッダファイルをインクルードする方式は、そのヘッダが変更されると、それをインクルードしているソースファイルが確実に再コンパイルされるため、堅牢性が高いです。
  • 依存関係の確認
    C/C++ソースコード内のプリプロセッサ定義が、適切にヘッダファイルを通じて伝播されているか確認してください。
  • クリーンビルド
    最も単純な解決策は、ビルドディレクトリを完全に削除し、最初からビルドし直す(クリーンビルド)ことです。これにより、すべてのファイルが新しい定義で再コンパイルされます。

add_definitions()は古く、多くの潜在的な問題(特にスコープの広さやエスケープの複雑さ)を抱えています。新しいCMakeプロジェクトでは使用を避けるべきです。

既存のプロジェクトでadd_definitions()に問題が発生している場合は、以下の順で検討してください。

  1. 定義のスコープを限定したい場合
    target_compile_definitions()に置き換える。
  2. 文字列などの複雑な値を定義したい場合、または設定ファイルをコードと同期させたい場合
    configure_file()とヘッダファイル生成のパターンに移行する。
  3. 一般的なコンパイラオプションを追加したい場合
    add_compile_options()またはtarget_compile_options()を使用する。


重要な注意点として、add_definitions()は現在非推奨(deprecated)のコマンドです。 新しいプロジェクトでは使用せず、代わりにadd_compile_definitions()target_compile_definitions()を使用することが強く推奨されます。

しかし、既存のプロジェクトを理解するため、またはadd_definitions()の動作を学ぶために、その使用例と、推奨される代替コマンドの例を比較しながら説明します。

例1: 基本的なプリプロセッサ定義の追加

この例では、デバッグビルドであることを示す定義を追加します。

プロジェクト構成

.
├── CMakeLists.txt
└── main.cpp

CMakeLists.txt (非推奨のadd_definitions()を使用)

cmake_minimum_required(VERSION 3.10)
project(MyLegacyApp CXX)

# add_definitions() を使用してプリプロセッサ定義を追加
# -D は自動的に付与されるわけではないので、手動で指定します。
add_definitions(-DDEBUG_BUILD)

add_executable(my_app main.cpp)

main.cpp

#include <iostream>

int main() {
    #ifdef DEBUG_BUILD
        std::cout << "This is a DEBUG build!" << std::endl;
    #else
        std::cout << "This is a RELEASE build." << std::endl;
    #endif

    return 0;
}

ビルドと実行

mkdir build
cd build
cmake ..
make
./my_app

出力

This is a DEBUG build!

例2: 値を持つプリプロセッサ定義の追加

この例では、バージョン番号や文字列のような値を持つ定義を追加します。

CMakeLists.txt (非推奨のadd_definitions()を使用)

cmake_minimum_required(VERSION 3.10)
project(MyLegacyAppWithVersion CXX)

# 値を持つ定義を追加する場合、スペースや特殊文字に注意が必要。
# 通常は文字列全体を引用符で囲み、内部の引用符はエスケープする必要がある。
# これは非常に間違いやすい。
add_definitions(-DAPP_VERSION="1.0.0" -DAPP_NAME="\"My Application\"")

add_executable(my_app main.cpp)

main.cpp

#include <iostream>

int main() {
    std::cout << "Application Name: " << APP_NAME << std::endl;
    std::cout << "Version: " << APP_VERSION << std::endl;

    return 0;
}

ビルドと実行
上記と同様にビルドして実行します。

出力

Application Name: "My Application"
Version: 1.0.0

問題点
APP_NAMEの出力に余分な引用符が含まれていることに注意してください。これはadd_definitions()"をそのまま文字列リテラルとしてコンパイラに渡すためです。C/C++ソースコードで期待される"My Application"という文字列リテラルになるためには、さらに複雑なエスケープ(\"My Application\"など)が必要になる場合があります。これがadd_definitions()の使いにくさの一因です。

add_compile_definitions() (add_definitions()の直接的な後継)

add_definitions()と同様に、カレントディレクトリとそのサブディレクトリのすべてのターゲットに適用されますが、より現代的な構文で、-Dを記述する必要がありません。また、文字列のエスケープも自動的に処理されます。

CMakeLists.txt (推奨されるadd_compile_definitions()を使用)

cmake_minimum_required(VERSION 3.12) # add_compile_definitionsはCMake 3.12以降で推奨
project(MyModernApp CXX)

# -D は不要。値を持つ場合はイコール記号で指定。
# 文字列は自動的にエスケープされるため、手動でのエスケープは不要。
add_compile_definitions(DEBUG_BUILD APP_VERSION="1.0.0" APP_NAME="My Application")

add_executable(my_app main.cpp)

main.cpp

#include <iostream>

int main() {
    #ifdef DEBUG_BUILD
        std::cout << "This is a DEBUG build!" << std::endl;
    #endif
    std::cout << "Application Name: " << APP_NAME << std::endl; // "My Application" となる
    std::cout << "Version: " << APP_VERSION << std::endl; // "1.0.0" となる

    return 0;
}

出力

This is a DEBUG build!
Application Name: My Application
Version: 1.0.0

APP_NAMEが期待通りに二重引用符なしで出力されている点に注目してください。

target_compile_definitions() (特定のターゲットに限定する場合に最適)

これが最も柔軟で推奨される方法です。定義を特定のターゲット(ライブラリや実行ファイル)にのみ適用できます。

CMakeLists.txt (推奨されるtarget_compile_definitions()を使用)

cmake_minimum_required(VERSION 3.12)
project(MySpecificApp CXX)

add_executable(my_app main.cpp)

# my_app ターゲットにのみ DEBUG_BUILD を定義
# PRIVATE: このターゲット自身のコンパイルにのみ定義が適用される
target_compile_definitions(my_app PRIVATE DEBUG_BUILD)

add_library(my_lib STATIC lib.cpp)
# my_lib ターゲットには DEBUG_BUILD は定義されない
# my_lib ターゲットに MY_LIB_FEATURE を定義
target_compile_definitions(my_lib PRIVATE MY_LIB_FEATURE)

main.cpp

#include <iostream>

int main() {
    #ifdef DEBUG_BUILD
        std::cout << "main.cpp: This is a DEBUG build for my_app!" << std::endl;
    #else
        std::cout << "main.cpp: DEBUG_BUILD is NOT defined." << std::endl;
    #endif

    // MY_LIB_FEATURE は main.cpp では定義されていないはず
    #ifdef MY_LIB_FEATURE
        std::cout << "main.cpp: MY_LIB_FEATURE is defined." << std::endl;
    #else
        std::cout << "main.cpp: MY_LIB_FEATURE is NOT defined." << std::endl;
    #endif

    return 0;
}

lib.cpp

#include <iostream>

// このファイルは my_lib ターゲットの一部
void lib_function() {
    #ifdef DEBUG_BUILD
        std::cout << "lib.cpp: DEBUG_BUILD is defined. (Should not happen if target_compile_definitions PRIVATE is used correctly)" << std::endl;
    #else
        std::cout << "lib.cpp: DEBUG_BUILD is NOT defined." << std::endl;
    #endif

    #ifdef MY_LIB_FEATURE
        std::cout << "lib.cpp: MY_LIB_FEATURE is defined for my_lib!" << std::endl;
    #else
        std::cout << "lib.cpp: MY_LIB_FEATURE is NOT defined." << std::endl;
    #endif
}

lib.cppmain.cppから呼び出されるか、別途main.cppにインクルードされる必要がありますが、ここでは説明の簡略化のため、それぞれのコンパイル時の定義の有無に焦点を当てています。)

ビルドと実行

mkdir build
cd build
cmake ..
make
./my_app

my_appを実行した際の出力例 (lib_functionが呼び出されない場合)

main.cpp: This is a DEBUG build for my_app!
main.cpp: MY_LIB_FEATURE is NOT defined.

lib.cppのコンパイル時に適用される定義
my_libをビルドする際には、MY_LIB_FEATUREが定義され、DEBUG_BUILDは定義されません。

configure_file() (最も堅牢な方法)

CMake変数からC/C++ヘッダファイルを生成し、その中に定義を書き込む方法です。値が複雑な場合や、ビルド設定をソースコードに反映させたい場合に最適です。

プロジェクト構成

.
├── CMakeLists.txt
├── main.cpp
└── config.h.in  <-- テンプレートファイル

CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(MyConfigApp CXX)

# CMake変数を定義
set(APP_VERSION_MAJOR 1)
set(APP_VERSION_MINOR 2)
set(BUILD_TYPE_DEBUG TRUE) # 真偽値も設定可能

# config.h.in テンプレートから config.h をビルドディレクトリに生成
configure_file(
    "${CMAKE_CURRENT_SOURCE_DIR}/config.h.in"  # 入力ファイル
    "${CMAKE_CURRENT_BINARY_DIR}/config.h"    # 出力ファイル
    @ONLY # @VAR@ 形式の変数のみを置換
)

# 生成されたヘッダファイルへのインクルードパスを追加
include_directories("${CMAKE_CURRENT_BINARY_DIR}")

add_executable(my_app main.cpp)

config.h.in (テンプレートファイル)

#pragma once

#define APP_VERSION_MAJOR @APP_VERSION_MAJOR@
#define APP_VERSION_MINOR @APP_VERSION_MINOR@

// CMake変数がTRUEの場合に #define を生成、それ以外の場合はコメントアウト
#cmakedefine BUILD_TYPE_DEBUG

main.cpp

#include "config.h" // 生成されたヘッダファイルをインクルード

#include <iostream>

int main() {
    std::cout << "Application Version: " << APP_VERSION_MAJOR << "." << APP_VERSION_MINOR << std::endl;

    #ifdef BUILD_TYPE_DEBUG
        std::cout << "This is a debug build configured via header file." << std::endl;
    #else
        std::cout << "This is a release build configured via header file." << std::endl;
    #endif

    return 0;
}

ビルドと実行

mkdir build
cd build
cmake ..
make
./my_app
Application Version: 1.2
This is a debug build configured via header file.


add_definitions()は非推奨(deprecated)であり、ほとんどの新しいプロジェクトでは使用すべきではありません。その代わりに、より明確な目的と優れたスコープ管理を提供する、いくつかの代替コマンドが提供されています。

主な代替方法は以下の3つです。

  1. add_compile_definitions(): プリプロセッサ定義を追加するための直接的な代替。
  2. target_compile_definitions(): 特定のターゲットに対してプリプロセッサ定義を追加する場合。最も推奨される方法。

これらの方法について、それぞれの特徴と使用例を説明します。

add_compile_definitions()

これはadd_definitions()の最も直接的な後継コマンドです。add_definitions()と同様に、このコマンドが呼び出されたCMakeLists.txtファイルとそのサブディレクトリにあるすべてのターゲットにプリプロセッサ定義が適用されます。

特徴

  • スコープはadd_definitions()と同様に広範。
  • 文字列などの値のエスケープが自動的に処理されるため、add_definitions()での複雑なエスケープが不要。
  • -Dプレフィックスが不要。
  • add_definitions()よりも目的が明確(コンパイル定義の追加)。

使用例

cmake_minimum_required(VERSION 3.12) # add_compile_definitionsは3.12で導入された推奨コマンド
project(MyProj CXX)

# このディレクトリとそのサブディレクトリのすべてのターゲットにDEBUG_MODEとAPP_NAMEが定義される
add_compile_definitions(DEBUG_MODE APP_NAME="My Awesome App")

add_executable(my_app main.cpp)
add_library(my_lib STATIC lib.cpp) # my_libにもこれらの定義が適用される

main.cpp

#include <iostream>

int main() {
    #ifdef DEBUG_MODE
        std::cout << "DEBUG_MODE is enabled." << std::endl;
    #endif
    std::cout << "Application Name: " << APP_NAME << std::endl;
    return 0;
}

lib.cpp

#include <iostream>

void do_something_in_lib() {
    #ifdef DEBUG_MODE
        std::cout << "Lib: DEBUG_MODE is enabled." << std::endl;
    #else
        std::cout << "Lib: DEBUG_MODE is disabled." << std::endl;
    #endif
}

利点
add_definitions()からの移行が容易。

欠点
スコープが広いため、特定のターゲットにのみ定義を適用したい場合には不向き。プロジェクトが大きくなると、意図しない定義の衝突や予期せぬ再コンパイルを引き起こす可能性がある。

target_compile_definitions()

これが、特定のターゲットに対してのみプリプロセッサ定義を追加するための最も推奨される方法です。定義の適用範囲を厳密に制御できるため、大規模なプロジェクトでのモジュール性と依存関係の管理が向上します。

特徴

  • 文字列などの値のエスケープが自動的に処理される。
  • -Dプレフィックスは不要。
  • PRIVATE, PUBLIC, INTERFACEキーワードを使用して、定義の伝播方法を制御できる。
    • PRIVATE: 定義はターゲット自身のコンパイルにのみ適用される。
    • INTERFACE: 定義はターゲットを使用する他のターゲットにのみ適用される(ターゲット自身のコンパイルには適用されない)。
    • PUBLIC: 定義はターゲット自身のコンパイルにも、ターゲットを使用する他のターゲットにも適用される。
  • 定義を特定のターゲット(実行ファイル、ライブラリ)に限定して適用できる。

使用例

cmake_minimum_required(VERSION 3.12)
project(MyScopedApp CXX)

# 実行ファイル
add_executable(my_app main.cpp)
# my_appにのみDEBUG_APP定義を適用
target_compile_definitions(my_app PRIVATE DEBUG_APP VERSION="1.0.0")

# 静的ライブラリ
add_library(my_static_lib STATIC static_lib.cpp)
# my_static_libにのみFEATURE_X定義を適用
target_compile_definitions(my_static_lib PRIVATE FEATURE_X)

# 共有ライブラリ
add_library(my_shared_lib SHARED shared_lib.cpp)
# my_shared_libをリンクするターゲットにFEATURE_Y定義を伝播させる
target_compile_definitions(my_shared_lib PUBLIC FEATURE_Y)

main.cpp

#include <iostream>

int main() {
    #ifdef DEBUG_APP
        std::cout << "my_app: DEBUG_APP is enabled." << std::endl;
    #endif
    std::cout << "my_app: Version = " << VERSION << std::endl; // my_appにのみ定義される

    // FEATURE_X (my_static_lib private) は定義されていない
    #ifdef FEATURE_X
        std::cout << "my_app: FEATURE_X is defined unexpectedly!" << std::endl;
    #else
        std::cout << "my_app: FEATURE_X is NOT defined (as expected)." << std::endl;
    #endif

    // FEATURE_Y (my_shared_lib public) は定義されているはず (my_appがmy_shared_libをリンクする場合)
    #ifdef FEATURE_Y
        std::cout << "my_app: FEATURE_Y is defined (due to PUBLIC on my_shared_lib)." << std::endl;
    #else
        std::cout << "my_app: FEATURE_Y is NOT defined (if not linked to my_shared_lib)." << std::endl;
    #endif

    return 0;
}

static_lib.cpp

#include <iostream>

void static_lib_function() {
    #ifdef FEATURE_X
        std::cout << "static_lib: FEATURE_X is enabled." << std::endl; // static_libにのみ定義される
    #else
        std::cout << "static_lib: FEATURE_X is NOT defined." << std::endl;
    #endif

    #ifdef DEBUG_APP
        std::cout << "static_lib: DEBUG_APP is defined unexpectedly!" << std::endl; // my_appのPRIVATEなので定義されない
    #else
        std::cout << "static_lib: DEBUG_APP is NOT defined (as expected)." << std::endl;
    #endif
}

shared_lib.cpp

#include <iostream>

void shared_lib_function() {
    #ifdef FEATURE_Y
        std::cout << "shared_lib: FEATURE_Y is enabled." << std::endl; // shared_lib自身にも定義される
    #else
        std::cout << "shared_lib: FEATURE_Y is NOT defined." << std::endl;
    #endif
}

利点
最もきめ細やかな制御が可能で、大規模なプロジェクトでの依存関係管理が大幅に改善される。コードベースのモジュール性が向上する。

欠点
複数のターゲットに共通の定義を適用したい場合、それぞれのtarget_compile_definitions()呼び出しが必要になる(ただし、これはtarget_link_libraries()などと同様の設計思想であり、明示的であることの利点が多い)。

configure_file()

特徴

  • 生成されたヘッダファイルをインクルードするだけで、その変更がソースファイルに伝わり、必要な再コンパイルが確実に実行されるため、ビルドシステムとの連携が非常に堅牢。
  • 複雑な文字列やパス、数値など、あらゆる種類の情報を安全に渡せる。
  • #cmakedefineディレクティブを使用すると、CMake変数がTRUEの場合は#defineを、それ以外の場合はコメントアウトを生成できる。
  • CMake変数の値をC/C++の#defineや他の構文でヘッダファイルに書き込む。

使用例

プロジェクト構成

.
├── CMakeLists.txt
├── main.cpp
└── config.h.in  # テンプレートファイル

CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(MyConfigApp CXX)

# CMake変数を定義
set(APP_VERSION_MAJOR 1)
set(APP_VERSION_MINOR 2)
set(BUILD_TYPE_DEBUG TRUE) # ON/OFFで #cmakedefine の挙動が変わる

# config.h.in をテンプレートとして、ビルドディレクトリに config.h を生成
configure_file(
    "${CMAKE_CURRENT_SOURCE_DIR}/config.h.in"  # 入力テンプレートファイル
    "${CMAKE_CURRENT_BINARY_DIR}/config.h"    # 出力ファイル
    @ONLY # `@VAR@`形式の変数のみを置換する(推奨)
)

# 生成されたヘッダファイルへのインクルードパスを追加
# これにより、main.cppがconfig.hを見つけられるようになる
include_directories("${CMAKE_CURRENT_BINARY_DIR}")

add_executable(my_app main.cpp)

config.h.in (テンプレートファイル)

#pragma once

// CMake変数APP_VERSION_MAJORの値を #define に変換
#define APP_VERSION_MAJOR @APP_VERSION_MAJOR@
#define APP_VERSION_MINOR @APP_VERSION_MINOR@

// CMake変数BUILD_TYPE_DEBUGがTRUEの場合に #define BUILD_TYPE_DEBUG を生成
// それ以外の場合は // #undef BUILD_TYPE_DEBUG または空行にコメントアウト
#cmakedefine BUILD_TYPE_DEBUG

// CMake変数の文字列を直接埋め込む場合 (文字列リテラルとして)
#define APP_NAME "@PROJECT_NAME@" // PROJECT_NAMEはCMakeが自動定義する変数

// その他の情報(例: ビルド日時、Gitコミットハッシュなど)
// CMakeのexecute_process()などと組み合わせて動的に情報を取得し、configure_file()で埋め込むことも可能

main.cpp

#include "config.h" // 生成されたヘッダファイルをインクルード

#include <iostream>

int main() {
    std::cout << "Application: " << APP_NAME << std::endl;
    std::cout << "Version: " << APP_VERSION_MAJOR << "." << APP_VERSION_MINOR << std::endl;

    #ifdef BUILD_TYPE_DEBUG
        std::cout << "This is a DEBUG build." << std::endl;
    #else
        std::cout << "This is a RELEASE build." << std::endl;
    #endif

    return 0;
}

利点

  • C/C++以外の言語の設定ファイル(例: Pythonスクリプトに設定を書き込む)の生成にも応用可能。
  • 生成されたヘッダファイルが変更されると、それをインクルードしているすべてのソースファイルが再コンパイルされるため、依存関係が確実に追跡される。
  • バージョン情報、ビルド設定、Gitハッシュなど、ビルド時に動的に生成される情報をコードに埋め込むのに非常に適している。
  • 最も堅牢で安全な方法。文字列のエスケープや複雑な値の渡し方を気にする必要がない。
  • テンプレートファイル(.inファイル)の管理が必要になる。
  • シンプルなプリプロセッサ定義を追加するだけの場合には、やや大げさな手順になる可能性がある。
  • バージョン情報、ビルド設定、あるいは複雑な文字列など、CMakeの情報をソースコードに埋め込みたい場合
    configure_file()
  • 特定のターゲットにのみ定義を適用したい場合(最も一般的なケース)
    target_compile_definitions()PRIVATEがデフォルトで推奨されることが多いです。
  • 単一のディレクトリ内のすべてのターゲットに簡単な定義を適用したい場合
    add_compile_definitions()。ただし、このシナリオでもtarget_compile_definitions()を使用する方が、将来的な拡張性や明確さの点で優れていることが多いです。