Compiler warnings, debugging, and testing
Read compiler diagnostics, use breakpoints, inspect program state, and test edge cases.
Learning objectives
- Distinguish syntax/compile, link, runtime, and logic problems.
- Read a compiler diagnostic and understand what a warning does—and does not—promise.
- Build with debug information and use breakpoints, stepping, and variable inspection.
- Design normal, boundary, empty, and invalid-input tests.
Prerequisites: compile and run a basic C++ program, read variables and loops, and understand compiler diagnostics. Review Your first C++ program, Compiler toolchain, and Loops.
Classify the problem first
| Problem type | When it appears | Example |
|---|---|---|
| Syntax / compile error | During translation | Missing semicolon or invalid expression |
| Link error | During linking | A declared function has no matching definition |
| Runtime failure | While the program executes | Opening a file fails and the program reports it |
| Logic error | Program runs but gives the wrong answer | Using < where the algorithm requires <= |
Do not treat every invalid memory access as a predictable “runtime error”: many such defects are undefined behavior, and the language does not guarantee a crash or a diagnostic. Sanitizers and debuggers can help expose some defects, but they do not prove a program correct.
Read diagnostics and enable warnings
Start with the file name, line/column location, and first useful diagnostic. Later messages may be consequences of the first error. Fix one cause and rebuild. A warning is not necessarily a compilation failure; it is a diagnostic about code that may be risky or suspicious. Warning sets differ between compilers and versions, and a clean warning run is not proof that the program is bug-free.
g++ -std=c++17 -Wall -Wextra -Wpedantic main.cpp -o app
For Clang, use the corresponding clang++ command with the same standard and warning options. Treat -Wall as a named group of warnings, not as “every warning that exists”. During learning, avoid adding -Werror until you understand the diagnostics; it turns enabled warnings into build failures.
Use a debugger systematically
- Compile a debug build with symbols and without aggressive optimization, for example
g++ -std=c++17 -g -O0 -Wall -Wextra -Wpedantic main.cpp -o app. - Start GDB with
gdb ./app. - Set a breakpoint with
break mainorbreak file.cpp:line, then enterrun. - Use
nextto execute a source line without stepping into a called function, orstepto enter it. - Inspect state with
print variable; usebacktraceto inspect the call stack. - Use
continueto resume andquitto leave GDB.
A debugger lets you observe a particular execution. Form a hypothesis first (“the index becomes 5 before the condition is checked”), pause near that point, and inspect the relevant state. A debugger cannot by itself establish correctness for every possible input.
Test a contract, not just one example
This function returns the index of the first matching value, or -1 if no match exists. Its loop condition keeps every vector access within the valid index range.
#include <cstddef>
#include <iostream>
#include <vector>
int find_index(const std::vector<int>& values, int target) {
for (std::size_t i{0}; i < values.size(); ++i) {
if (values[i] == target) {
return static_cast<int>(i);
}
}
return -1;
}
int main() {
const std::vector<int> values{4, 8, 12};
std::cout << find_index(values, 4) << '\n';
std::cout << find_index(values, 12) << '\n';
std::cout << find_index(values, 7) << '\n';
std::cout << find_index({}, 7) << '\n';
}The cast is safe for this small teaching example because the vector has only three elements. In a general API, choose a return type and sentinel/optional result that can represent the full index range without narrowing.
| Test category | Example for this function | What it checks |
|---|---|---|
| First position | {4, 8, 12}, target 4 | Index 0 is included |
| Last position | Target 12 | Last valid index is included |
| Absent value | Target 7 | Search ends and returns -1 |
| Empty input | {} | No element is accessed |
Common debugging mistakes
- Changing many lines at once, so the cause of a new failure is unclear.
- Ignoring the first diagnostic and chasing secondary messages.
- Testing only the happy path or only one data value.
- Assuming a warning-free build or successful compilation proves correctness.
- Using print statements without a specific question about the program state.
Practice
- What is the difference between a compile error and a link error?
- Which GDB command pauses execution at the start of
main? - Why should a search test include an empty vector?
- Does
-Wallenable every warning a compiler can issue? - Why is a logic error different from a runtime failure?
Worked answers
Show reasoned answers
- A compile error prevents translation of a source unit; a link error occurs when the linker cannot resolve required symbols/definitions.
break mainsets a breakpoint;runstarts execution under GDB.- It checks that the function returns the documented “not found” result without accessing an element.
- No.
-Wallenables a documented group of warnings; other warnings may need separate options. - A runtime failure occurs during execution; a logic error is incorrect reasoning that can still produce a normal-looking execution with the wrong result.
Summary
Classify the failure, read the first useful diagnostic, build with suitable warnings and debug symbols, and investigate with a focused hypothesis. Test boundaries and invalid states as well as typical inputs; passing tests are evidence, not a proof of correctness.