Skip to content
C++

Debugging C++ — Techniques

C++ debugging techniques — sanitizers, print debugging, assertions, core dumps, watchpoints, and debugging optimized code.

Compile-Time Debugging

cpp
// static_assert — catch errors before running
static_assert(sizeof(MyStruct) == 24, "size changed — check padding");
static_assert(std::is_trivially_copyable_v<Packet>);

// constexpr evaluation — test at compile time
constexpr auto result = my_function(42);
static_assert(result == expected_value);

// type printing trick (works via error message)
template<typename T> struct ShowType;
ShowType<decltype(my_var)>{};  // error: incomplete type ShowType<int> — reveals type

Sanitizers (Best First Line of Defense)

bash
# AddressSanitizer — detects: use-after-free, buffer overflow, use-after-scope
clang++ -fsanitize=address -g -O1 myapp.cpp

# UndefinedBehaviorSanitizer — detects: signed overflow, null deref, misaligned access
clang++ -fsanitize=undefined -g myapp.cpp

# ThreadSanitizer — detects: data races, deadlocks
clang++ -fsanitize=thread -g -O1 myapp.cpp

# LeakSanitizer — detects: memory leaks (built into ASAN)
ASAN_OPTIONS=detect_leaks=1 ./myapp

# MemorySanitizer — detects: use of uninitialized values (Clang only)
clang++ -fsanitize=memory -g myapp.cpp

# Combine (except thread + address):
clang++ -fsanitize=address,undefined -g -O1 myapp.cpp

cpp
// Structured debug print
#ifdef DEBUG
#define DPRINT(x) std::println("[{}:{}] {} = {}", __FILE__, __LINE__, #x, (x))
#else
#define DPRINT(x)
#endif

DPRINT(some_variable);   // [main.cpp:42] some_variable = 17

// Debug-only code block
#ifdef NDEBUG
    // release
#else
    // debug
#endif

// C++20 source_location for logging without macros
void log(auto value, std::source_location loc = std::source_location::current()) {
    std::println("[{}:{}] {}", loc.file_name(), loc.line(), value);
}

Core Dumps (Linux)

bash
# Enable core dumps (disabled by default)
ulimit -c unlimited

# Run the crashing program
./myapp  # → "Segmentation fault (core dumped)" → core file created

# Analyze with GDB
gdb ./myapp core
(gdb) bt              # backtrace
(gdb) frame 3         # select frame 3
(gdb) info locals     # see local variables
(gdb) print var       # print a variable

# Set core dump location (systemd)
echo '/tmp/core.%e.%p' | sudo tee /proc/sys/kernel/core_pattern

GDB Quick Commands

cpp
gdb ./myapp
  break main              set breakpoint at main
  break file.cpp:42       breakpoint at line 42
  run arg1 arg2           run program
  continue (c)            continue execution
  next (n)                next line (step over)
  step (s)                step into
  finish                  run until current function returns
  bt                      backtrace (call stack)
  frame N                 select frame N
  info locals             local variables in current frame
  print expr              print expression
  watch var               watchpoint: stop when var changes
  x/10x $rsp             examine 10 hex words at stack pointer
  thread apply all bt     backtrace all threads
  quit                    exit GDB

Debugging Optimized Code

cpp
// Problem: optimizer removes variables and reorders code
// -O2 makes values "optimized out" in the debugger

// Use __attribute__((optimize("O0"))) on specific functions (GCC/Clang)
[[gnu::optimize("O0")]]  // debug this function even at -O2
void debug_this() {
    // variables visible in debugger
}

// Or compile specific .cpp with -O0 via CMake:
// set_source_files_properties(suspect.cpp PROPERTIES COMPILE_FLAGS "-O0 -g")

// Prevent optimization of a variable (barrier for debugging)
volatile int debug_val = suspicious_result;
// The compiler can't optimize away the read, so you can inspect it

Valgrind

bash
# Memory error detection (slower than ASan but no recompile needed)
valgrind --tool=memcheck ./myapp

# Detailed leak check
valgrind --tool=memcheck --leak-check=full --show-leak-kinds=all ./myapp

# Cache profiling
valgrind --tool=cachegrind ./myapp
cg_annotate cachegrind.out.PID

# Call graph
valgrind --tool=callgrind ./myapp
kcachegrind callgrind.out.PID  # GUI visualization

Common Bug Patterns

bash
# Segfault at startup: usually static initialization order fiasco
# → Add -Wl,-z,defs to catch undefined symbols
# → Use local static / Meyers singleton

# Random crashes: usually memory corruption
# → Run with -fsanitize=address first
# → Look for buffer overflows, use-after-free, stack corruption

# Deadlock: program hangs
# → gdb + "thread apply all bt" to see where all threads are waiting
# → Run with -fsanitize=thread

# Data race: incorrect output, nondeterministic
# → -fsanitize=thread

# Slow performance: profile before optimizing
# → perf stat ./myapp (hardware counters)
# → perf record + perf report (sampling)
# → valgrind --tool=cachegrind (cache misses)

Compile Flags for Debugging

cmake
# CMake: debug build
cmake -DCMAKE_BUILD_TYPE=Debug ..

# Add sanitizers in CMake
target_compile_options(myapp PRIVATE
    -g3           # maximum debug info (includes macros)
    -fno-omit-frame-pointer   # reliable backtraces
    -fsanitize=address,undefined
)
target_link_options(myapp PRIVATE -fsanitize=address,undefined)

# Enable extra warnings (catch bugs at compile time)
target_compile_options(myapp PRIVATE
    -Wall -Wextra -Wpedantic
    -Wshadow -Wconversion -Wsign-conversion
    -Wunused -Wnull-dereference
)
Edit on GitHub