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.
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:
ASAN_OPTIONS=abort_on_error=1 gdb --args ./my_programFor 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:
# Compile
clang++ -fsanitize=undefined -fsanitize-trap=all -g -o my_program my_program.cpp
# Run under gdb — stops at every UB site
gdb ./my_programAlternatively, 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:
#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:
#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:
# 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:
# 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:
#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:
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: the recommended sanitizer config
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. 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.
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.
// 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:
// 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:
// 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:
// 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:
// 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:
// 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:
// 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
// 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:
// 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:
// 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 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