библиотека — диагностикаlibrary — diagnostics
Портируемый, без платформенных API, доступ к текущему стеку вызовов — std::stacktrace::current() и итерация по кадрам с файлом, строкой и именем функции для каждого.Portable, platform-API-free access to the current call stack — std::stacktrace::current() plus iteration over frames, each with a file, a line, and a function name.
std::stacktrace (заголовок <stacktrace>) даёт портируемый доступ к стеку вызовов: std::stacktrace::current() возвращает объект, представляющий стек в момент вызова, по которому можно итерироваться — каждый элемент, std::stacktrace_entry, несёт файл, строку и имя функции для одного кадра. Это тот же набор данных, что у std::source_location (см. cpp20/source-location.html), но не для одной точки программы, а для всей цепочки вызовов, приведшей к текущему месту.std::stacktrace (header <stacktrace>) provides portable access to the call stack: std::stacktrace::current() returns an object representing the stack at the moment of the call, over which you can iterate — each element, a std::stacktrace_entry, carries a file, a line, and a function name for one frame. It's the same kind of data as std::source_location (see cpp20/source-location.html), but not for a single point in the program — for the entire chain of calls that led there.
#ifdef _WIN32
#include <dbghelp.h>
void print_stack() { /* CaptureStackBackTrace + SymFromAddr *//* CaptureStackBackTrace + SymFromAddr */ }
#else
#include <execinfo.h>
void print_stack() { /* backtrace + backtrace_symbols *//* backtrace + backtrace_symbols */ }
#endif
// два независимых куска платформенного кода на одну и ту же задачу// two independent chunks of platform code for the same job
#include <stacktrace>
void print_stack() {
std::cout << std::stacktrace::current() << "\n";
}
// одна и та же функция без #ifdef на POSIX, Windows, где угодно// the same function with no #ifdef, on POSIX, Windows, anywhere
Получение стека вызовов в портируемом C++ коде было невозможно до C++23 без сторонних средств:Getting the call stack in portable C++ code was impossible before C++23 without third-party means:
POSIX-системы (через glibc) давали backtrace()/backtrace_symbols(), Windows — CaptureStackBackTrace()/SymFromAddr() из DbgHelp. Ни один из вариантов не работал на другой платформе — любой проект, желающий печатать стек вызовов при ошибке, был вынужден писать и поддерживать минимум два независимых куска кода.POSIX systems (via glibc) provided backtrace()/backtrace_symbols(); Windows provided CaptureStackBackTrace()/SymFromAddr() from DbgHelp. Neither worked on the other platform — any project wanting to print a call stack on error was forced to write and maintain at least two independent chunks of code.
Превращение сырых адресов возврата в читаемые имена функций (символизация) требует debug-информации и своего формата на каждой платформе — даже получив адреса через backtrace(), разработчик должен был отдельно разбираться с addr2line, DWARF, PDB и т.п., прежде чем получить осмысленный текстовый вывод.Turning raw return addresses into readable function names (symbolization) requires debug information in its own format per platform — even after getting the addresses via backtrace(), a developer had to separately deal with addr2line, DWARF, PDB, and so on, before getting a meaningful text output.
Печать стека вызовов при логировании ошибок, в обработчиках необработанных исключений, в кастомных assert-макросах — распространённый паттерн, который каждый проект либо реализовывал сам под каждую платформу, либо тянул стороннюю библиотеку (Boost.Stacktrace, которая, по сути, и стала прототипом для std::stacktrace). Стандартизация убирает необходимость выбора между этими двумя вариантами.Printing the call stack during error logging, in unhandled-exception handlers, in custom assert macros — a common pattern that every project either implemented itself per platform or pulled in via a third-party library (Boost.Stacktrace, which essentially became the prototype for std::stacktrace). Standardization removes the need to choose between those two options.
std::stacktrace::current() захватывает текущий стек вызовов в момент обращения и возвращает объект-контейнер из std::stacktrace_entry, по одному на каждый кадр — от места вызова current() и вверх по цепочке до (обычно) точки входа в поток или программу. Каждая запись поддерживает .description() (обычно имя функции), .source_file() и .source_line() — тот же набор полей, что у std::source_location, но повторённый для каждого кадра стека, а не для одной точки.std::stacktrace::current() captures the current call stack at the moment of the call and returns a container-like object of std::stacktrace_entry, one per frame — from the point where current() was called, up the chain to (typically) the thread's or program's entry point. Each entry supports .description() (usually the function name), .source_file(), and .source_line() — the same set of fields as std::source_location, but repeated for every frame on the stack instead of a single point.
#include <stacktrace>
#include <iostream>
void log_error(std::string_view msg) {
std::cerr << msg << "\n" << std::stacktrace::current() << "\n";
// operator<< печатает готовый читаемый отчёт по всем кадрам сразу// operator<< prints a ready, readable report for all frames at once
}
void inspect_frames() {
auto trace = std::stacktrace::current();
for (const auto& frame : trace) {
std::cout << frame.description() << " at "
<< frame.source_file() << ":" << frame.source_line() << "\n";
}
}
Контраст с std::source_location стоит подчеркнуть отдельно: source_location::current() отвечает на вопрос "где именно в коде я нахожусь" — файл, строка, функция для ОДНОЙ конкретной точки, обычно захватываемой через параметр по умолчанию. std::stacktrace::current() отвечает на другой вопрос — "как я сюда попал" — вся цепочка вызовов целиком, от текущей точки и вверх до самого начала.The contrast with std::source_location is worth drawing out explicitly: source_location::current() answers "exactly where in the code am I" — a file, a line, a function for ONE specific point, usually captured via a default argument. std::stacktrace::current() answers a different question — "how did I get here" — the entire chain of calls, from the current point all the way up to the very start.
Обработчик верхнего уровня, ловящий все необработанные исключения, печатает не только сообщение об ошибке, но и путь, которым она была вызвана.A top-level handler catching all unhandled exceptions prints not just the error message, but also the path that led to it.
try {
run_application();
} catch (const std::exception& e) {
std::cerr << "unhandled exception: " << e.what() << "\n"
<< std::stacktrace::current() << "\n";
return 1;
}
Захват стека в момент СОЗДАНИЯ исключения (а не в момент его отлова где-то выше) — стек к моменту catch уже другой, поэтому его нужно сохранить заранее, внутри самого объекта исключения.Capturing the stack at the moment the exception is CREATED (not at the point it's caught further up) — the stack by the time of catch is already different, so it must be saved ahead of time, inside the exception object itself.
class TracedError : public std::runtime_error {
public:
explicit TracedError(std::string_view msg)
: std::runtime_error(std::string(msg)), trace_(std::stacktrace::current()) {}
const std::stacktrace& trace() const { return trace_; }
private:
std::stacktrace trace_;
};
Собственный макрос проверки инварианта, печатающий полный путь вызова перед аварийным завершением — полезно для отладки в местах, где стандартный assert даёт слишком мало контекста.A custom invariant-checking macro that prints the full call path before aborting — useful for debugging in places where the standard assert gives too little context.
void fail_invariant(const char* expr) {
std::cerr << "invariant failed: " << expr << "\n"
<< std::stacktrace::current() << "\n";
std::abort();
}
#define REQUIRE(cond) do { if (!(cond)) fail_invariant(#cond); } while (0)
Обёртка логирования, где сами вспомогательные функции логирования засоряют стек своими кадрами — пропуск первых N записей делает вывод релевантнее для читающего лог человека.A logging wrapper where the logging helper functions themselves clutter the stack with their own frames — skipping the first N entries makes the output more relevant to a human reading the log.
void log_with_trace(std::string_view msg, std::size_t skip_frames = 1) {
auto trace = std::stacktrace::current(skip_frames); // пропустить log_with_trace в отчёте// skip log_with_trace itself in the report
std::cerr << msg << "\n" << trace << "\n";
}
Получение стека может требовать чтения debug-информации и символизации адресов — это заметно дороже обычного вызова функции. Вызывать std::stacktrace::current() в коде, исполняющемся часто (не только при реальной ошибке — например, на каждой итерации цикла или на каждый входящий запрос), может ощутимо просадить производительность. Использовать его следует в путях обработки ошибок и логирования исключительных ситуаций, а не в обычном потоке исполнения.Capturing the stack may require reading debug information and symbolizing addresses — noticeably more expensive than a regular function call. Calling std::stacktrace::current() in code that runs frequently (not only on an actual error — say, on every loop iteration or every incoming request) can measurably hurt performance. It should be used on error-handling and exceptional-situation-logging paths, not in the ordinary flow of execution.
Полнота символизации (имена функций, номера строк) напрямую зависит от того, скомпилирована ли программа с debug-информацией. В release-сборке без debug symbols стек вызовов может показывать только сырые адреса или урезанную информацию вместо читаемых имён — стоит явно проверять поведение на реальном production-билде проекта, а не полагаться на вывод, полученный в debug-сборке при разработке.The completeness of symbolization (function names, line numbers) depends directly on whether the program was compiled with debug information. In a release build with no debug symbols, the call stack may show only raw addresses or truncated information instead of readable names — it's worth explicitly checking the behavior on the project's actual production build, rather than relying on output obtained from a development-time debug build.
Легко спутать эти два типа при выборе подходящего инструмента: если нужно знать только "откуда был вызван именно этот вызов" (например, для логирования одного конкретного места), std::source_location дешевле и достаточен. Если нужно понять, как исполнение дошло до текущей точки через несколько уровней вызовов, необходим именно std::stacktrace — source_location физически не может дать эту информацию, он не хранит ничего о вызывающей стороне выше одного уровня.These two types are easy to confuse when picking the right tool: if you only need to know "where was this particular call made from" (say, for logging one specific spot), std::source_location is cheaper and sufficient. If you need to understand how execution reached the current point through several levels of calls, you specifically need std::stacktrace — source_location simply cannot provide that information; it stores nothing about the caller beyond one level.