80 / 163 · C++11 · 14 min
Diagnostics and Measurement: Warnings, Sanitizers, and the Debugger
Let compiler warnings, runtime instrumentation, and the debugger each answer different questions, then verify the fix with repeatable inputs. You must distinguish undefined, unspecified, and implementation-defined behavior; a sanitizer reporting nothing only means this run did not trigger the enabled checks, and performance conclusions must also come from independent measurement.
In this lesson
Identify the rules first, then choose the tools
Undefined behavior means the standard no longer constrains the program, for example signed integer overflow or out-of-bounds access; you cannot treat one observed output as legitimate semantics. Unspecified behavior is a choice among permitted results; the implementation need not document the particular choice, for example the evaluation order of function arguments that do not depend on each other. Implementation-defined behavior requires the implementation to provide documentation, for example whether plain char is signed. The latter two are not a license for arbitrary execution.
Diagnosis therefore starts from the contract: what are the input ranges, object lifetimes, and index bounds? The example below checks bounds before addition, rather than computing a possibly overflowing sum and then comparing. On failure the output parameter keeps its original value, capturing boundary errors and also verifying the interface's promise to the caller.
Warnings and instrumentation: pin down one reproducible run
Save as diagnostics.cpp. First run g++ -std=c++11 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -g -Og diagnostics.cpp -o diag, then run ./diag. Start fixing from the first diagnostic and its context; do not silence them in bulk with casts. Record the compiler version, the full command, and the inputs so the run is reproducible. A warning set is not a complete language-error oracle.
When Clang and its sanitizer runtime are installed, run clang++ -std=c++11 -O1 -g -fsanitize=address,undefined -fno-sanitize-recover=undefined -fno-omit-frame-pointer diagnostics.cpp -o diag-san, then run ./diag-san. ASan mainly finds memory-access errors; UBSan covers some undefined operations. Multi-file builds should instrument the relevant source files and also pass sanitizer options at final link.
Use the debugger to locate the first bad state
Run gdb ./diag, then enter in order break checked_add, run, print a, print b, bt. Use next to advance by source line, step to enter a call, continue to the next breakpoint, and quit to exit. The crash site may only be where corruption was discovered; follow the call stack to the first write that violated an invariant, and set a watchpoint while the object is still alive if needed.
Optimization may show locals as optimized out and may change single-step order. Keep the original problem's optimized configuration for reproduction, then use an easier-to-debug build to aid understanding. Passing instrumentation is not a proof: unexecuted branches, uninstrumented dependencies, and rules the tool does not cover remain risks; concurrent data races still require ThreadSanitizer separately on a suitable platform.
Confirm correctness first, then compare performance
Build a non-instrumented optimized binary with g++ -std=c++11 -O2 -g diagnostics.cpp -o diag-opt. The small program below is a correctness probe, not a meaningful benchmark. Real measurement should fix input size, compile options, and machine load, warm up first, then run repeatedly; record the median, variation, and cost per operation rather than picking only the shortest run.
Surround the work under test with steady_clock, and give the result an observable use outside the timed interval so the work is not eliminated; output alone still may not prevent constant folding, so use real runtime inputs and inspect the optimized result. Whether allocation and I/O sit inside or outside the timed interval is decided by the question. Do not judge a release build by time spent under sanitizers or a debugger, and do not prove algorithmic complexity from one fast or slow run.
Pitfalls
- UBSan by default continues after reporting some errors; the example explicitly disables recovery for undefined checks so continued execution is not mistaken for the error having been handled safely.
- assert is suitable for expressing debug and test contracts, not external-input validation; release configurations may disable assertions, so necessary checks must remain in ordinary control flow.
Run an example
Minimum C++11 · complete program · Download .cpp
#include <cassert>
#include <iostream>
#include <limits>
bool checked_add(int a, int b, int& result) noexcept {
const int low = std::numeric_limits<int>::min();
const int high = std::numeric_limits<int>::max();
if ((b > 0 && a > high - b) ||
(b < 0 && a < low - b)) {
return false;
}
result = a + b;
return true;
}
int main() {
int result = 99;
bool ok = checked_add(12, 30, result);
assert(ok && result == 42);
ok = checked_add(std::numeric_limits<int>::max(), 1, result);
assert(!ok && result == 42);
ok = checked_add(std::numeric_limits<int>::min(), -1, result);
assert(!ok && result == 42);
ok = checked_add(std::numeric_limits<int>::min(), 0, result);
assert(ok && result == std::numeric_limits<int>::min());
ok = checked_add(-5, 7, result);
assert(ok && result == 2);
std::cout << result << '\n';
}
Compile locally
g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread tooling-diagnostics.cpp -o example && ./exampleExpected result
2
CHECK YOUR UNDERSTANDING
Close the answer. Explain it.
Someone writes the check as int sum = a + b; if (sum < a) return false; and cites "the sanitizer reported nothing" as proof of correctness. Point out two logical problems, and explain why the example does not overflow again during the bound checks.
Show a reference answer
First, if signed addition overflows, UB has already occurred before the comparison, so a post-check is invalid. Second, a legitimate negative add can also make sum < a, so that condition cannot represent signed overflow in general. A clean report covers only the actual inputs and the enabled checks. The correct implementation compares a with max - b when b > 0, and a with min - b when b < 0; the first subtraction result lies in the representable range, and the second also lies between min and 0, without computing -b, so b equal to the minimum is also safe. Only after confirming safety is a + b executed, and the failure path does not write result.
Check the sources
- cppreference:Undefined behavior 与行为分类
- Clang:AddressSanitizer 的编译、链接与检测范围
- Clang:UndefinedBehaviorSanitizer 与恢复选项
- GDB:A Sample GDB Session
Drafts and official chapters change. The version mark is only the example’s minimum.