Question
In a project built with GCC, how can I:
- Configure and build each CMake build type, such as Debug and Release?
- Specify C and C++ compiler flags for Debug and Release builds in CMake?
- Build the main executable as C++ (using
g++) while building a nested library from C source files (usinggcc)?
Short Answer
You will learn how CMake models build configurations, how Debug and Release differ, and how to apply configuration-specific compiler options without mixing build outputs. You will also see how CMake automatically compiles C files with the C compiler and C++ files with the C++ compiler in the same project.
Concept
CMake separates a project’s build instructions from the build configuration used to produce binaries.
Common configurations include:
- Debug: designed for debugging. It normally includes debug symbols and little or no optimization.
- Release: designed for distribution and performance. It normally enables optimization and may omit debug information.
- RelWithDebInfo: optimized while retaining debug information.
- MinSizeRel: optimized for smaller binaries.
The exact default flags are compiler- and platform-dependent. With GCC-like toolchains, Debug commonly uses flags similar to -g, while Release commonly uses flags similar to -O3 and -DNDEBUG.
CMake has two generator families:
- Single-configuration generators, such as Unix Makefiles and Ninja. A build directory has one configuration, selected while configuring with
CMAKE_BUILD_TYPE. - Multi-configuration generators, such as Visual Studio, Xcode, and Ninja Multi-Config. One build directory can contain several configurations, selected when building with
--config.
For compiler selection, CMake works by language, not by assigning a compiler to each target manually:
.csource files are compiled as C, usually bygcc..cpp,.cc, and.cxxsource files are compiled as C++, usually byg++.
Enable both C and CXX in the project, then give each target source files of the appropriate language.
Mental Model
Think of a CMake build directory as a workshop.
With a single-configuration generator, the workshop is set up for one mode only: either Debug or Release. Trying to use the same workshop for both can leave old objects and settings behind, so create separate workshops (build directories).
A multi-configuration generator is like a workshop with separate labeled shelves for Debug and Release outputs. You choose the shelf when you build.
For C and C++, imagine CMake sorting documents by language:
- C documents (
.c) go to the C compiler. - C++ documents (
.cpp) go to the C++ compiler.
You tell CMake which languages the project supports; it routes each source file correctly.
Syntax and Examples
For a portable project, apply options to the target that needs them. Generator expressions let an option apply only to a particular configuration.
cmake_minimum_required(VERSION 3.20)
project(Example LANGUAGES C CXX)
add_library(mathlib STATIC
lib/math.c
)
add_executable(app
src/main.cpp
)
target_link_libraries(app PRIVATE mathlib)
# C++ options for the executable.
target_compile_options(app PRIVATE
$<$<AND:$<CONFIG:Debug>,$<COMPILE_LANGUAGE:CXX>>:-Wall;-Wextra;-g>
$<$<AND:$<CONFIG:Release>,$<COMPILE_LANGUAGE:CXX>>:-O2>
)
# C options for the C library.
target_compile_options(mathlib PRIVATE
$<$<AND:$<CONFIG:Debug>,$<COMPILE_LANGUAGE:C>>:-Wall;-Wextra;-g>
$<$<AND:$<CONFIG:Release>,$<COMPILE_LANGUAGE:C>>:-O2>
)
$<$<CONFIG:Debug>:...> means “use these options only in the Debug configuration.” The additional $<COMPILE_LANGUAGE:...> test makes the intended language explicit.
In many projects, you should not add -g or -O2 yourself unless you have a specific need: CMake’s default Debug and Release settings already choose appropriate baseline flags. Project-specific warnings and sanitizer flags are more common additions.
For simple, configuration-specific preprocessor definitions, use target_compile_definitions:
target_compile_definitions(app PRIVATE
$<$<CONFIG:Debug>:APP_ENABLE_DIAGNOSTICS=1>
$<$<CONFIG:Release>:APP_ENABLE_DIAGNOSTICS=0>
)
For single-configuration generators, configure separate directories:
cmake -S . -B build/debug -G Ninja -DCMAKE_BUILD_TYPE=Debug
cmake --build build/debug
cmake -S . -B build/release -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build/release
For a multi-configuration generator, configure once and choose the configuration at build time:
cmake -S . -B build -G "Ninja Multi-Config"
cmake --build build --config Debug
cmake --build build --config Release
Step by Step Execution
Consider this small project:
cmake_minimum_required(VERSION 3.20)
project(MixedProject LANGUAGES C CXX)
add_library(parser STATIC parser.c)
add_executable(tool main.cpp)
target_link_libraries(tool PRIVATE parser)
target_compile_definitions(tool PRIVATE
$<$<CONFIG:Debug>:TOOL_VERBOSE=1>
)
Build Debug with Ninja:
cmake -S . -B build/debug -G Ninja -DCMAKE_BUILD_TYPE=Debug
cmake --build build/debug
What happens:
project(... LANGUAGES C CXX)asks CMake to find and test both a C and C++ compiler.parser.cis recognized as C, so CMake creates a C compilation command, typically usinggcc.main.cppis recognized as C++, so CMake creates a C++ compilation command, typically usingg++.- Because the configuration is
Debug,TOOL_VERBOSE=1is added when compilingtool. - CMake builds
libparser.a, then links it intotool. - Since the final executable contains C++ code, CMake uses the appropriate C++ linker driver, typically
g++. This ensures the C++ standard library is linked correctly.
Then configure Release in a different directory:
cmake -S . -B build/release -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build/release
This produces an independent Release build without overwriting Debug object files.
Real World Use Cases
- Local debugging: Build Debug binaries with symbols so a debugger can show function names, source lines, and variables.
- Continuous integration: Run unit tests in Debug builds, often with additional warnings or sanitizers enabled.
- Production packaging: Build Release artifacts with optimization for deployment.
- Performance investigation: Use
RelWithDebInfoto profile optimized code while retaining useful stack traces. - Mixed-language projects: Use an existing C library for parsing, cryptography, or hardware access from a C++ application.
- Feature diagnostics: Enable extra logging in Debug using a configuration-specific compile definition.
Real Codebase Usage
Real CMake projects generally prefer target-based commands over modifying global flag variables.
Keep settings attached to targets
add_library(network network.cpp)
target_compile_features(network PUBLIC cxx_std_20)
target_compile_options(network PRIVATE -Wall -Wextra)
This avoids accidentally applying a library’s flags to unrelated targets.
Use configuration-specific behavior sparingly
target_compile_options(network PRIVATE
$<$<CONFIG:Debug>:-fsanitize=address>
)
target_link_options(network PRIVATE
$<$<CONFIG:Debug>:-fsanitize=address>
)
Some flags, such as sanitizers, must be present during both compilation and final linking.
Use CMake presets for repeatable builds
A CMakePresets.json file prevents developers and CI jobs from having to remember long commands:
{
"version": 3,
"configurePresets": [
{
"name": "debug",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/debug",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "release",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/release",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
}
]
}
Then run cmake --preset debug or cmake --preset release.
Let source extensions choose the language
add_library(c_api STATIC api.c)
add_executable(client main.cpp)
target_link_libraries(client PRIVATE c_api)
No gcc or g++ command needs to be written in CMakeLists.txt. CMake manages that based on the source files and enabled languages.
Common Mistakes
Setting CMAKE_BUILD_TYPE only when building
This does not select a configuration for a normal Ninja or Makefiles build:
# Incorrect for a single-configuration generator
cmake --build build -DCMAKE_BUILD_TYPE=Debug
Set it while configuring instead:
cmake -S . -B build/debug -DCMAKE_BUILD_TYPE=Debug
cmake --build build/debug
Reusing one single-config build directory for Debug and Release
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
Although CMake can reconfigure, separate directories are clearer and avoid confusion about cached options and artifacts. Prefer build/debug and build/release.
Assuming CMAKE_BUILD_TYPE works with every generator
For Visual Studio, Xcode, and Ninja Multi-Config, CMAKE_BUILD_TYPE is usually ignored. Select the configuration when building:
cmake --build build --config Release
Overwriting global flags
# Risky: can discard CMake's defaults and user-provided flags.
set(CMAKE_CXX_FLAGS "-O2")
Prefer target-level commands:
target_compile_options(app PRIVATE $<$<CONFIG:Release>:-O2>)
Trying to choose gcc for a target and g++ for another target
# Not the normal CMake model.
# set_target_compiler(app g++)
CMake selects compilation language from source files. Use .c for C and .cpp for C++, and enable both with project(... LANGUAGES C CXX).
Compiling C code as C++ by accident
Renaming a C source file from library.c to library.cpp changes language rules and may break valid C code. Keep the proper extension unless you intentionally want C++ compilation.
Adding optimization to Debug without considering debugging
High optimization levels can make variables disappear, reorder execution, and make stepping through code confusing. Use Debug defaults unless you specifically need an optimized debugging configuration such as RelWithDebInfo.
Comparisons
| Topic | Debug | Release |
|---|---|---|
| Main purpose | Diagnose and test code | Deploy and measure performance |
| Optimization | Usually low or disabled | Usually enabled |
| Debug symbols | Usually included | May be omitted or reduced |
| Typical experience | Easier stepping and inspection | Faster, but harder to debug |
| Build system type | How to select configuration |
|---|---|
| Single-config: Ninja, Unix Makefiles | Configure with -DCMAKE_BUILD_TYPE=Debug or Release |
| Multi-config: Visual Studio, Xcode, Ninja Multi-Config | Build with cmake --build ... --config Debug or Release |
| Mechanism | Best use |
|---|---|
target_compile_options() | Flags for a specific target |
target_compile_definitions() | Preprocessor macros such as FEATURE_X=1 |
target_link_options() | Options needed by the linker |
CMAKE_C_FLAGS_DEBUG / CMAKE_CXX_FLAGS_DEBUG | Global, language-wide configuration flags; use carefully |
The variables CMAKE_C_FLAGS_DEBUG and CMAKE_CXX_FLAGS_DEBUG exist, but target-based settings are generally safer because they do not unintentionally affect every target, including third-party code.
Cheat Sheet
# Enable C and C++ in one project.
project(MyProject LANGUAGES C CXX)
# C source -> C compiler; C++ source -> C++ compiler.
add_library(core STATIC core.c)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE core)
# A flag only for Debug.
target_compile_options(app PRIVATE
$<$<CONFIG:Debug>:-Wall>
)
# A definition only for Release.
target_compile_definitions(app PRIVATE
$<$<CONFIG:Release>:NDEBUG>
)
# Single-configuration generator: configure separate build directories.
cmake -S . -B build/debug -G Ninja -DCMAKE_BUILD_TYPE=Debug
cmake --build build/debug
cmake -S . -B build/release -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build/release
# Multi-configuration generator.
cmake -S . -B build -G "Ninja Multi-Config"
cmake --build build --config Debug
- Use
Debug,Release,RelWithDebInfo, orMinSizeRelwith the usual capitalization. CMAKE_BUILD_TYPEis for single-config generators.--configis for multi-config generators.- Prefer
target_compile_options()overCMAKE_CXX_FLAGS. - Use
.cfor C and.cpp/.ccfor C++. - Put Debug and Release outputs in different directories for single-config generators.
FAQ
How do I build Debug and Release with CMake?
For Ninja or Makefiles, configure separate build directories with -DCMAKE_BUILD_TYPE=Debug and -DCMAKE_BUILD_TYPE=Release. For multi-config generators, build with cmake --build <dir> --config Debug or Release.
Does CMake automatically add Debug and Release compiler flags?
Yes. CMake provides default flags for standard configurations, though the exact flags depend on the compiler and platform. Add only project-specific options when possible.
Should I set CMAKE_CXX_FLAGS_RELEASE directly?
Usually no. It changes flags globally and can affect dependencies. Prefer target_compile_options() with a $<CONFIG:Release> generator expression.
Can one CMake project use both GCC and G++?
Yes. Enable both C and CXX. CMake normally uses GCC’s C compiler for .c files and GCC’s C++ driver for C++ source files.
Do I need to manually link a C library differently from a C++ executable?
Usually no. Link the C library using target_link_libraries(). CMake knows the final executable needs C++ linkage when it contains C++ sources.
Why is CMAKE_BUILD_TYPE empty?
It is often empty when you did not set it for a single-config generator. It is also not the configuration selector for multi-config generators.
Which configuration should I use when debugging an optimized problem?
Try RelWithDebInfo. It is intended to combine optimization with debug information, although optimized code can still be harder to inspect than a Debug build.
Mini Project
Description
Create a small mixed-language command-line program. A C library supplies a numeric function, while a C++ executable calls it and prints additional diagnostic output only in Debug builds. This demonstrates separate Debug and Release build directories, configuration-specific definitions, and automatic C/C++ compiler selection.
Goal
Build and run a C++ executable that links to a C static library in both Debug and Release configurations.
Requirements
- Enable both C and C++ in the CMake project.
- Create a C static library containing a function that adds two integers.
- Create a C++ executable that calls the C library.
- Print a diagnostic message only when the Debug configuration is built.
- Configure Debug and Release into separate build directories.
Keep learning
Related questions
Advantages of Brace Initialization in C++
Learn why C++ brace initialization is often clearer and safer than other object initialization styles, with examples and common pitfalls.
Basic Rules and Idioms for Operator Overloading in C++
Learn the core rules, syntax, and common idioms for operator overloading in C++, including member vs non-member operators.
C++ Aggregates, Trivial Types, Trivially Copyable Types, and PODs Explained
Learn what aggregates, trivial types, trivially copyable types, and PODs mean in C++, how they differ, and why they matter.