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.

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.

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