Your C++ program prints 12 when you expected 10. Before changing a line, capture the input and both results. Then look for the earliest point where the program’s state stops matching what you expected. That gives you a smaller question to answer than “What’s wrong with this program?”
It also helps to identify the kind of failure. Code that will not compile, code that crashes or behaves unpredictably, and code that runs but gives the wrong answer call for different evidence. Compiler diagnostics can point to syntax or type errors; a debugger can show the path to a crash; a small test can expose a mistaken assumption in an algorithm.
Make the failure repeatable
Before editing, save a minimal input that triggers the problem. Record the exact build command, program arguments, and output. If the program reads a file, work from a local test copy instead of changing the only copy of important data. Change the input, compiler options, and source code all at once, and you may not know which change affected the result.
Now shrink the case without losing the failure. If a loop miscounts entries in a large file, try one with three entries. If a crash follows many operations, find the shortest sequence that still crashes. Smaller cases are quicker to check and sometimes make the mistaken assumption obvious.
- Expected: State the precise output or behavior, not just “it should work.”
- Actual: Copy the error message or output exactly.
- Trigger: Keep the smallest known input and command that reproduce it.
- Recent change: Note what changed, but treat it as a clue rather than proof.

Start with the compiler’s first useful diagnostic
When a build fails, read the first relevant error before chasing the rest. A missing brace can make later lines look invalid and produce a long list of secondary diagnostics. Check the cited line and a few lines above it; the parser may not notice a mistake until it reaches the next statement.
Enable warnings during ordinary development. For a small program built with GCC or Clang, this is a useful starting command:
g++ -std=c++20 -Wall -Wextra -Wpedantic -g -O0 main.cpp -o app
The warning flags flag suspicious constructs; -g adds debug information, and -O0 makes stepping through source code easier. Warnings are leads, not explanations for every bug. Read each one in context. A signed-versus-unsigned comparison warning, for instance, deserves a close look when you’re working with container sizes or indexes.
If a name is reported as undeclared, check its spelling and scope before adding another declaration. For a type mismatch, compare the values passed with the function signature. Fix one cause, rebuild, and see what remains. Don’t silence a warning just to get a clean build when you haven’t understood it yet.
Trace values at the point they change
For a wrong answer, pick one small input and write down the expected values after each important step. Suppose a loop counts values greater than 10 in {10, 11, 12}. The count should go from 0 to 0, then 1, then 2. If the program returns 3, inspect the comparison rather than the output statement: value >= 10 includes the boundary value; value > 10 does not.
Temporary logging helps when a debugger isn’t available or the program handles a short sequence. Print only values that could explain the result, with labels and loop indexes:
std::cerr << "index=" << i
<< " value=" << values[i]
<< " count=" << count << 'n';
std::cerr separates diagnostic output from normal std::cout output, though both may appear in the same terminal. Place the print immediately before or after the operation you’re checking, and know which state it shows. Hundreds of unrelated values can bury the useful one. Remove or deliberately disable diagnostic prints once the problem is resolved, especially if they might expose private input.
Use breakpoints to inspect a path, not just a line
A debugger lets you pause a program and inspect its state without repeatedly editing print statements. Most IDE debuggers let you set a breakpoint, run until it’s reached, inspect variables, and advance one step. GDB and LLDB offer similar controls in a terminal.
- Build with debug information and start the program under the debugger.
- Set a breakpoint shortly before the first state you distrust.
- Run with the input that reproduces the bug.
- Inspect relevant locals and the call stack.
- Step over statements to watch their effects; step into a function when its behavior is uncertain.
“Step over” executes a function call without entering its body. “Step into” follows the call; “step out” finishes the current function and returns to its caller. If a loop runs many times, use a conditional breakpoint or stop near the failing index instead of pausing on every iteration. Make sure the breakpoint was reached during the failing run. A line the program never visited cannot explain that run.
For a crash, inspect the call stack. It shows the active function calls, so you can work back from a low-level failure to the application code that supplied a bad value. A crash at a vector access does not necessarily make the vector the root cause; an earlier calculation may have produced an invalid index.

Check bounds, lifetimes, and undefined behavior
Some C++ defects don’t produce a consistent wrong answer. Reading outside an array, using an uninitialized variable, or accessing an object after its lifetime ends can cause undefined behavior. The program might crash, appear correct, or behave differently after an unrelated edit. One successful run is not proof that the code is safe.
Consider this loop over a vector:
for (std::size_t i = 0; i <= values.size(); ++i) {
total += values[i];
}
On the final iteration, i == values.size(), which is past the last valid index. The condition should be i < values.size(). When checking an index, inspect it alongside the container’s size on the same iteration. During diagnosis, values.at(i) can help too: unlike unchecked operator[], it throws an exception for out-of-range access. It does not fix the loop condition.
Where supported, make a separate diagnostic build with sanitizers:
g++ -std=c++20 -Wall -Wextra -g -O1
-fsanitize=address,undefined -fno-omit-frame-pointer
main.cpp -o app-check
Run app-check with the saved failing input. AddressSanitizer can detect many out-of-bounds and use-after-free errors; UndefinedBehaviorSanitizer catches a range of other undefined operations. Neither catches everything, and availability and reports vary by compiler and platform. Read the location and stack trace, then work backward to the decision that allowed the invalid operation. Keep this build separate from the normal release build.
Distinguish a bad calculation from bad input
If a variable seems to change unexpectedly, check where its value came from. A failed formatted-input extraction does not assign the value you intended. Code that uses the variable afterward can make a calculation look broken when the real problem is unhandled input failure.
int count{};
if (!(std::cin >> count)) {
std::cerr << "Expected an integer countn";
return 1;
}
For file-reading bugs, check that the file opened and that the loop processes each successful read once. Testing eof() before a read is a common mistake: the read itself typically discovers end-of-file. Use that read as the loop condition:
int value;
while (input >> value) {
process(value);
}
If the loop stops early, find out whether it reached the end normally or hit invalid input or another stream error. Another print after the loop won’t make that distinction for you. Use a small synthetic input in shared bug reports rather than real credentials, personal records, or production data.
Test a hypothesis with one controlled change
Once you suspect a cause, predict what a fix should do before you edit. For example: “If the loop includes the element at size(), changing <= to < should remove the out-of-range report for the three-element case.” Make that one change, rebuild with the same options, and rerun the same input. If the prediction fails, revisit the hypothesis instead of piling on speculative fixes.
When the immediate failure is gone, try nearby cases: an empty vector, one element, the boundary value, and a typical larger input. These probes don’t replace a considered test suite, but they can show whether the fix merely moved the failure. In a repository, review the diff for leftover logging, disabled checks, or experimental edits.
When the bug disappears in the debugger
Stepping changes timing, and a debug build can behave differently from an optimized one. First check that both runs use the same input, arguments, working directory, and relevant environment. Rebuild from the current source so you aren’t comparing an old executable with new code. If the difference persists, don’t assume the debugger caused the bug. Undefined behavior—and timing-dependent access in multithreaded programs—can produce inconsistent symptoms. A sanitizer run and a reduced reproducer give you better evidence than another edit based on one successful run.
To finish checking the index fix, rerun the input that first failed, then the empty and one-element cases. If you set a breakpoint before values[i], compare i with values.size() on the last iteration. With three elements, index 2 is the last valid one; the loop must stop before attempting index 3.
