C++ / NAMESPACES, HEADERS, AND BUILDS
Compiler flags, warnings, and sanitizer builds
Choose compiler flags on purpose: enable the warnings that catch real bugs, and build a separate sanitizer binary to catch what the compiler cannot see.
What you will learn
- Turn on -Wall -Wextra -Wshadow -Wconversion and make warnings errors in CI
- Read each warning as a bug report about a specific code path, not as style noise
- Build a second binary with -fsanitize=address,undefined -g for running tests
- Know that the -O level changes which warnings fire and how remaining UB behaves
Understanding Compiler flags, warnings, and sanitizer builds
A compile command bundles three separate decisions: which dialect the source is read as (-std=c++20), how freely the optimizer may rearrange the code (-O0 through -O3), and whether the finished binary can still be explained (-g). Adding -g only attaches debug info and never changes the instructions emitted, so keeping it on in optimized builds costs nothing but file size. -O is not neutral in the same way: the optimizer is allowed to assume your program contains no undefined behaviour, so code that appears to work at -O0 can genuinely misbehave at -O2. The flag did not create the bug; it stopped concealing it.
Warnings are conclusions the compiler already reached while generating code, which is why they cost zero run time. -Wall is a historical selection rather than everything: -Wextra adds another batch, and valuable checks such as -Wshadow, -Wconversion, -Wold-style-cast and -Wnon-virtual-dtor must be asked for by name. Some warnings depend on dataflow information that only exists after inlining, so GCC's -Wmaybe-uninitialized can appear at -O2 and disappear at -O0 on identical source: same code, different amount of analysis.
A sanitizer is not analysis at all. It is extra checking code compiled into your binary plus a runtime library linked beside it, which is why -fsanitize=address must appear on both the compile and the link command. Because those checks actually execute, they see what no compile-time reasoning can: this pointer really is dangling right now, this shift really is by 33 bits, this addition really did overflow. The trade is that a sanitizer judges only the execution you performed, and ASan's roughly 2x slowdown and 3x memory use mean it belongs in a dedicated test build rather than in what you ship.
<iostream>
// g++ -std=c++20 -Wall -Wextra -Wconversion -g -o demo demo.cpp
int main() {
unsigned int limit = 4;
int delta = -2;
// -Wsign-compare: delta is converted to unsigned first, becoming
// 4294967294, which is not below 4.
if (delta < limit)
std::cout << "delta is below limit\n";
else
std::cout << "delta is NOT below limit\n";
// -Wfloat-conversion (part of -Wconversion): the fraction is dropped,
// not rounded.
double exact = 7 / 2.0;
int half_days = exact;
std::cout << "exact = " << exact << ", half_days = " << half_days << '\n';
}
Warnings prove things about your source before it runs; sanitizers instrument the binary so one real execution can expose the undefined behaviour no compile-time check could see.
Worked examples
A shadowed variable that -Wall will not mention
Shows a silent logic bug that only the opt-in -Wshadow reports.
<iostream>
// g++ -std=c++20 -Wall -Wextra -Wshadow shadow.cpp -o shadow
int main() {
int total = 0;
for (int value : {1, -2, 3}) {
if (value > 0) {
int total = value; // meant: total += value;
std::cout << "adding " << total << '\n';
}
}
std::cout << "total = " << total << '\n';
}
Example explained
Line 1int total = value; creates a second variable that dies at the closing brace, so the outer total keeps its 0.
Line 2The per-item lines still print 1 and 3, which makes the bug look like a printing problem instead of a scope problem.
Line 3-Wall and -Wextra are silent here; only -Wshadow reports that the declaration shadows a previous local.
Signed overflow found only at run time
Demonstrates UBSan reporting undefined behaviour that no warning can predict.
<iostream>
// g++ -std=c++20 -g -fsanitize=undefined overflow.cpp -o overflow
int main() {
volatile int big = 2147483647; // volatile blocks constant folding
int wrapped = big + 1; // signed overflow: undefined behaviour
std::cout << "wrapped = " << wrapped << '\n';
}
Example explained
Line 1volatile forces a real load, so the compiler cannot fold big + 1 and no -Woverflow is possible at compile time.
Line 2The message comes from a check the compiler inserted around that addition, and it fires only because line 6 executed.
Line 3UBSan recovers by default, so the wrapped value is still printed; the diagnostic went to stderr, the value to stdout.
Line 4The file, line and column in the message come from your own build, not from the sanitizer.
How -DNDEBUG changes what assert does
Shows that a preprocessor flag can delete code you thought was always running.
<cassert>
<iostream>
// g++ -std=c++20 assert_demo.cpp -o assert_demo
// g++ -std=c++20 -DNDEBUG assert_demo.cpp -o assert_release
int main() {
int calls = 0;
assert(++calls == 1); // the side effect lives inside the assert
std::cout << "calls = " << calls << '\n';
}
Example explained
Line 1assert(++calls == 1) expands to nothing when NDEBUG is defined, so ++calls never happens.
Line 2CMake's Release and RelWithDebInfo build types define NDEBUG for you, which is why only the release binary prints 0.
Line 3The fix is to write ++calls; on its own line and assert the resulting value afterwards.
Important notes
Sanitizer diagnostics go to stderr and their exact wording, line and column vary by compiler and version; ASan aborts on the first error while UBSan keeps running unless you build with -fno-sanitize-recover=all.
-fsanitize=address and -fsanitize=thread cannot live in the same binary, and neither belongs in a release build: ASan roughly doubles run time and triples memory use.
Common mistakes
Passing -fsanitize=address only on the compile command: the link fails with undefined references to __asan_report_load4 and friends, or you get a binary with no checks in it at all.
Assuming -Wall means every warning, so -Wshadow, -Wconversion and -Wnon-virtual-dtor stay off and bugs like the shadowed total above compile without a word.
Silencing delta < limit with static_cast<unsigned>(delta): the warning disappears and the comparison is still wrong for every negative value.
Try it yourself
Change, predict, then run
In an online compiler, fix both warnings from the main program at their source until -Wall -Wextra -Wconversion is silent: use two signed operands for the comparison, and produce the rounded value with std::lround stored in a long. Then swap the flags for -fsanitize=undefined -g, add volatile int zero = 0; and print 7 / zero to see what a runtime check catches that no warning could.
Open the C++ workspaceCheck your understanding
Your build with -std=c++20 -Wall -Wextra -Wconversion -Wshadow is completely silent. Which bug is still waiting for you, findable only by a sanitizer run?
- A pointer into a std::vector that is used again after push_back reallocated the buffer
- Comparing a signed loop counter against v.size()
- Assigning a double result into an int variable
- A local variable in an inner scope that hides an outer one with the same name
Show answer
Whether the vector reallocated depends on capacity and allocation state during a particular run, so no compile-time check can know the pointer went stale; AddressSanitizer can, because it poisons the freed buffer and traps the load. Assigning a double to an int looks tempting, but -Wconversion reports that narrowing at compile time, just as -Wsign-compare and -Wshadow already report the other two.