The one core idea: a CMake "build type" is nothing but a nickname for a fixed set of words you hand the compiler — words like -O3 or -g. Changing the type never changes your program's meaning; it only changes how fast , how small , and how explainable the resulting machine code is.
Before you can understand the parent note , you need to know what each little scribble (-g, -O2, -DNDEBUG, CMAKE_CXX_FLAGS_<TYPE>) actually is . This page builds every one of them from zero, in the order they stack on top of each other.
Everything here is about one journey: source code → machine code → running program . Let's draw that pipeline first, because every symbol we meet is a knob on some stage of it.
Definition Source code vs machine code
Source code is the .cpp text a human writes. Machine code is the raw numbers the CPU actually executes. The program that translates one into the other is the compiler (e.g. g++, clang++). A build type only ever tweaks how the compiler does that translation.
A flag is a short extra word you type after the compiler's name to change its behaviour. It always starts with a dash -. Example:
g++ main.cpp -O2 -g
Here -O2 and -g are two flags. The compiler reads them like checkboxes: "optimize at level 2" and "include debug info".
Think of the compiler as a coffee machine and flags as buttons: extra shot , decaf , no sugar . Same beans (your code), different drink (your binary). A build type is just a saved "favourite order" — one button that presses several others for you.
The picture: a command line is a base word (g++ main.cpp) followed by a growing list of these dash-words.
-O<n> optimization level
-O (capital letter O, "oh") tells the compiler how hard to work rewriting your code to run faster . The number after it is the effort dial:
-O0 → no optimization (fastest to compile, slowest to run, easiest to debug)
-O1, -O2 → moderate
-O3 → aggressive (fastest to run, hardest to debug)
-Os → optimize for s ize (smallest binary)
Why does the topic need this tool, and not just "make it fast"? Because there is no free lunch: making code run faster means the compiler reorders and deletes instructions. That rearrangement is exactly what makes a debugger confused later. So -O is the single knob that trades runtime speed against debuggability . The whole build-type idea exists to pick a spot on that trade-off.
Worked example Reading the dial
-O0 = the compiler transcribes your code almost line-for-line. -O3 = the compiler treats your code as a goal and finds the fastest machine code that achieves it, even if the steps look nothing like what you wrote.
-g debug symbols
-g tells the compiler to bundle a "map" into the binary: a table that says "machine instruction #4172 came from main.cpp line 12, and the value in this register is the variable count." This map is called a symbol table (debug symbols).
Why this tool? Machine code has no idea what a "variable name" or "line number" is — those are human ideas that normally get thrown away. If your program crashes and you want the debugger to say crashed in parse() at line 12 instead of crashed at address 0x4a11c8, you need that map. -g is the request to keep it.
-g is like keeping the receipt for every instruction: it says which source line "bought" it. Crucially, the receipt sits in a drawer the CPU never opens — so it costs zero runtime speed , only extra disk size.
Wrong idea: "-g slows my program down."
Truth: -g only adds data to the file; the CPU skips right over it while running. What slows debugging down is a low -O , which is a different flag entirely.
The parent note leans hard on the word orthogonal . Let's earn it.
Two knobs are orthogonal when turning one does not turn the other — you may set them in any combination. -O (speed) and -g (explainability) are orthogonal, so all four corners of the square below are legal builds.
Debug lives in one corner (-O0 -g: slow but very readable). Release lives in another (-O3, no -g: fast but silent). RelWithDebInfo is the third corner nobody expects — -O2 and -g together — which is only possible because the two knobs are orthogonal. That corner is the whole reason the type exists.
A macro is a name that the compiler's pre-processor ==finds and replaces with something else before real compilation==. If NDEBUG is "defined", the pre-processor knows a certain switch is ON.
-D<NAME> define flag
The flag -D followed by a name defines that macro. So -DNDEBUG means "before compiling, act as if the programmer wrote #define NDEBUG." No value needed — its mere existence is the signal.
Why does the topic need this? Because C and C++ ship a safety tool called assert() that is switched off by exactly this macro. To understand -DNDEBUG you must first meet assert.
assert(condition)
assert is a built-in check: "I claim condition is true here; if it isn't, crash immediately and tell me where." It's a safety net for catching your own bugs during development. See assert and the NDEBUG macro for the full story.
Here is the rule the standard library follows:
So -DNDEBUG removes every assert from the final binary . Shipping builds do this for speed; debug builds leave asserts in as a safety net.
Wrong idea: "NDEBUG disables all my debugging."
Truth: NDEBUG only controls assert() (and code you wrapped in #ifndef NDEBUG). Your own if (verbose) print(...) logic is untouched. Debuggability comes from -g; they are unrelated things with confusingly similar names.
Now we can read the scary-looking variable names.
CMAKE_CXX_FLAGS
A CMake variable is a named box that stores text. CMAKE_CXX_FLAGS is the box holding flags applied to every C++ compile, no matter the build type. (CXX is the traditional abbreviation for "C++".) These live in the CMake cache .
CMAKE_CXX_FLAGS_<TYPE>
The <TYPE> part is a placeholder you replace with a build-type name in capitals. So there is a separate box for each type:
CMAKE_CXX_FLAGS_DEBUG → holds -g
CMAKE_CXX_FLAGS_RELEASE → holds -O3 -DNDEBUG
CMAKE_CXX_FLAGS_RELWITHDEBINFO → holds -O2 -g -DNDEBUG
Degenerate case you must know: if CMAKE_BUILD_TYPE is empty (the silent default!), then T is nothing, so the second box is empty, so you get only CMAKE_CXX_FLAGS — meaning no -O, no -g at all. Your "Release" that you forgot to set is secretly an -O0 build. This is the single most common CMake performance trap.
A generator is the tool that turns your CMake project into real build files. A single-config generator (Make, Ninja ) bakes ONE build type into the cache. A multi-config generator (Visual Studio, Xcode, Ninja Multi-Config) carries ALL types and picks one at build time. See Single-config vs Multi-config Generators .
You don't need the details yet — just know the word "generator" so the parent note's "multi-config trap" section makes sense. The key fact: for multi-config, CMAKE_BUILD_TYPE is ignored and you write --config Release at build time instead.
Source code vs machine code
What is a flag - dash word
Orthogonal - independent knobs
Macro - preprocessor replace
CMAKE_CXX_FLAGS_TYPE boxes
CMAKE_BUILD_TYPE picks the box
Generators single vs multi config
Cover the right-hand side and test yourself. If any line is fuzzy, re-read its section above before opening the parent note.
A "flag" is a dash-word after the compiler name that toggles one behaviour (e.g. -O2).
What -O controls how hard the compiler works to make the code run faster (0 = none, 3 = aggressive, s = smallest).
Why high -O hurts debugging it reorders and deletes instructions, so the debugger can't line them up with your source.
What -g adds a symbol table mapping machine instructions back to source lines and variable names.
Runtime cost of -g zero — it only enlarges the file; the CPU skips over it.
Meaning of "orthogonal" for -O and -g they're independent knobs; any combination (including both at once) is legal.
What a macro is a name the pre-processor finds and replaces before real compilation.
What -DNDEBUG does defines the NDEBUG macro, which makes every assert() expand to nothing.
Does NDEBUG disable your own debug prints no — only assert() and #ifndef NDEBUG code; your if(verbose) logic is untouched.
CMAKE_CXX_FLAGS vs CMAKE_CXX_FLAGS_<TYPE>the first is always applied; the second is glued on only for the chosen type.
Final flags formula flags(T) = CMAKE_CXX_FLAGS + CMAKE_CXX_FLAGS_<T>.
What happens if CMAKE_BUILD_TYPE is empty only the base box applies → no -O, no -g → effectively -O0.
Which variable picks the type (single-config) CMAKE_BUILD_TYPE.