Conditional Compilation Without Source Clutter Using CMake Generator Expressions
CMake generator expressions keep platform- and configuration-specific compiler flags in the build system instead of #ifdef blocks in source, with a worked target_compile_options example and a compile_commands.json check.
23 Jun 2026, 14:15 UTC

Platform flags belong in the build, not in the source
If your C++ headers are full of #ifdef _WIN32 just to pick a compiler flag, build logic has leaked into the code. CMake generator expressions let you keep that decision in CMakeLists.txt: they are evaluated when CMake generates the build system (Makefiles, Ninja, Visual Studio projects), not when your source is compiled.
What a generator expression is
A generator expression is a string of the form $<...> that CMake resolves late — after configuration, while writing build files. Because resolution happens per target, per configuration and per platform, one CMakeLists.txt can describe Debug and Release, or Linux and Windows, without a single preprocessor branch in your code.
Two building blocks cover most cases: $<PLATFORM:UNIX> is true when the target system name is UNIX, and $<CONFIG:Debug> is true for the Debug configuration. $<AND:...> combines conditions, and the ternary form $<condition:value-if-true> yields the value only when the condition holds.
Worked example: a warning flag only for Unix Debug builds
Assume CMake 3.15 or newer and a single-config generator such as Ninja or Unix Makefiles. In the project's CMakeLists.txt, after the target exists:
add_executable(MyTarget main.cpp)
target_compile_options(MyTarget PRIVATE
$<$<AND:$<PLATFORM:UNIX>,$<CONFIG:Debug>>:-Wno-deprecated-declarations>
)Run configuration from the project root: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug. No elevated permissions are needed; you only need write access to the build directory. The flag reaches the compiler only when both conditions hold, so an MSVC build on Windows never sees a GCC-style option.
Expected check: the option appears in the compile command for MyTarget in Debug on Unix, and is absent in Release or in any Windows configuration.
Verifying without guessing
Rather than reading generated Makefiles, export the compile database and inspect the real command line:
- Configure with
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON(supported by the Makefile and Ninja generators). - Open
build/compile_commands.jsonand find the entry formain.cpp. - Confirm the flag appears in the
commandfield for the Debug build, then reconfigure with-DCMAKE_BUILD_TYPE=Releaseand confirm it is gone.
If the flag never appears, check whether a multi-config generator is in use. With Visual Studio or Ninja Multi-Config, CMAKE_BUILD_TYPE is ignored and the configuration is chosen at build time with cmake --build build --config Debug.
Trade-offs and limits
Generator expressions are not free. A deeply nested expression is harder to read than a plain if() block, and mistakes surface late — at generate time rather than configure time — so a typo can produce a confusing message. Keep expressions short, state the intent in a comment, and prefer one expression per target property over a global add_compile_options that leaks into every target.
They also cannot do everything: they cannot create targets or read files, and they are not a substitute for a toolchain file when the compiler itself differs. Use a toolchain file for the toolchain, and generator expressions for the per-target, per-configuration details.
Start with one flag on one target, verify it in compile_commands.json, and only then generalise.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.