CMake プログラミング:target_compile_definitions() の使い方と注意点
基本的な役割
このコマンドを使うと、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_executable
やmy_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;
}
- ライブラリのバージョン情報を定義する。
- 特定の機能の有効/無効を切り替えるためのフラグを定義する。
- プラットフォーム固有のコードを切り替えるために、プラットフォーム名(例えば、
WIN32
、LINUX
)を定義する。
スコープ (INTERFACE, PUBLIC, PRIVATE) の誤解
- トラブルシューティング
- 定義がどの範囲で必要なのかを明確にしましょう。
- ライブラリ内部のみであれば
PRIVATE
、ライブラリ自身とその利用者であればPUBLIC
、ライブラリの利用者のみであればINTERFACE
を選択します。 - CMake の設定を再度確認し、スコープ指定が正しいか見直しましょう。
- 実際にビルドして、プリプロセッサ定義が意図した通りに設定されているか(例えば、コンパイラのコマンドラインオプションを確認するなど)を検証します。
- 原因
スコープの指定が不適切である可能性があります。例えば、あるライブラリ内部でのみ使用したい定義をPUBLIC
にしてしまい、意図せずリンク先の実行ファイルにも影響を与えてしまう、あるいはその逆のケースです。 - エラー
定義が意図したターゲットや、そのターゲットを使用する他のターゲットに伝播しない。
定義名の衝突
- トラブルシューティング
- 定義名には、ターゲット固有のプレフィックスやサフィックスを付けるなどして、名前空間を意識しましょう(例:
MYLIB_VERSION
、APP_DEBUG_MODE
)。 - CMake の変数 (
CMAKE_BUILD_TYPE
など) を活用して、条件付きで定義を設定することも有効です。 - ビルドログを確認し、どのターゲットでどのような定義がされているかを把握します。
- 定義名には、ターゲット固有のプレフィックスやサフィックスを付けるなどして、名前空間を意識しましょう(例:
- 原因
特に複数のライブラリや実行可能ファイルが絡むプロジェクトで、グローバルな意味合いを持つような定義名(例えば、DEBUG
、VERSION
など)を安易に使うと衝突する可能性があります。 - エラー
異なるターゲットで同じ名前の定義を行い、意図しない挙動を引き起こす。
定義の値の扱い
- トラブルシューティング
- 文字列型の値を定義する場合は、必ずクォートで囲みましょう(例:
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_lib
はINTERFACE
ライブラリであり、実体のあるコードを持ちません。ヘッダーファイル (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_FLAGS
、CMAKE_C_FLAGS
、CMAKE_EXE_LINKER_FLAGS
、CMAKE_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;
}