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:

TaskGDBLLDB
Startgdb ./mainlldb ./main
Breakpointbreak inventory.cpp:42b inventory.cpp:42
Break when an exception is throwncatch throwbreak set -E c++
Run, step over, step intorun, next, steprun, next, step
Print a variable or expressionprint v.size()p v.size()
Call stackbacktracebt

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:

C++
#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';
}
Output
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:

C++
#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:

C++
#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";
}
Output
all tests passed

Tooling

  • clang-format — automatic, consistent formatting; check a .clang-format file 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_unique by default, std::make_shared only 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 const and constexpr wherever possible; mark member functions const when they don't modify.
  • enum class over plain enums; std::string_view/std::span for read-only parameters.
  • Make single-argument constructors explicit; use override on every overriding function and a virtual destructor in polymorphic bases.
  • Express "maybe" with std::optional, alternatives with std::variant, recoverable errors with exceptions or std::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.

Try it yourself · main.cpp
Edit the code, copy it into main.cpp, then compile and run it with c++ -std=c++20 -Wall main.cpp -o main && ./main. How to set up a C++ compiler
Output
all colour tests passed