Exercises — CMake build types — Debug, Release, RelWithDebInfo
Before we start, one picture to anchor the only idea this whole topic rests on: a build type is a named point in the 3-knob space .

- The axis (left→right) = how hard the compiler optimizes, from
-O0(nothing) to-O3(aggressive). Higher = faster program, but variables get reordered or deleted so stepping in a debugger gets confusing. - The axis (up) = whether we embed the debug symbol table (
-g), the little map from machine instructions back to your source lines. It costs disk space but zero runtime. - The NDEBUG marker = whether
assert()is switched off (-DNDEBUGpresent = asserts vanish).
Keep this picture open. Every exercise is really "where does this build type sit in that space, and why?"
Level 1 — Recognition
(Can you read a flag string and name the type? Can you name the flags from the type?)
Recall Solution 1.1
Answer: Release.
Walk the two flags:
-O3= the maximum standard optimization level → this is a "make it fast" build.-DNDEBUG= theNDEBUGmacro is defined →assert()expands to nothing.
On our picture that's the point — the far-right, ground-level "clean printed final copy" corner. No -g, so no symbols. That is exactly Release.
Recall Solution 1.2
| Type | ? | NDEBUG? | |
|---|---|---|---|
| Debug | 0 | ✔ (-g) |
✘ |
| RelWithDebInfo | 2 | ✔ (-g) |
✔ |
| MinSizeRel | s (size) | ✘ | ✔ |
Reasoning per row:
- Debug = "messy draft with margin notes": symbols on, no optimization, asserts kept (you want the safety checks firing while developing).
- RelWithDebInfo = "clean copy with a key in the back": nearly-fast (
-O2) and carries-g, asserts off like a real release. - MinSizeRel = optimize for size (
-Os), not speed; asserts off, no symbols.
Recall Solution 1.3
False. -g only appends a symbol table to the binary file — data that sits there for a debugger to read. The CPU never executes it. The only cost is a bigger file on disk. This is why -g and -O are called orthogonal: you can freely combine them (that combination is literally what RelWithDebInfo is). See Debug Symbols & gdb.
Level 2 — Application
(Can you produce the right command / cache state for a stated goal?)
Recall Solution 2.1
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build-S .= source dir is here;-B build= put generated files inbuild/(out-of-source, keeps your repo clean).-DCMAKE_BUILD_TYPE=Release= select theReleasebundle → CMake will appendCMAKE_CXX_FLAGS_RELEASE=-O3 -DNDEBUG.- The second line runs the actual compiler. See Ninja vs Make for which underlying tool runs.
Recall Solution 2.2
You got -O0 (effectively no optimization).
When CMAKE_BUILD_TYPE is left empty, CMake appends none of the per-type flag variables — no -O, no -g, no -DNDEBUG. There is no hidden default of "Release." On our picture you're stuck at the origin , which runs at Debug speed while looking like a normal build. Fix: always pass -DCMAKE_BUILD_TYPE=... explicitly.
Recall Solution 2.3
cmake -S . -B build # configure once, no build type here
cmake --build build --config ReleaseMulti-config generators ignore CMAKE_BUILD_TYPE at configure time — they carry all configs at once and you choose at build time with --config. Setting -DCMAKE_BUILD_TYPE=Release would be silently useless here. See Single-config vs Multi-config Generators.
Level 3 — Analysis
(Given a symptom, diagnose the cause from first principles.)
Recall Solution 3.1
Cause: Release uses -O3 without -g. With no symbol table embedded, the debugger has no map from machine addresses back to source, so every frame prints ??.
Fix: RelWithDebInfo = -O2 -g -DNDEBUG.
-O2≈ nearly Release speed (and behaves like the real workload, unlike-O0Debug).-gre-embeds the symbol table → readable backtrace.-DNDEBUGkeeps it a true release (asserts off).
On the picture this is the point that is far right on and up on — the only standard type living in that upper-right region.
Recall Solution 3.2
NDEBUG is innocent. The NDEBUG macro gates only:
- the standard
assert()macro (makes it expand to nothing), and - any code the teammate personally wrapped in
#ifndef NDEBUG ... #endif.
A plain runtime if (debugMode) is ordinary C++ — the preprocessor never touches it. So -DNDEBUG cannot have turned it off. The real cause is elsewhere (e.g. debugMode is false, or an optimizer removed provably-dead code, or a different config was built). See assert and the NDEBUG macro.
Recall Solution 3.3
Switching to Release adds -DNDEBUG, which makes assert(x > 0) expand to nothing — the check disappears entirely.
Risk: the assert was catching x <= 0, a genuine bug. In Release that bug is no longer reported; it silently proceeds with bad data (undefined behaviour, wrong results, or a crash later, far from the cause). The assert firing is the good outcome. The right move is to fix why x <= 0, not to silence the alarm. (For catching such bugs even in optimized builds, see Sanitizers (ASan, UBSan).)
Level 4 — Synthesis
(Build something new by combining the pieces.)
Recall Solution 4.1
set(CMAKE_CXX_FLAGS_ASAN
"-O1 -g -fsanitize=address"
CACHE STRING "" FORCE)cmake -S . -B build -DCMAKE_BUILD_TYPE=ASAN
cmake --build buildWhy this works: a build type is nothing but a variable lookup of the form CMAKE_CXX_FLAGS_<TYPE>. Once CMAKE_CXX_FLAGS_ASAN exists in the cache, the name ASAN becomes a valid CMAKE_BUILD_TYPE. Choices explained:
-O1= light optimization so the sanitizer stays fast but code still resembles a real build.-g= symbols, so ASan can print source lines in its reports.-fsanitize=address= the actual ASan instrumentation.CACHE STRING "" FORCE= store it as a cache variable so CMake treats it exactly like a built-in per-type flag string.
Recall Solution 4.2
Target point on the picture: optimize for size and carry symbols.
set(CMAKE_CXX_FLAGS_MINSIZEDBG
"-Os -g -DNDEBUG"
CACHE STRING "" FORCE)-Os= optimize for size (theMinSizeReloptimization).-g= symbols — remember, zero runtime cost, so it doesn't slow the firmware; it only enlarges the on-disk / debug file. In practice you often strip-gout of the flashed image but keep a separate symbol file, so the device image stays tiny while you can still symbolicate.-DNDEBUG= it's a shipping build; asserts off.
Level 5 — Mastery
(Reason about the whole system; predict end-to-end behaviour.)
Recall Solution 5.1
Rule: .
So for RelWithDebInfo:
Full line: -Wall -fPIC -O2 -g -DNDEBUG.
-Wall(always-on base) = enable warnings.-fPIC(always-on base) = position-independent code.-O2 -g -DNDEBUG= the per-typeRelWithDebInfobundle. Order note: the per-type flags come after the base, so where flags conflict, the per-type ones win.
Recall Solution 5.2
The source is identical; the flags cause both differences:
- Missing assert in Release:
Releaseadds-DNDEBUG, so everyassert()compiled to nothing. Not a source change — a preprocessor change. - Tiny FP differences:
Releaseuses-O3, which lets the compiler reorder/associate floating-point operations, use fused multiply-add, keep values in wider registers, etc. Under-O0(Debug) these optimizations are off, so rounding happens at different moments → slightly different last bits. See Compiler Optimization Levels (-O0 to -O3). Conclusion: neither the source nor either compiler is "wrong." This is exactly whyRelWithDebInfoexists — you debug at production-like-O2so the behaviour you observe matches the shipped binary, instead of chasing bugs that only exist (or only vanish) at-O0.
Recall Solution 5.3
Speed ranking (fastest → slowest):
Release(-O3) is generally fastest.RelWithDebInfo(-O2) is very close — usually within a few percent.MinSizeRel(-Os) trades some speed for size.Debug(-O0) is dramatically slower (often several times).
For profiling production performance: RelWithDebInfo. It runs at near-Release speed (so timings are realistic) and carries -g symbols (so the profiler attributes hot spots to real function names and lines). Profiling Debug would measure the wrong thing; profiling bare Release would leave the profiler blind.
Recall One-line self-check before you leave
Say out loud: "A build type is a named bundle of flags = a point in space; single-config bakes it into the cache, multi-config picks it at build time." If that sentence feels obvious, you've mastered the topic.