Question

Debug and Release Builds in CMake with GCC and G++

cppcgcccmake

Question

In a project built with GCC, how can I:

  1. Configure and build each CMake build type, such as Debug and Release?
  2. Specify C and C++ compiler flags for Debug and Release builds in CMake?
  3. Build the main executable as C++ (using g++) while building a nested library from C source files (using gcc)?

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:

  1. Single-configuration generators, such as Unix Makefiles and Ninja. A build directory has one configuration, selected while configuring with CMAKE_BUILD_TYPE.
  2. 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:

  • .c source files are compiled as C, usually by gcc.
  • .cpp, .cc, and .cxx source files are compiled as C++, usually by g++.

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.

Take Quiz

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:

  1. project(... LANGUAGES C CXX) asks CMake to find and test both a C and C++ compiler.
  2. parser.c is recognized as C, so CMake creates a C compilation command, typically using gcc.
  3. main.cpp is recognized as C++, so CMake creates a C++ compilation command, typically using g++.
  4. Because the configuration is Debug, TOOL_VERBOSE=1 is added when compiling tool.
  5. CMake builds libparser.a, then links it into tool.
  6. 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 RelWithDebInfo to 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.
Take Quiz

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

TopicDebugRelease
Main purposeDiagnose and test codeDeploy and measure performance
OptimizationUsually low or disabledUsually enabled
Debug symbolsUsually includedMay be omitted or reduced
Typical experienceEasier stepping and inspectionFaster, but harder to debug
Build system typeHow to select configuration
Single-config: Ninja, Unix MakefilesConfigure with -DCMAKE_BUILD_TYPE=Debug or Release
Multi-config: Visual Studio, Xcode, Ninja Multi-ConfigBuild with cmake --build ... --config Debug or Release
MechanismBest 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_DEBUGGlobal, 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.

Take Quiz

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, or MinSizeRel with the usual capitalization.
  • CMAKE_BUILD_TYPE is for single-config generators.
  • --config is for multi-config generators.
  • Prefer target_compile_options() over CMAKE_CXX_FLAGS.
  • Use .c for C and .cpp/.cc for 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.

Take Quiz

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.
Take Quiz

Keep learning

Related questions

View more