Skip to content
C++
Library
since C++26
Basic

Debugging Utilities

C++26 <debugging> header — std::breakpoint(), std::is_debugger_present(), and std::breakpoint_if_debugging() for programmatic debugger integration.

<debugging>since C++26

The <debugging> header provides three standard functions — std::is_debugger_present, std::breakpoint, and std::breakpoint_if_debugging — for portable, programmatic interaction with an attached debugger.

Overview

Before C++26, breaking programmatically into a debugger required compiler-specific or OS-specific intrinsics:

  • MSVC: __debugbreak()
  • GCC/Clang on x86: __asm__("int3") or __builtin_trap()
  • POSIX: raise(SIGTRAP)

Each had different semantics, portability constraints, and optimiser interactions. C++26 standardises this through three functions in <debugging> (P2546R5).

std::is_debugger_present() queries the runtime environment for a debugger attachment. The mechanism is implementation-defined: on Windows this calls IsDebuggerPresent(); on Linux it typically parses TracerPid from /proc/self/status. Returns bool.

std::breakpoint() unconditionally triggers a breakpoint. If a debugger is attached, execution pauses. If no debugger is present, behaviour is implementation-defined — typically a trap signal that terminates the process. Use with care in production.

std::breakpoint_if_debugging() is equivalent to if (std::is_debugger_present()) std::breakpoint(). Safe to leave in production because it is a no-op when no debugger is attached.

All three functions are noexcept and impose no preconditions.

Syntax

cpp
#include <debugging>    // C++26

namespace std {
    bool is_debugger_present() noexcept;  // C++26
    void breakpoint() noexcept;           // C++26
    void breakpoint_if_debugging() noexcept; // C++26
}

Feature-test macro: __cpp_lib_debugging >= 202311L.

Examples

Conditional break at error site

cpp
#include <debugging>    // C++26
#include <stdexcept>
#include <span>

void process_packet(std::span<const std::byte> data) {
    if (data.empty()) [[unlikely]] {
        std::breakpoint_if_debugging();   // C++26 — no-op without a debugger
        throw std::invalid_argument{"empty packet"};
    }
    // ...
}

The breakpoint lands immediately before the throw, so a developer running under a debugger inspects live stack state at the exact failure site. Release builds running without a debugger are unaffected.

Custom debug assertion with source location

C++20's std::source_location pairs naturally with C++26 debugging utilities to replace the assert() macro:

cpp
#include <debugging>         // C++26
#include <source_location>   // C++20
#include <string_view>
#include <iostream>
#include <cstdlib>

void debug_assert(
    bool condition,
    std::string_view message,
    std::source_location loc = std::source_location::current()  // C++20
) noexcept {
    if (!condition) [[unlikely]] {
        std::cerr << loc.file_name() << ':' << loc.line()
                  << " [" << loc.function_name() << "]: "
                  << message << '\n';
        std::breakpoint_if_debugging();   // C++26
        std::abort();
    }
}

// At call site — loc is captured automatically
void update_cache(Cache& cache, int key) {
    debug_assert(key >= 0, "negative cache key");
    // ...
}

Unlike the assert() macro (available since C, inherited via <cassert> in C++98), this is a real function: it participates in overload resolution, cannot be accidentally bypassed by parenthesising the condition, and captures location without preprocessor stringification. The tradeoff is that it is not automatically stripped by NDEBUG — add that gate explicitly if you want parity with assert.

Tuning diagnostic verbosity at startup

cpp
#include <debugging>   // C++26
#include <spdlog/spdlog.h>

void configure_logging() {
    if (std::is_debugger_present()) {   // C++26
        spdlog::set_level(spdlog::level::trace);
        spdlog::warn("Debugger detected — trace logging active");
    } else {
        spdlog::set_level(spdlog::level::warn);
    }
}

Call is_debugger_present() once at startup and cache the result. On Linux the implementation reads from /proc, so hitting it in a hot loop has measurable cost.

Portability shim for pre-C++26 compilers

Until C++26 support is universal, an inline shim lets you adopt the standard interface today:

cpp
// debug_compat.hpp
#if defined(__cpp_lib_debugging) && __cpp_lib_debugging >= 202311L
#  include <debugging>
#else
#  include <cstdlib>
namespace std {

#  if defined(_MSC_VER)
    inline bool is_debugger_present() noexcept {
        return static_cast<bool>(::IsDebuggerPresent());
    }
    inline void breakpoint() noexcept { __debugbreak(); }
#  elif defined(__GNUC__) || defined(__clang__)
    inline bool is_debugger_present() noexcept {
        // Best-effort: parse TracerPid from /proc/self/status
        // Omitted for brevity; return false as a safe fallback
        return false;
    }
    inline void breakpoint() noexcept {
#    if defined(__i386__) || defined(__x86_64__)
        __asm__ volatile("int3");
#    else
        __builtin_trap();
#    endif
    }
#  else
    inline bool is_debugger_present() noexcept { return false; }
    inline void breakpoint() noexcept { std::abort(); }
#  endif

    inline void breakpoint_if_debugging() noexcept {
        if (is_debugger_present()) breakpoint();
    }

} // namespace std
#endif

This is a drop-in: once the compiler ships <debugging>, delete the shim and the include.

Integration with Sanitizers

AddressSanitizer (ASan) and UndefinedBehaviorSanitizer (UBSan) detect errors at runtime, but by default they print a diagnostic report and either continue or call abort() — neither of which gives you a live debugger session at the error site.

Getting a debugger stop on a sanitizer error

For ASan, set ASAN_OPTIONS=abort_on_error=1 before running the binary. This replaces the default error dump with a call to abort(), which produces SIGABRT. When a debugger is attached, most debuggers stop on SIGABRT by default, landing you right at the point of the memory violation:

bash
ASAN_OPTIONS=abort_on_error=1 gdb --args ./my_program

For UBSan, use -fsanitize-trap=all at compile time to emit a SIGTRAP instruction on every violation instead of calling the runtime library. A debugger catches SIGTRAP and suspends execution inline — no handler, no stack unwind, no missed context:

bash
# Compile
clang++ -fsanitize=undefined -fsanitize-trap=all -g -o my_program my_program.cpp

# Run under gdb — stops at every UB site
gdb ./my_program

Alternatively, UBSan's runtime supports UBSAN_OPTIONS=abort_on_error=1 for the same stop-on-abort behaviour as ASan without recompiling.

Relationship to breakpoint_if_debugging()

Sanitizer interception happens at the error site inside the sanitizer runtime — before your own code resumes. This means a std::breakpoint_if_debugging() call you placed after a bad memory access fires only if the process survives the sanitizer check. The correct pattern is to rely on the sanitizer's own trap/abort mechanism for initial discovery, then use std::breakpoint_if_debugging() for application-level invariant checks that you want to catch under a debugger:

cpp
#include <debugging>

void process(int* ptr, std::size_t n) {
    // std::breakpoint_if_debugging() fires here during development
    // ASan/UBSan would already have stopped on a bad ptr before this runs
    std::breakpoint_if_debugging();
    for (std::size_t i = 0; i < n; ++i)
        ptr[i] = 0;
}

Do not combine ASAN_OPTIONS=abort_on_error=1 with production binaries — it disables ASan's leak summary report and may confuse CI signal interpretation.

Breakpoints and C++26 Contracts

C++26 Contracts (P2900) add pre, post, and contract_assert — language-level assertion constructs that replace the assert() macro. When a contract assertion fails under the enforce or observe evaluation semantic, the implementation invokes a contract-violation handler before terminating.

Default handler behaviour

The default ::handle_contract_violation provided by the implementation logs a diagnostic and either returns normally (for observe) or terminates (for enforce). Under a debugger this means the process disappears without giving you a chance to inspect state.

Installing a debuggable violation handler

Because the contract-violation handler is a replaceable function (like a custom operator new), you can install your own to call std::breakpoint_if_debugging() before termination:

cpp
#include <debugging>      // C++26
#include <contracts>      // C++26 — provides std::contracts::contract_violation
#include <iostream>
#include <cstdlib>

// Replaces the default handler — linked in once, applies program-wide
void handle_contract_violation(
    const std::contracts::contract_violation& v) noexcept
{
    std::cerr << "Contract violation at "
              << v.location().file_name() << ':'
              << v.location().line() << '\n'
              << "  assertion: " << v.comment() << '\n';

    std::breakpoint_if_debugging();  // C++26 — pause if a debugger is attached

    // Default enforce behaviour: terminate after the handler returns for enforce,
    // but return normally for observe so execution continues.
    // std::abort() here forces termination regardless of semantic.
    std::abort();
}

With this handler in place, any contract failure under a debugger produces a hard stop at the std::breakpoint_if_debugging() call, with the full call stack and local variables intact. Without a debugger the handler logs and aborts as before.

Controlling evaluation semantics

Compilers implementing P2900 accept flags to select the evaluation semantic globally:

bash
# GCC / Clang
-fcontract-semantic=enforce   # check + terminate on failure (default)
-fcontract-semantic=observe   # check + continue on failure (useful in CI)
-fcontract-semantic=ignore    # disable all contract checks (release builds)

Pair observe with a custom handler that calls breakpoint_if_debugging() during QA runs to surface soft violations without terminating; switch to enforce (or ignore) for release.

Build Configuration

std::breakpoint_if_debugging() is always available at runtime but is a no-op without a debugger. For tighter control — ensuring breakpoint-related code is entirely absent from optimised release binaries — you need a build-system gate.

CMake: enabling breakpoints only in Debug builds

CMake's CMAKE_BUILD_TYPE controls which compiler flags are applied. The Debug configuration does not define NDEBUG; Release and MinSizeRel do (-DNDEBUG). Use a generator expression to pass a preprocessor definition only for non-Debug configs:

cmake
# CMakeLists.txt
add_executable(my_app main.cpp)
target_compile_options(my_app PRIVATE
    $<$<CONFIG:Debug>:-g -O0>
    $<$<CONFIG:Release,RelWithDebInfo,MinSizeRel>:-DNDEBUG>
)

Then guard breakpoint calls in application code with the NDEBUG macro:

cpp
#include <debugging>  // C++26

#ifdef NDEBUG
  #define DEBUG_BREAK() ((void)0)
#else
  #define DEBUG_BREAK() std::breakpoint_if_debugging()
#endif

void validate_index(std::size_t idx, std::size_t size) {
    if (idx >= size) [[unlikely]] {
        DEBUG_BREAK();
        throw std::out_of_range{"index out of bounds"};
    }
}

Multi-config generators (Visual Studio, Xcode, Ninja Multi-Config)

For multi-config generators, CMAKE_BUILD_TYPE has no effect at configure time — the config is chosen at build time. Use $<CONFIG:Debug> generator expressions exclusively:

cmake
target_compile_definitions(my_app PRIVATE
    $<$<NOT:$<CONFIG:Debug>>:NDEBUG>
)

This ensures NDEBUG is defined in all non-Debug configurations without hardcoding build-type strings.

RelWithDebInfo (-O2 -g, no -DNDEBUG) is the ideal configuration for sanitizer runs: optimisations expose real UB, debug symbols give meaningful stack traces, and NDEBUG is absent so assert() and custom DEBUG_BREAK() guards remain active. Breakpoints placed via breakpoint_if_debugging() are live; sanitizer traps also fire normally.

Best Practices

  • Default to breakpoint_if_debugging() in library and production code. Unconditional breakpoint() can crash processes that have no debugger; the conditional variant is safe to ship in all configurations.
  • Place breakpoints as close to the error source as possible, not in a catch-all error handler. Stack state and local variables remain intact, making the pause maximally useful.
  • Cache is_debugger_present() at startup when called in a loop or tight path. Linux implementations perform a file read; the overhead is negligible once but adds up at high frequency.
  • Annotate branches with [[unlikely]] on the path that leads to a breakpoint. This communicates intent and allows the compiler to lay out the fast path without penalty.
  • Do not use is_debugger_present() for security decisions. TracerPid can be spoofed; containerised environments sometimes produce false negatives. The API exists for developer ergonomics, not anti-tamper protection.

Common Pitfalls

Calling std::breakpoint() without a guard in non-debug builds. Without a debugger attached, the implementation typically raises SIGTRAP or equivalent, terminating the process. Use breakpoint_if_debugging() unless a forced crash is the explicit goal.

Conflating __builtin_trap() with std::breakpoint(). __builtin_trap() is documented by GCC/Clang as producing an abnormal termination instruction and may be treated as UB-adjacent by the optimiser in some contexts. std::breakpoint() has defined, standard semantics. They are not interchangeable.

Assuming <debugging> respects NDEBUG. Unlike assert() from <cassert>, the functions in <debugging> are always active regardless of NDEBUG. If you want NDEBUG-sensitive breakpoints, wrap the calls in a macro or if constexpr predicate yourself.

Forgetting that is_debugger_present() accuracy varies by platform. On macOS and Windows the result is generally reliable. On Linux under Docker or certain sandboxes, /proc/self/status may be unavailable or TracerPid may read as 0 even under gdb/lldb. Treat the result as best-effort.

Stack Traces with std::stacktrace (C++23)

std::stacktrace is the first standard facility for capturing a snapshot of the call stack at runtime. Include <stacktrace> to obtain a std::stacktrace object that holds zero or more std::stacktrace_entry values, each representing one frame.

cpp
// C++23
#include <stacktrace>
#include <iostream>

void inner() {
    auto trace = std::stacktrace::current();   // C++23 — captures here
    for (const auto& frame : trace) {
        std::cout << frame.source_file()   << ':'
                  << frame.source_line()   << "  "
                  << frame.description()   << '\n';
    }
}

void middle() { inner(); }
void outer()  { middle(); }

int main() { outer(); }

std::stacktrace::current() accepts an optional skip count (default 0) and a max_depth limit so you can trim implementation-internal frames from the top or cap a deep trace for performance.

Filtering frames

Raw stack traces often include noise from the standard library and runtime. A simple predicate filter keeps only application frames:

cpp
// C++23
#include <stacktrace>
#include <string_view>
#include <vector>

std::vector<std::stacktrace_entry>
app_frames(const std::stacktrace& trace, std::string_view src_prefix) {
    std::vector<std::stacktrace_entry> result;
    for (const auto& f : trace)
        if (std::string_view{f.source_file()}.starts_with(src_prefix))
            result.push_back(f);
    return result;
}

Pass the project source root as src_prefix (e.g. "/home/user/myproject/") to discard system and third-party frames before logging or displaying the trace.

Capturing a trace inside an exception handler

The most useful place to capture a std::stacktrace is at the throw site, not the catch site — by the time execution reaches catch, the intermediate frames have already been unwound. Embed the trace in a custom exception type:

cpp
// C++23
#include <stacktrace>
#include <stdexcept>
#include <string>

class traced_error : public std::runtime_error {
public:
    explicit traced_error(const std::string& msg)
        : std::runtime_error{msg}
        , trace_{std::stacktrace::current(/*skip=*/1)} // skip this constructor
    {}

    const std::stacktrace& trace() const noexcept { return trace_; }

private:
    std::stacktrace trace_;
};

// Usage
void risky() {
    throw traced_error{"something went wrong"};
}

int main() {
    try { risky(); }
    catch (const traced_error& e) {
        std::cerr << e.what() << '\n' << e.trace() << '\n';
    }
}

std::stacktrace is printable directly via operator<< or std::to_string(trace). The output format is implementation-defined but typically includes symbol names and source coordinates when the binary was compiled with -g.

Custom Assertion Macros with std::source_location

std::source_location (C++20) provides compile-time call-site information without the preprocessor, making it possible to build assertion helpers that are real functions rather than macros — with proper type-checking, ODR compliance, and debugger-friendly stack frames.

A minimal drop-in for assert() that emits structured diagnostics:

cpp
// C++20
#include <source_location>
#include <string_view>
#include <cstdio>
#include <cstdlib>

[[noreturn]] void assertion_failed(
    std::string_view expression,
    std::string_view message,
    std::source_location loc = std::source_location::current()
) noexcept {
    std::fprintf(stderr,
        "Assertion failed: %.*s\n"
        "  Message  : %.*s\n"
        "  File     : %s\n"
        "  Line     : %u\n"
        "  Function : %s\n",
        static_cast<int>(expression.size()), expression.data(),
        static_cast<int>(message.size()),    message.data(),
        loc.file_name(),
        loc.line(),
        loc.function_name()
    );
    std::abort();
}

// Thin macro wrapper — only role is to stringify the expression
#define ASSERT(cond, msg) \
    do { if (!(cond)) [[unlikely]] assertion_failed(#cond, msg); } while(false)

The macro boundary is intentionally minimal: it exists solely to stringify cond via #cond. All diagnostic logic lives in the [[noreturn]] function, which the compiler can inline, devirtualise, and reason about for control-flow analysis. The [[unlikely]] attribute on the failure branch keeps the fast path free of branch-prediction pressure.

Variant: returning rather than aborting

For libraries that must not call std::abort(), return a bool and let the caller decide:

cpp
// C++20
#include <source_location>
#include <string_view>
#include <iostream>

inline bool check(
    bool condition,
    std::string_view message,
    std::source_location loc = std::source_location::current()
) noexcept {
    if (!condition) [[unlikely]] {
        std::cerr << "[CHECK FAILED] " << message
                  << " @ " << loc.file_name() << ':' << loc.line()
                  << " (" << loc.function_name() << ")\n";
    }
    return condition;
}

Callers can write if (!check(ptr != nullptr, "null pointer")) return std::unexpected{Error::NullPtr};, composing naturally with std::expected error propagation without unwinding the stack.

std::breakpoint_if_debugging() — Conditional Programmatic Breakpoints (C++26)

std::breakpoint_if_debugging() is the safe variant of std::breakpoint(): it tests for debugger attachment first and only traps if one is present. This makes it possible to scatter breakpoint-quality stops throughout code that ships to end users without the risk of terminating a process running outside a debugger session.

Using it as a watchpoint substitute

Hardware watchpoints monitor a memory address and stop on any write. For cases where the address is not known at debugger start time — e.g. a dynamically allocated buffer — you can approximate the behaviour by calling breakpoint_if_debugging() inside a setter:

cpp
// C++26
#include <debugging>

class WatchedValue {
public:
    void set(int v) noexcept {
        value_ = v;
        std::breakpoint_if_debugging();  // C++26 — stop every time value changes
    }
    int get() const noexcept { return value_; }
private:
    int value_{};
};

Under a debugger, each set() call produces a stop. In production — or during normal test runs without a debugger — the call is a no-op and the overhead is negligible (one is_debugger_present() check, typically a single syscall or memory read cached by the OS).

Pairing with [[unlikely]] and cold functions

Mark the breakpoint path [[unlikely]] so the optimiser routes the hot path through branch-free code:

cpp
// C++26
#include <debugging>
#include <cstddef>

void validate_alignment(const void* ptr, std::size_t alignment) noexcept {
    if (reinterpret_cast<std::uintptr_t>(ptr) % alignment != 0) [[unlikely]] {
        std::breakpoint_if_debugging();  // C++26
    }
}

The [[unlikely]] hint lets the compiler place the cold path (and the function call to is_debugger_present()) in a separate code section, so the alignment check in the hot path compiles down to a branch that is almost never taken.

Integrating C++23/26 Debugging Utilities into Logging Frameworks

Structured logging becomes dramatically more actionable when log entries carry the exact file, line, function, and stack trace from the site that produced them. The C++20/23/26 debugging facilities make this possible without macros.

Structured log entry with std::source_location

cpp
// C++20
#include <source_location>
#include <string>
#include <string_view>
#include <chrono>
#include <iostream>
#include <format>  // C++20

struct LogEntry {
    std::string_view message;
    std::source_location location;
    std::chrono::system_clock::time_point timestamp;
};

void log_error(
    std::string_view message,
    std::source_location loc = std::source_location::current()
) {
    LogEntry entry{
        .message   = message,
        .location  = loc,
        .timestamp = std::chrono::system_clock::now(),
    };
    std::cout << std::format("[ERROR] {} | {}:{} | {}\n",
        entry.message,
        entry.location.file_name(),
        entry.location.line(),
        entry.location.function_name()
    );
}

Every call to log_error("disk full") records the call site automatically — no __FILE__/__LINE__ macros required. Structured logging back-ends (JSON, OpenTelemetry, etc.) can consume LogEntry fields individually rather than parsing a formatted string.

Adding a std::stacktrace to error log entries

For error-level logs, attaching a stack trace turns a log line into a self-contained incident report:

cpp
// C++23
#include <source_location>
#include <stacktrace>
#include <string_view>
#include <iostream>
#include <format>  // C++20

void log_critical(
    std::string_view message,
    std::source_location loc = std::source_location::current()
) {
    auto trace = std::stacktrace::current(/*skip=*/1); // skip log_critical itself
    std::cerr << std::format("[CRITICAL] {} @ {}:{}\n",
        message,
        loc.file_name(),
        loc.line()
    );
    // Print application frames only
    for (const auto& frame : trace) {
        std::string_view file{frame.source_file()};
        if (!file.empty())
            std::cerr << std::format("    {}:{} {}\n",
                file, frame.source_line(), frame.description());
    }
}

skip=1 removes the log_critical frame itself so the first printed frame is always the direct caller — the site that detected the problem.

Triggering a debugger stop from within the logging framework

Logging a critical error and immediately pausing for a debugger makes the log entry and the live program state available simultaneously:

cpp
// C++26
#include <debugging>
#include <source_location>
#include <string_view>
#include <iostream>

void log_and_break(
    std::string_view message,
    std::source_location loc = std::source_location::current()
) noexcept {
    std::cerr << "[BREAK] " << message
              << " @ " << loc.file_name() << ':' << loc.line() << '\n';
    std::breakpoint_if_debugging();  // C++26 — live stop if debugger is attached
}

In a CI run without a debugger, log_and_break degrades to a plain std::cerr write. Under gdb or lldb, it stops at the breakpoint_if_debugging() call with the message already written to stderr, letting you inspect locals before deciding whether to continue.

See Also

  • <cassert> — assert() macro stripped by NDEBUG, available since C++98
  • static_assert — compile-time assertion with no runtime overhead, introduced in C++11
  • std::source_location — zero-cost call-site capture (file, line, function), introduced in C++20