Debugging Utilities
C++26 <debugging> header — std::breakpoint(), std::is_debugger_present(), and std::breakpoint_if_debugging() for programmatic debugger integration.
<debugging>since C++26The <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
#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
#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:
#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
#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:
// 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
#endifThis 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. Unconditionalbreakpoint()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.TracerPidcan 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 byNDEBUG, available since C++98static_assert— compile-time assertion with no runtime overhead, introduced in C++11std::source_location— zero-cost call-site capture (file, line, function), introduced in C++20