#опытным
Одна из проблема исключений - непонятно, откуда оно прилетело. Ну да, по сообщению об ошибке и типу исключения обычно можно отследить нужное место в программе. Но как исполнение дошло до этой точки? А это ведь самое важное - понимание, при каком сценарии наступила исключительная ситуация.
Когда мы дебажим программу, мы видим, что привело к точке останова. В любом дебаггере есть команда аля backtrace, которая выводит визуализированный стек вызовов. Например так:
(gdb) bt
#0 0x0000555555556a2a in apply_discount (order_id=1001, total=250.5) at discount.c:45
#1 0x0000555555556b10 in calculate_final_price (order=0x7fffffffde40) at order_engine.c:89
#2 0x0000555555556c45 in process_payment (order=0x7fffffffde40, method=0x555555758020 "credit") at payment.c:120
#3 0x0000555555556d8a in handle_order_request (request=0x7fffffffdf00) at api_handler.c:56
#4 0x0000555555556e20 in main () at server.c:32
В данном случае мы видим, что ошибка произошла в функции apply_discount, которую вызвал калькулятор цены при обработке запроса платежа. То есть в данном случае мы видим четкий сценарий возникновения ошибки
И вот было бы круто видеть такую картину каждый раз, когда в catch залетает исключение. Да и в принципе в любой ситуации, когда нам интересна информация из трейса.
С++23 предоставил нам возможность конструировать кастомные исключения с описанием стека вызова внутри. Делается это через класс std::stacktrace. Работает он очень просто:
#include <iostream>
#include <stacktrace>
std::stacktrace bar() {
auto trace = std::stacktrace::current();
return trace;
}
std::stacktrace foo() {
return bar();
}
int main() {
auto trace = foo();
std::cout << trace << '\n';
}
С помощью статического метода std::stacktrace::current() можно получить стек вызовов в моменте использования метода. Для типа std::stacktrace даже перегружен оператор вывода в поток, поэтому вы спокойно можете вывести сырой трейс на консоль. Вывод может быть примерно таким:
0# bar() at /app/example.cpp:5 [0x4033ee]
1# foo() at /app/example.cpp:10 [0x40340d]
2# main at /app/example.cpp:14 [0x403428]
3# <unknown> [0x74eed1a2a1c9]
4# libc_start_main [0x74eed1a2a28a]
5# start [0x403304]
Вот примерчик, чтобы поиграться.
Теперь легко сделать свое исключение со стектрейсом и прочими прелестями.
class TracedException : public std::exception {
std::string message;
std::stacktrace trace;
mutable std::string full_what; // cached formatted output
public:
explicit TracedException(std::string msg)
: message_(std::move(msg))
// Capture the stack RIGHT when the exception is created
// skip = 1 means skip this constructor from the trace
, trace_(std::stacktrace::current(/skip=/1))
{}
const std::stacktrace& trace() const noexcept {
return trace_;
}
const char* what() const noexcept override {
if (full_what_.empty()) {
full_what_ = message_ + "\n\n--- Stack Trace ---\n"
+ std::to_string(trace_);
}
return full_what_.c_str();
}
};Трейс захватываем в конструкторе, собственно ровно в том месте, откуда и летит исключение. И скипаем один фрейм сверху, чтобы не учитывать конструктор, с помощью параметра метода std::stacktrace::current. В целом, использование очень похоже на std::source_location.
Know your origin. Stay cool.
#cpp23