What you'll learn
Professional debugging turns a symptom into a small, reproducible violated contract. Tests prevent its return; warnings, sanitizers, and static analysis expose defect classes before users do. You will build a layered evidence-driven quality workflow.
By the end, you'll be able to:
- Create strict multi-compiler diagnostic builds
- Use a debugger to trace state and reduce a failure
- Design unit, integration, property, and regression tests
- Detect undefined behavior with sanitizers and static analysis
Warnings are design feedback
Compile with a modern language mode and strict warnings on every change. GCC, Clang, and MSVC diagnose overlapping but different defects, so a continuous-integration matrix adds real value. Treat project warnings as errors in controlled builds, while keeping third-party headers isolated from that policy.
# GCC / Clang
g++ -std=c++20 -Wall -Wextra -Wpedantic -Wconversion -Wshadow app.cpp
clang++ -std=c++20 -Wall -Wextra -Wpedantic -Wconversion -Wshadow app.cpp
# MSVC Developer Command Prompt
cl /std:c++20 /W4 /permissive- /EHsc app.cppTip
Debugger workflow
Reproduce the failure with exact input and build settings, then stop near the first known divergence. Breakpoints pause execution; stepping follows control flow; watches and locals reveal state; stack traces show the active call chain. Conditional breakpoints and data watchpoints reduce noise in long runs.
g++ -std=c++20 -g -O0 -Wall -Wextra app.cpp -o app
gdb ./app
(gdb) break parse_record
(gdb) run broken-input.txt
(gdb) next
(gdb) print index
(gdb) backtrace- Confirm the exact failing revision, input, platform, and configuration
- Find the earliest incorrect state, not merely the eventual crash
- Reduce the input and remove unrelated execution paths
- Form one testable hypothesis at a time
- Turn the minimized failure into a regression test before fixing it
Key idea
Layered automated tests
Unit tests exercise a focused contract quickly. Integration tests verify component boundaries such as files or databases. Property tests generate many inputs for invariants; regression tests preserve one historical failure. The pyramid is a feedback strategy, not a mandate to mock every dependency.
| Test kind | Question |
|---|---|
| Unit | Does this focused contract hold at normal and boundary inputs? |
| Integration | Do real components agree at their boundary? |
| Property | Does an invariant hold over a generated input space? |
| Regression | Will this exact historical defect stay fixed? |
| End-to-end | Does a critical user workflow work in a production-like system? |
#include <cassert>
#include <stdexcept>
int clamp_percentage(int value) {
if (value < 0 || value > 100) throw std::out_of_range{"percentage"};
return value;
}
int main() {
assert(clamp_percentage(0) == 0);
assert(clamp_percentage(100) == 100);
bool threw{};
try { clamp_percentage(101); }
catch (const std::out_of_range&) { threw = true; }
assert(threw);
}Runtime sanitizers
Sanitizers instrument a build to detect invalid memory access, leaks, signed overflow and other undefined behavior, or data races. They need representative tests and are normally run in separate configurations because ThreadSanitizer is not combined with AddressSanitizer.
# AddressSanitizer + UndefinedBehaviorSanitizer
clang++ -std=c++20 -g -O1 -fno-omit-frame-pointer \
-fsanitize=address,undefined app.cpp -o app-asan
# ThreadSanitizer in a separate supported build
clang++ -std=c++20 -g -O1 -fno-omit-frame-pointer \
-fsanitize=thread app.cpp -o app-tsanWatch out
Static analysis and review
Static analyzers reason without running the program and can flag lifetime mistakes, unchecked returns, suspicious conversions, and style patterns linked to defects. Formatters reduce irrelevant review churn. Guideline checks should support an adopted policy rather than turn every recommendation into an unexamined rule.
- Run clang-tidy or an equivalent analyzer on changed code
- Use compiler static analysis where available
- Make formatting deterministic and automated
- Review ownership, bounds, failure, synchronization, and invalidation explicitly
- Track analyzer suppressions with a reason and a narrow scope
Undefined and implementation-defined behavior
Undefined behavior has no language-imposed requirements: examples include out-of-bounds access, signed overflow, use-after-lifetime, data races, and invalid shifts. Unspecified behavior chooses among allowed outcomes without documenting which; implementation-defined behavior chooses and documents one outcome. Portable code knows which category it relies on.
| Category | Language promise | Engineering response |
|---|---|---|
| Defined | Specified behavior | Rely on the contract |
| Unspecified | One of several allowed outcomes | Do not depend on which one |
| Implementation-defined | Implementation chooses and documents | Isolate and verify per target |
| Undefined | No requirements | Prevent; never reason from observed behavior |
Optimization exposes broken assumptions
Recap & quick check
Key takeaways
- Strict warnings across multiple compilers catch different defect classes early.
- Debugging should locate the first violated contract and produce a minimized regression test.
- Unit, integration, property, regression, and end-to-end tests answer different questions.
- Sanitizers observe executed paths; static analysis reasons over code without execution.
- Undefined behavior cannot be validated by one successful run and must be designed out.
Quick check
1. What should a debugger investigation seek first?
2. Which sanitizer targets data races?
3. What does a regression test preserve?
4. What guarantee applies after undefined behavior occurs?
Next: Module 27 — CMake, Dependencies, Libraries & Packaging, where these quality configurations become reproducible project targets.