5.3.9 · D5Build Systems & Toolchain

Question bank — CMake build types — Debug, Release, RelWithDebInfo

1,734 words8 min readBack to topic

Read each question, answer it out loud in a full sentence before revealing. If your answer is a bare "yes"/"no", you failed — the reasoning is the point.


Picture the whole topic first

Before the traps, fix three pictures in your head. Every question below is really asking about one of these diagrams.

Picture 1 — the three independent knobs. The claim "these three settings are orthogonal" means each one moves along its own axis and does not drag the others with it. Look at the three separate dials: you can crank -O up without touching -g, and flip -DNDEBUG without touching either. There is no wire connecting them.

Figure — CMake build types — Debug, Release, RelWithDebInfo

Because they are separate axes, a "build type" is just one chosen point in that 3-axis space — a specific reading on each dial. Look at where the four standard types land: Debug sits at speed-low / symbols-on / asserts-on, Release at speed-high / symbols-off / asserts-off, and RelWithDebInfo deliberately picks the "both" corner (fast and symbols).

Figure — CMake build types — Debug, Release, RelWithDebInfo

Picture 2 — the default-flag matrix. Keep this table beside you; most "which flags?" traps are just misremembering one cell of it.

Figure — CMake build types — Debug, Release, RelWithDebInfo

Picture 3 — single-config vs multi-config, and the "empty type" trap. A single-config generator (Makefiles, plain Ninja) bakes ONE type into the cache at configure time — and if you leave CMAKE_BUILD_TYPE unset, the string is literally the empty string "", which matches no CMAKE_CXX_FLAGS_<TYPE> variable, so zero extra flags are added (silent -O0, asserts still live). A multi-config generator (Visual Studio, Xcode, Ninja Multi-Config) instead carries all types and picks one at build time with --config. Follow the two branches:

Figure — CMake build types — Debug, Release, RelWithDebInfo

True or false — justify

Release always produces a faster binary than Debug.
Usually true because Release adds -O3 while Debug is -O0, but it is not a law — for tiny loops or I/O-bound code the difference can vanish, and a mis-set empty type gives you -O0 while thinking you shipped Release.
-g makes your program run slower.
False — -g only embeds a symbol table on disk for gdb; it adds zero runtime cost. The thing that slows/changes behaviour is a low -O level, which is a separate knob.
Debug and RelWithDebInfo both contain -g, so they debug equally well.
False — both carry symbols, but RelWithDebInfo also has -O2, so the optimizer reorders and eliminates variables; stepping in gdb will "jump around" and some locals show as <optimized out>.
Setting CMAKE_BUILD_TYPE=Release guarantees -DNDEBUG is passed.
True for the standard Release bundle — CMake appends CMAKE_CXX_FLAGS_RELEASE which is -O3 -DNDEBUG by default — but only if the generator is single-config and someone hasn't overridden that cache variable.
A build type changes which .cpp files get compiled.
False — the build type only changes flags. The exact same source files and libraries are compiled either way; that is the whole "named bundle of flags" mental model.
MinSizeRel will always be smaller and slower than Release.
Smaller: usually yes (-Os optimizes for size). Slower: not guaranteed — smaller code can fit the instruction cache better and sometimes runs faster, so "size vs speed" is a trend, not a rule.
Left with no build type set, CMake defaults to Debug.
False and this is the classic gotcha — for single-config generators an empty CMAKE_BUILD_TYPE means no -O/-g/-DNDEBUG flags at all, behaving like -O0 with asserts still active.

Spot the error

"I passed -DCMAKE_BUILD_TYPE=Debug so Visual Studio will build Debug."
Wrong for a multi-config generator — VS/Xcode/Ninja-Multi-Config ignore CMAKE_BUILD_TYPE and choose the config at build time via cmake --build build --config Debug.
"-DNDEBUG turns off my own if(debug_mode) logging."
Wrong — NDEBUG only gates the standard assert() macro (and code you wrote inside #ifndef NDEBUG); your own runtime if(...) branches are untouched. See assert and the NDEBUG macro.
"My Release binary is slow, so -O3 must be broken."
The likely error is that CMAKE_BUILD_TYPE was left empty, so it was really -O0; check grep BUILD_TYPE build/CMakeCache.txt before blaming the optimizer.
"Release strips -g, therefore a Release core dump can never be symbolized."
The reasoning that Release has no -g is right, but the conclusion is too strong — you can either ship RelWithDebInfo, or keep a separate .debug symbol file and match it to the stripped binary later.
"I added -fsanitize=address to my flags but forgot to change the build type; that's fine."
Risky — ASan wants -O1 -g and no aggressive optimization or -DNDEBUG; pairing it with a Release bundle can hide bugs and disable asserts, defeating the point.
"To make a custom ASAN type I only need to name it in --build --config ASAN."
Wrong for single-config — you must first define the cache variable CMAKE_CXX_FLAGS_ASAN; a build type is just a lookup of CMAKE_CXX_FLAGS_<TYPE>, and an undefined one contributes no flags.

Why questions

Why are -g and -O called "orthogonal"?
Because they answer two independent questions — -g asks "can I map machine code back to source?" while -O asks "how hard should the compiler optimize?" You can turn each on or off without touching the other, which is exactly why RelWithDebInfo can have both.
Why does RelWithDebInfo use -O2 instead of -O3?
-O2 keeps most of the speed while producing debug info that still lines up reasonably with the source; -O3's more aggressive transforms would make backtraces and stepping even more confusing for the profiling/crash-dump use case.
Why does shipping code define NDEBUG?
Because assert() checks cost runtime and a shipped binary shouldn't abort on a failed internal invariant in front of users; -DNDEBUG compiles those checks out entirely for speed.
Why is the effective flag line CMAKE_CXX_FLAGS + CMAKE_CXX_FLAGS_<TYPE> and not just one of them?
CMAKE_CXX_FLAGS holds flags you always want (warnings, standard version) regardless of type, and the per-type variable adds the profile on top — separating "always" from "this profile" so you don't repeat yourself for every type.
Why prefer RelWithDebInfo over Debug for reproducing a production performance bug?
Debug's -O0 runs a fundamentally different (slower, un-optimized) program, so timing bugs may not reproduce; RelWithDebInfo runs code close to your real -O2 workload while still carrying symbols.
Why doesn't setting CMAKE_BUILD_TYPE help with a Ninja Multi-Config generator?
Multi-config generators bake all configs into the build tree at configure time and let you pick one per invocation with --config; there is no single "the type" to store, so the variable is simply ignored.

Edge cases

If you set CMAKE_BUILD_TYPE=release (lowercase), what happens?
CMake type names are case-sensitive for the flag lookup, so CMAKE_CXX_FLAGS_RELEASE won't match release and you get no optimization flags — the same silent trap as leaving it empty.
Can a target still contain assert() behaviour in a Release build?
Only if NDEBUG was somehow not defined for that translation unit (e.g., custom flags, or an assert-like macro you rolled yourself); the standard assert is compiled out once -DNDEBUG is present.
What if you pass both -O0 in CMAKE_CXX_FLAGS and pick Release (-O3)?
The command line ends up with -O0 ... -O3, and for GCC/Clang the last -O wins, so you'd effectively get -O3 — but relying on ordering like this is fragile and worth avoiding.
In a multi-config build tree, is there any use for CMAKE_BUILD_TYPE?
It is ignored for choosing the compile config, though some projects read it in scripts as a hint; the authoritative choice is always the --config flag at build time.
Does MinSizeRel include -g?
No — like Release it drops symbols (-Os -DNDEBUG), because its whole purpose is the smallest possible firmware/embedded binary, and symbols add size.
What is the smallest change to get gdb-usable symbols in a Release-speed binary without redefining types?
Just switch to the built-in RelWithDebInfo type (-O2 -g -DNDEBUG); no custom cache variable needed, near-Release speed with symbols intact.

Recall One-line self-test

Empty CMAKE_BUILD_TYPE on a single-config generator behaves like which type? ::: Like -O0 with no -DNDEBUG — so asserts are active but nothing is optimized, which is not the same as Debug (Debug explicitly adds -g).