Debugging, Testing and Best Practices
Writing C++ that works — and keeps working — takes more than knowing the language. This lesson covers debuggers, unit testing and the habits experienced C++ developers rely on, summarised as a practical checklist.
Compiler settings for development
g++ -std=c++20 -Wall -Wextra -pedantic -Wshadow -Wconversion -g -O0 \
-fsanitize=address,undefined -D_GLIBCXX_ASSERTIONS main.cpp -o main
Treat warnings as errors (-Werror) in CI, so they never accumulate. Release builds use -O2 without sanitizers.
Using a debugger
GDB (Linux) and LLDB (macOS) let you pause a program, step through it and inspect variables. IDEs — Visual Studio, CLion, VS Code — wrap the same features in a graphical interface:
| Task | GDB | LLDB |
|---|---|---|
| Start | gdb ./main | lldb ./main |
| Breakpoint | break inventory.cpp:42 | b inventory.cpp:42 |
| Break when an exception is thrown | catch throw | break set -E c++ |
| Run, step over, step into | run, next, step | run, next, step |
| Print a variable or expression | print v.size() | p v.size() |
| Call stack | backtrace | bt |
Debuggers can pretty-print standard containers, so print names shows a vector's contents. Compile with -g -O0 for the most faithful stepping.
Assertions
assert checks assumptions in debug builds; static_assert checks them at compile time:
#include <cassert> #include <iostream> #include <vector> double average(const std::vector<double>& values) { assert(!values.empty() && "average of an empty vector"); double sum = 0; for (double v : values) sum += v; return sum / static_cast<double>(values.size()); } static_assert(sizeof(double) == 8, "this code assumes 64-bit doubles"); int main() { std::cout << average({2, 4, 9}) << '\n'; }
5
Assertions are compiled out when NDEBUG is defined (as CMake's Release builds do), so they must not contain side effects.
Unit testing
Popular frameworks are GoogleTest, Catch2 and doctest. They all follow the same pattern: small test functions with checks, discovered and run automatically. A Catch2 test looks like this:
#include <catch2/catch_test_macros.hpp> #include "fraction.hpp" TEST_CASE("fractions are reduced") { Fraction f{6, 8}; REQUIRE(f.numerator() == 3); REQUIRE(f.denominator() == 4); } TEST_CASE("zero denominator is rejected") { REQUIRE_THROWS_AS((Fraction{1, 0}), std::invalid_argument); }
Even without a framework, a test program built from assertions catches regressions:
#include <cassert> #include <iostream> #include <string> std::string to_title_case(std::string s) { bool start = true; for (char& c : s) { if (start && c >= 'a' && c <= 'z') c = static_cast<char>(c - 'a' + 'A'); start = (c == ' '); } return s; } int main() { assert(to_title_case("") == ""); assert(to_title_case("hello") == "Hello"); assert(to_title_case("the old man") == "The Old Man"); assert(to_title_case("Already Fine") == "Already Fine"); std::cout << "all tests passed\n"; }
all tests passed
Tooling
- clang-format — automatic, consistent formatting; check a
.clang-formatfile into the repository. - clang-tidy — finds bugs and suggests modernisations (
modernize-*,bugprone-*,cppcoreguidelines-*checks). - Sanitizers — ASan, UBSan, TSan (lessons 47, 51).
- Profilers —
perf, Instruments, Visual Studio's profiler — measure before optimising. - Compiler Explorer — see the machine code your C++ produces.
Best-practice checklist
Resources and memory
- Use values, containers and RAII; no naked
new/delete. std::make_uniqueby default,std::make_sharedonly for shared ownership; raw pointers never own.- Follow the rule of zero; write the rule of five only for resource-managing classes.
Types and interfaces
- Initialise everything; prefer
{}to catch narrowing. - Use
constandconstexprwherever possible; mark member functionsconstwhen they don't modify. enum classover plain enums;std::string_view/std::spanfor read-only parameters.- Make single-argument constructors
explicit; useoverrideon every overriding function and a virtual destructor in polymorphic bases. - Express "maybe" with
std::optional, alternatives withstd::variant, recoverable errors with exceptions orstd::expected.
Code
- Prefer standard algorithms and ranges to hand-written loops when they express intent better.
- Keep functions short and focused; return results by value.
- Never
using namespace std;in headers. - Constrain templates with concepts.
Process
- Warnings on and fixed; sanitizers and tests in CI; formatter and linter enforced.
- Use version control (see the Git course) and code review.
- Read the C++ Core Guidelines — the reference for modern C++ style, maintained by Bjarne Stroustrup and Herb Sutter.
Try it yourself
Write assertion-based tests for a function that checks whether a string is a valid hexadecimal colour like #1a2B3c.
all colour tests passed