CMake プログラミング:target_compile_definitions() の使い方と注意点

2025-05-31

基本的な役割

このコマンドを使うと、C/C++ コンパイラに対して -D オプションを通じてマクロ定義を渡すことができます。これは、ソースコード内で #ifdef#ifndef などのプリプロセッサディレクティブを使って、特定の条件に基づいてコードのコンパイルや実行を制御するために非常に役立ちます。

構文

target_compile_definitions(<target> <INTERFACE|PUBLIC|PRIVATE> <definition1> [<definition2> ...])
  • <definition1> [<definition2> ...]: 設定したいプリプロセッサの定義です。これらは通常、NAME または NAME=value の形式で指定します。
  • <INTERFACE|PUBLIC|PRIVATE>: 定義のスコープを指定します。
    • INTERFACE: このターゲットを使用する他のターゲットに対して、この定義が公開されます。つまり、このターゲットをリンクする他のターゲットもこの定義を利用できます。ただし、このターゲット自身のコンパイルには影響しません。
    • PUBLIC: このターゲット自身のコンパイルと、このターゲットを使用する他のターゲットの両方に対して、この定義が適用されます。
    • PRIVATE: このターゲット自身のコンパイルに対してのみ、この定義が適用されます。他のターゲットには影響しません。
  • <target>: 定義を適用するターゲットの名前(例えば、my_executablemy_library)。

具体的な例

例えば、デバッグモードとリリースモードで異なる動作をさせたい場合、次のように CMakeLists.txt に記述できます。

add_executable(my_app main.cpp)

if(CMAKE_BUILD_TYPE EQUAL Debug)
  target_compile_definitions(my_app PRIVATE DEBUG_MODE)
elseif(CMAKE_BUILD_TYPE EQUAL Release)
  target_compile_definitions(my_app PRIVATE RELEASE_MODE)
endif()

この例では、ビルドタイプが Debug の場合、my_app ターゲットのコンパイル時に DEBUG_MODE というプリプロセッサ定義が設定されます。ビルドタイプが Release の場合は、RELEASE_MODE が設定されます。

そして、main.cpp の中では、これらの定義を使って条件付きコンパイルを行うことができます。

#include <iostream>

int main() {
#ifdef DEBUG_MODE
  std::cout << "デバッグモードで実行中" << std::endl;
#elif defined(RELEASE_MODE)
  std::cout << "リリースモードで実行中" << std::endl;
#endif
  return 0;
}
  • ライブラリのバージョン情報を定義する。
  • 特定の機能の有効/無効を切り替えるためのフラグを定義する。
  • プラットフォーム固有のコードを切り替えるために、プラットフォーム名(例えば、WIN32LINUX)を定義する。


スコープ (INTERFACE, PUBLIC, PRIVATE) の誤解

  • トラブルシューティング
    • 定義がどの範囲で必要なのかを明確にしましょう。
    • ライブラリ内部のみであれば PRIVATE、ライブラリ自身とその利用者であれば PUBLIC、ライブラリの利用者のみであれば INTERFACE を選択します。
    • CMake の設定を再度確認し、スコープ指定が正しいか見直しましょう。
    • 実際にビルドして、プリプロセッサ定義が意図した通りに設定されているか(例えば、コンパイラのコマンドラインオプションを確認するなど)を検証します。
  • 原因
    スコープの指定が不適切である可能性があります。例えば、あるライブラリ内部でのみ使用したい定義を PUBLIC にしてしまい、意図せずリンク先の実行ファイルにも影響を与えてしまう、あるいはその逆のケースです。
  • エラー
    定義が意図したターゲットや、そのターゲットを使用する他のターゲットに伝播しない。

定義名の衝突

  • トラブルシューティング
    • 定義名には、ターゲット固有のプレフィックスやサフィックスを付けるなどして、名前空間を意識しましょう(例: MYLIB_VERSIONAPP_DEBUG_MODE)。
    • CMake の変数 (CMAKE_BUILD_TYPE など) を活用して、条件付きで定義を設定することも有効です。
    • ビルドログを確認し、どのターゲットでどのような定義がされているかを把握します。
  • 原因
    特に複数のライブラリや実行可能ファイルが絡むプロジェクトで、グローバルな意味合いを持つような定義名(例えば、DEBUGVERSION など)を安易に使うと衝突する可能性があります。
  • エラー
    異なるターゲットで同じ名前の定義を行い、意図しない挙動を引き起こす。

定義の値の扱い

  • トラブルシューティング
    • 文字列型の値を定義する場合は、必ずクォートで囲みましょう(例: VERSION="1.2.3")。
    • CMake の構文エラーメッセージを注意深く確認します。
    • コンパイラのコマンドラインオプションを確認し、定義された値が正しく渡っているか確認します。
  • 原因
    NAME=value の形式で定義する場合、value が文字列を含む場合は通常クォートで囲む必要があります。
  • エラー
    文字列型の値を定義する際に、クォート(")で囲むのを忘れると、CMake の構文エラーになったり、コンパイラに正しく値が渡らなかったりする。

条件分岐との組み合わせの誤り

  • トラブルシューティング
    • message() コマンドを使って、条件分岐で評価している変数の値を確認しましょう。
    • 条件式のロジックが正しいか再検討します。
    • 意図したビルドタイプやオプションで CMake を実行しているか確認します。
  • 原因
    CMake の変数の値や条件式の評価が期待通りになっていない可能性があります。
  • エラー
    if() などの条件分岐と組み合わせて target_compile_definitions() を使用する際に、条件が意図通りに評価されず、定義が設定されない。

キャッシュの残存

  • トラブルシューティング
    • CMake のキャッシュをクリアしてみましょう。これには、ビルドディレクトリを削除して再度 CMake を実行する方法や、CMake GUI ツールでキャッシュをクリアする方法があります。
    • CMake を実行する際に、-DCMAKE_BUILD_TYPE=... などのオプションを明示的に指定することで、キャッシュの影響を避けることができる場合があります。
  • 原因
    CMake は設定情報をキャッシュしており、変更がすぐに反映されないことがあります。
  • エラー
    CMakeLists.txt を修正しても、以前の設定がキャッシュに残っていて、意図した定義が反映されない。
  • トラブルシューティング
    • CMakeLists.txt を注意深く見直し、スペルミスがないか確認しましょう。
    • IDE の補完機能などを活用すると、スペルミスを防ぐのに役立ちます。
  • エラー
    定義名やスコープ指定のスペルを間違えると、CMake はエラーを出さずに無視したり、意図しない動作をしたりする可能性があります。


例1: デバッグモードとリリースモードの切り替え

これは先ほども触れましたが、基本的な使用例として再度ご紹介します。

CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(DefineExample)

add_executable(my_app main.cpp)

if(CMAKE_BUILD_TYPE STREQUAL Debug)
  target_compile_definitions(my_app PRIVATE DEBUG_MODE)
elseif(CMAKE_BUILD_TYPE STREQUAL Release)
  target_compile_definitions(my_app PRIVATE RELEASE_MODE)
endif()

main.cpp

#include <iostream>

int main() {
#ifdef DEBUG_MODE
  std::cout << "[デバッグ] デバッグモードで実行中です。" << std::endl;
#elif defined(RELEASE_MODE)
  std::cout << "[リリース] リリースモードで実行中です。" << std::endl;
#else
  std::cout << "[不明] ビルドタイプが設定されていません。" << std::endl;
#endif
  return 0;
}

説明

  • PRIVATE なので、この定義は my_app のコンパイルにのみ影響し、もし他のターゲット(ライブラリなど)がこの実行ファイルをリンクしても、これらの定義は伝播しません。
  • main.cpp では、これらの定義が存在するかどうかを #ifdef#elif でチェックし、実行時に異なるメッセージを出力します。
  • CMakeLists.txt では、CMAKE_BUILD_TYPE 変数の値に応じて、my_app ターゲットに対して DEBUG_MODE または RELEASE_MODE というプリプロセッサ定義を PRIVATE スコープで設定しています。

例2: ライブラリのバージョン情報

ライブラリのバージョン情報をプリプロセッサ定義として設定し、ソースコード内で利用する例です。

CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(LibVersionExample)

add_library(my_lib my_lib.cpp my_lib.h)
target_compile_definitions(my_lib PUBLIC
  "MY_LIB_VERSION_MAJOR=${MY_LIB_VERSION_MAJOR}"
  "MY_LIB_VERSION_MINOR=${MY_LIB_VERSION_MINOR}"
  "MY_LIB_VERSION_PATCH=${MY_LIB_VERSION_PATCH}"
)

set(MY_LIB_VERSION_MAJOR 1)
set(MY_LIB_VERSION_MINOR 2)
set(MY_LIB_VERSION_PATCH 3)

add_executable(app main.cpp)
target_link_libraries(app my_lib)

my_lib.h

#ifndef MY_LIB_H
#define MY_LIB_H

#define LIB_VERSION_MAJOR MY_LIB_VERSION_MAJOR
#define LIB_VERSION_MINOR MY_LIB_VERSION_MINOR
#define LIB_VERSION_PATCH MY_LIB_VERSION_PATCH

#endif // MY_LIB_H

my_lib.cpp

// 特に内容なし

main.cpp

#include <iostream>
#include "my_lib.h"

int main() {
  std::cout << "ライブラリのバージョン: "
            << LIB_VERSION_MAJOR << "."
            << LIB_VERSION_MINOR << "."
            << LIB_VERSION_PATCH << std::endl;
  return 0;
}

説明

  • main.cpp では、これらのマクロを通じてライブラリのバージョン情報を取得し、出力しています。
  • my_lib.h では、これらのプリプロセッサ定義を使って、より使いやすいマクロ (LIB_VERSION_MAJOR など) を定義しています。
  • PUBLIC なので、my_lib をリンクする app 実行ファイルもこれらの定義を利用できます。
  • my_lib ライブラリに対して、PUBLIC スコープでバージョン情報をプリプロセッサ定義として設定しています。CMake の変数の値を展開して定義しています。

例3: プラットフォーム固有の定義 (INTERFACE)

ヘッダーファイルのみを提供するライブラリで、特定のプラットフォームでのみ必要な定義を公開する例です。

CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(PlatformDefineExample)

add_library(my_header_lib INTERFACE)

if(CMAKE_SYSTEM_NAME MATCHES "Linux")
  target_compile_definitions(my_header_lib INTERFACE LINUX_PLATFORM)
elseif(CMAKE_SYSTEM_NAME MATCHES "Windows")
  target_compile_definitions(my_header_lib INTERFACE WINDOWS_PLATFORM)
endif()

add_executable(app main.cpp)
target_link_libraries(app my_header_lib)

my_header.h

#ifndef MY_HEADER_H
#define MY_HEADER_H

#ifdef LINUX_PLATFORM
#define PLATFORM_NAME "Linux"
#elif defined(WINDOWS_PLATFORM)
#define PLATFORM_NAME "Windows"
#else
#define PLATFORM_NAME "Unknown"
#endif

#endif // MY_HEADER_H

main.cpp

#include <iostream>
#include "my_header.h"

int main() {
  std::cout << "現在のプラットフォーム: " << PLATFORM_NAME << std::endl;
  return 0;
}
  • main.cpp では、このマクロを使って現在のプラットフォーム名を出力します。
  • my_header.h では、これらの定義に基づいて PLATFORM_NAME マクロを定義しています。
  • INTERFACE なので、my_header_lib をリンクする app 実行ファイルは、これらの定義を受け継ぎます。
  • CMakeLists.txt では、実行されているシステムの名前 (CMAKE_SYSTEM_NAME) に基づいて、LINUX_PLATFORM または WINDOWS_PLATFORM というプリプロセッサ定義を INTERFACE スコープで設定しています。
  • my_header_libINTERFACE ライブラリであり、実体のあるコードを持ちません。ヘッダーファイル (my_header.h) のみを提供します。


add_definitions() コマンド (グローバルな定義)

  • 使用例
  • 欠点
    特定のターゲットにのみ適用したい定義もグローバルに影響を与えるため、スコープ管理が難しくなる可能性があります。ターゲットごとに細かく制御したい場合には不向きです。
  • 利点
    プロジェクト全体で共通の定義を簡単に設定できます。
  • 説明
    add_definitions() コマンドは、プロジェクト全体、またはそれ以降に定義されるすべてのターゲットに対してプリプロセッサ定義を追加します。
cmake_minimum_required(VERSION 3.10)
project(GlobalDefineExample)

add_definitions(-DGLOBAL_FEATURE_ENABLED)

add_executable(app1 main1.cpp)
add_executable(app2 main2.cpp)

# app1 と app2 の両方で GLOBAL_FEATURE_ENABLED が定義される

CMAKE_CXX_FLAGS などのコンパイラフラグ変数の利用

  • 使用例
  • 欠点
    スコープがグローバルになるか、ターゲットごとのプロパティ設定が必要になり、target_compile_definitions() ほど直感的ではありません。また、リンカフラグと混同しやすい点もあります。
  • 説明
    CMake には、コンパイラに渡すフラグを制御するための変数 (CMAKE_CXX_FLAGSCMAKE_C_FLAGSCMAKE_EXE_LINKER_FLAGSCMAKE_SHARED_LINKER_FLAGS など) があります。これらの変数に -D<definition> オプションを追加することで、プリプロセッサ定義を設定できます。
cmake_minimum_required(VERSION 3.10)
project(CompilerFlagsExample)

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DCOMPILER_FLAG_DEFINE")

add_executable(app main.cpp)

# app のコンパイル時に -DCOMPILER_FLAG_DEFINE が渡される

ターゲットごとに設定する場合は、set_target_properties() を使用します。

set_target_properties(app PROPERTIES
  CXX_FLAGS "${CMAKE_CXX_FLAGS} -DTARGET_SPECIFIC_DEFINE"
)

ヘッダーファイルでの定義 (#define)

  • 使用例 (main.cpp)
  • 欠点
    CMake のビルド設定とは分離しており、ビルド構成(Debug/Release など)に応じた切り替えや、複数のターゲット間での共有が難しくなります。
  • 利点
    最も直接的で、特定のソースファイル内でのみ有効な定義を作成できます。
  • 説明
    ソースコード内で直接 #define を使用してマクロを定義する方法です。
#include <iostream>

#define IN_SOURCE_DEFINE 1

int main() {
#ifdef IN_SOURCE_DEFINE
  std::cout << "ソースコード内で定義されています。" << std::endl;
#endif
  return 0;
}

設定ファイルやコマンドライン引数の利用

  • 使用例 (簡単なコマンドライン引数の例)
  • 欠点
    プリプロセッサによるコンパイル時の条件分岐はできなくなり、実行時のオーバーヘッドが発生する可能性があります。
  • 利点
    ビルド時に定義を固定せず、実行時の設定変更が容易になります。
  • 説明
    プリプロセッサ定義の代わりに、実行時に読み込む設定ファイル(JSON、XML、INI など)や、コマンドライン引数を通じてプログラムの動作を制御する方法です。
#include <iostream>
#include <string>

int main(int argc, char *argv[]) {
  std::string mode = "default";
  for (int i = 1; i < argc; ++i) {
    if (std::string(argv[i]) == "--debug") {
      mode = "debug";
    } else if (std::string(argv[i]) == "--release") {
      mode = "release";
    }
  }

  if (mode == "debug") {
    std::cout << "[実行時設定] デバッグモード" << std::endl;
  } else if (mode == "release") {
    std::cout << "[実行時設定] リリースモード" << std::endl;
  } else {
    std::cout << "[実行時設定] デフォルトモード" << std::endl;
  }
  return 0;
}

CMakeLists.txt は通常通り add_executable(app main.cpp) のように記述します。

CMake のオプション (option() コマンド)

  • 使用例 (CMakeLists.txt)
  • 欠点
    プリプロセッサ定義を直接設定するわけではありませんが、同様の条件付きコンパイルを実現できます。
  • 利点
    ユーザーがビルド構成をカスタマイズできます。
  • 説明
    option() コマンドを使うと、ユーザーが CMake の構成時にオン/オフを切り替えられるオプションを定義できます。これらのオプションの状態に基づいて、target_compile_definitions() を条件付きで設定することができます。
cmake_minimum_required(VERSION 3.10)
project(OptionExample)

option(ENABLE_FEATURE "Enable a specific feature" OFF)

add_executable(app main.cpp)

if(ENABLE_FEATURE)
  target_compile_definitions(app PRIVATE FEATURE_ENABLED)
endif()
#include <iostream>

int main() {
#ifdef FEATURE_ENABLED
  std::cout << "特定機能が有効です。" << std::endl;
#else
  std::cout << "特定機能は無効です。" << std::endl;
#endif
  return 0;
}