C++23 std::stacktrace for Diagnostic Debugging
The introduction of the <stacktrace> library in
C++23 brings standardized, native programmatic stack tracing to the
language for the first time. This article explores how
std::stacktrace enables developers to capture, inspect, and
format execution call stacks directly within application code without
relying on third-party libraries or platform-specific APIs. By
integrating call-site information directly into error handling, logging,
and assertion frameworks, C++23 substantially improves runtime
diagnostics and debugging workflows.
The Problem with Pre-C++23 Diagnostics
Prior to C++23, obtaining a call stack during runtime required
platform-dependent APIs—such as backtrace() on POSIX
systems or CaptureStackBackTrace() alongside the DbgHelp
library on Windows. Alternatively, developers had to rely on external
dependencies like Boost.Stacktrace. This lack of standardization made
portable diagnostic reporting difficult, often forcing developers to
debug production crashes using post-mortem core dumps or external log
parsers rather than handling diagnostics programmatically inside the
running process.
Capturing Call Stacks
with std::stacktrace
C++23 solves this by introducing std::basic_stacktrace
and its common alias std::stacktrace inside the
<stacktrace> header. Capturing the current execution
path requires only a single call to
std::stacktrace::current():
#include <iostream>
#include <stacktrace>
void log_failure() {
std::stacktrace trace = std::stacktrace::current();
std::cout << std::to_string(trace) << '\n';
}The current() factory function records a snapshot of the
call stack at that exact point of execution. The resulting object acts
as a standard sequence container of std::stacktrace_entry
objects, allowing iteration and programmatic inspection of individual
frames.
Inspecting Stack Frames Programmatically
A major diagnostic advantage is the ability to query individual stack
frame metadata directly in code. Each std::stacktrace_entry
provides member functions to retrieve source code details:
description(): Returns the demangled name of the function or symbol.source_file(): Returns the file path where the call occurred.source_line(): Returns the line number corresponding to the frame.native_handle(): Provides the underlying implementation-defined representation (such as an instruction pointer address).
Because these values can be evaluated as individual fields, diagnostic frameworks can filter, sanitize, or parse stack frames before writing them to structured logs (e.g., JSON output for log aggregators).
Diagnostic Exception Handling
std::stacktrace significantly improves custom exception
diagnostics. In standard C++, when an exception is caught, the stack has
already unwound, making it impossible to determine the original call
path that led to the throw. By capturing the trace inside the
exception's constructor, the original throw site is preserved:
#include <exception>
#include <stacktrace>
#include <string>
class TracedException : public std::exception {
std::string message_;
std::stacktrace trace_;
public:
TracedException(std::string message, std::stacktrace trace = std::stacktrace::current())
: message_(std::move(message)), trace_(std::move(trace)) {}
const char* what() const noexcept override {
return message_.c_str();
}
const std::stacktrace& trace() const noexcept {
return trace_;
}
};Using default arguments, std::stacktrace::current()
evaluates automatically at the throw site, ensuring developers receive
accurate contextual backtraces wherever the exception is handled.
Integration with
std::format
C++23 also integrates stack traces directly into the
std::format and std::print ecosystems. Both
std::stacktrace and std::stacktrace_entry have
built-in formatter specializations:
#include <print>
#include <stacktrace>
void report_checkpoint() {
std::println("Current execution trace:\n{}", std::stacktrace::current());
}This formatting automatically resolves and demangles symbols when debugging symbols are available in the binary, producing clean and human-readable diagnostic messages.
Performance and Debugging Overhead
Stack capturing separates the physical collection of instruction
addresses from symbol resolution. Invoking
std::stacktrace::current() primarily captures program
counters, which is relatively fast. The more expensive
operation—resolving symbols, demangling function names, and extracting
source lines—occurs lazily when converting the trace to a string or
querying specific entry functions. This separation allows systems to
capture traces on non-fatal errors or critical paths without paying an
immediate symbol-resolution performance penalty unless the diagnostic
information is actively requested.