Phase 5 · Professional C++ EngineeringModule 26~56 min read

Debugging, Testing & Undefined Behavior

Find and prevent defects through diagnostics, debugger evidence, automated tests, sanitizers, analysis, and defined-behavior discipline.

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.

strict-builds.txt
# 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.cpp

Tip

Enable warning groups incrementally, fix the underlying intent, and document only narrowly justified suppressions. Blanket suppression hides future defects.

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.

gdb-session.txt
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

A debugger shows what the program did, not why the design is correct. Connect observed state to a specific violated precondition, postcondition, invariant, or lifetime rule.

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 kindQuestion
UnitDoes this focused contract hold at normal and boundary inputs?
IntegrationDo real components agree at their boundary?
PropertyDoes an invariant hold over a generated input space?
RegressionWill this exact historical defect stay fixed?
End-to-endDoes a critical user workflow work in a production-like system?
simple_test.cpp
#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);
}
A production suite should use a test framework for isolation, diagnostics, fixtures, and discovery.

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.

sanitizer-builds.txt
# 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-tsan

Watch out

A clean sanitizer run proves only that the executed paths did not trigger a detected issue. It does not prove the absence of undefined behavior in untested paths.

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.

CategoryLanguage promiseEngineering response
DefinedSpecified behaviorRely on the contract
UnspecifiedOne of several allowed outcomesDo not depend on which one
Implementation-definedImplementation chooses and documentsIsolate and verify per target
UndefinedNo requirementsPrevent; never reason from observed behavior

Optimization exposes broken assumptions

An optimizer may assume undefined behavior never occurs and transform code accordingly. “It worked in Debug” is not evidence that the source has defined meaning.

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.