Add formatter for std::stacktrace and std::stacktrace_entry (C++23)

Adds support for formatting std::stacktrace and std::stacktrace_entry
from <stacktrace> (C++23, __cpp_lib_stacktrace).
- stacktrace_entry formats as 'function at file:line' when symbols
  are available, or as a hex address otherwise
- stacktrace formats one indexed entry per line: '#N description'
- Both formatters inherit string_view specs (width, fill, align)
- Guarded by __cpp_lib_stacktrace for portability
This commit is contained in:
siribeesu 2026-10-11 19:32:55 +05:30
parent 35c58f084e
commit cfa0f66ffb
3 changed files with 146 additions and 4 deletions

View File

@ -512,11 +512,18 @@ chrono-format-spec).
- [`std::monostate`](
https://en.cppreference.com/w/cpp/utility/variant/monostate)
- [`std::optional`](https://en.cppreference.com/w/cpp/utility/optional)
- `std::stacktrace`
- `std::stacktrace_entry`
- [`std::source_location`](
https://en.cppreference.com/w/cpp/utility/source_location)
- [`std::thread::id`](https://en.cppreference.com/w/cpp/thread/thread/id)
- [`std::variant`](https://en.cppreference.com/w/cpp/utility/variant/variant)
When the C++23 stacktrace feature is available, `std::stacktrace_entry` is
formatted as a function description followed by its source location when
available, or its address when symbols are unavailable. `std::stacktrace`
formats one indexed entry per line. Both support width and alignment specs.
::: ptr(const std::unique_ptr<T, Deleter>&)
::: ptr(const std::shared_ptr<T>&)

View File

@ -11,6 +11,10 @@
#include "format.h"
#include "ostream.h"
#if FMT_HAS_INCLUDE(<version>)
# include <version>
#endif
#ifndef FMT_MODULE
# include <atomic>
# include <bitset>
@ -45,12 +49,12 @@
# if FMT_CPLUSPLUS > 202002L && FMT_HAS_INCLUDE(<expected>)
# include <expected>
# endif
# if FMT_CPLUSPLUS >= 202302L && FMT_HAS_INCLUDE(<stacktrace>) && \
defined(__cpp_lib_stacktrace)
# include <stacktrace>
# endif
#endif // FMT_MODULE
#if FMT_HAS_INCLUDE(<version>)
# include <version>
#endif
// GCC 4 does not support FMT_HAS_INCLUDE.
#if FMT_HAS_INCLUDE(<cxxabi.h>) || defined(__GLIBCXX__)
# include <cxxabi.h>
@ -467,6 +471,69 @@ template <> struct formatter<std::source_location> {
};
#endif
#ifdef __cpp_lib_stacktrace
namespace detail {
inline auto write_stacktrace_address(appender out,
const std::stacktrace_entry& entry)
-> appender {
return format_to(out, "{:#x}",
static_cast<std::uintptr_t>(
reinterpret_cast<std::uintptr_t>(entry.native_handle())));
}
inline auto write_stacktrace_entry(appender out,
const std::stacktrace_entry& entry)
-> appender {
auto description = entry.description();
if (description.empty())
return write_stacktrace_address(out, entry);
out = write(out, string_view(description));
auto source_file = entry.source_file();
auto source_line = entry.source_line();
if (!source_file.empty() && source_line != 0) {
out = write(out, " at ");
out = write(out, string_view(source_file));
out = write(out, ':');
out = write<char>(out, source_line);
}
return out;
}
} // namespace detail
FMT_EXPORT template <> struct formatter<std::stacktrace_entry>
: formatter<string_view> {
template <typename FormatContext>
auto format(const std::stacktrace_entry& entry, FormatContext& ctx) const
-> decltype(ctx.out()) {
auto buf = memory_buffer();
detail::write_stacktrace_entry(appender(buf), entry);
return formatter<string_view>::format(string_view(buf.data(), buf.size()),
ctx);
}
};
FMT_EXPORT template <> struct formatter<std::stacktrace>
: formatter<string_view> {
template <typename FormatContext>
auto format(const std::stacktrace& trace, FormatContext& ctx) const
-> decltype(ctx.out()) {
auto buf = memory_buffer();
auto out = appender(buf);
auto index = std::size_t();
for (const auto& entry : trace) {
if (index != 0) *out++ = '\n';
out = detail::write(out, '#');
out = detail::write<char>(out, index++);
out = detail::write(out, ' ');
out = detail::write_stacktrace_entry(out, entry);
}
return formatter<string_view>::format(string_view(buf.data(), buf.size()),
ctx);
}
};
#endif // __cpp_lib_stacktrace
#if FMT_CPP_LIB_VARIANT
template <typename T> struct is_variant_like {

View File

@ -120,6 +120,74 @@ TEST(std_test, source_location) {
}
#endif
#ifdef __cpp_lib_stacktrace
TEST(std_test, stacktrace_entry) {
auto trace = std::stacktrace::current();
ASSERT_FALSE(trace.empty());
const auto& entry = trace[0];
auto formatted = fmt::format("{}", entry);
EXPECT_FALSE(formatted.empty());
auto description = entry.description();
if (!description.empty())
EXPECT_NE(formatted.find(description), std::string::npos);
auto source_file = entry.source_file();
auto source_line = entry.source_line();
if (!source_file.empty() && source_line != 0) {
auto location = fmt::format("{}:{}", source_file, source_line);
EXPECT_NE(formatted.find(location), std::string::npos);
if (!description.empty()) {
auto expected = fmt::format("{} at {}", description, location);
EXPECT_NE(formatted.find(expected), std::string::npos);
}
}
auto expected = formatted;
if (expected.size() < 40) expected.insert(0, 40 - expected.size(), ' ');
EXPECT_EQ(fmt::format("{:>40}", entry), expected);
expected = formatted;
if (expected.size() < 40) expected.append(40 - expected.size(), ' ');
EXPECT_EQ(fmt::format("{:<40}", entry), expected);
EXPECT_EQ(fmt::format("{:>{}}", entry, formatted.size() + 2),
" " + formatted);
}
TEST(std_test, stacktrace) {
auto trace = std::stacktrace::current();
ASSERT_FALSE(trace.empty());
auto formatted = fmt::format("{}", trace);
EXPECT_FALSE(formatted.empty());
EXPECT_NE(formatted.find(fmt::format("#0 {}", trace[0])),
std::string::npos);
if (trace.size() > 1)
EXPECT_NE(formatted.find("\n#1 "), std::string::npos);
auto line_count = std::size_t();
for (auto c : formatted)
if (c == '\n') ++line_count;
EXPECT_EQ(line_count + 1, trace.size());
auto expected = formatted;
if (expected.size() < 40) expected.insert(0, 40 - expected.size(), ' ');
EXPECT_EQ(fmt::format("{:>40}", trace), expected);
expected = formatted;
if (expected.size() < 40) expected.append(40 - expected.size(), ' ');
EXPECT_EQ(fmt::format("{:<40}", trace), expected);
EXPECT_EQ(fmt::format("{:>{}}", trace, formatted.size() + 2),
" " + formatted);
}
TEST(std_test, stacktrace_empty) {
// An empty stacktrace should format to an empty string without crashing.
auto trace = std::stacktrace();
EXPECT_EQ(fmt::format("{}", trace), "");
// Width/align on empty trace should pad correctly.
EXPECT_EQ(fmt::format("{:>5}", trace), " ");
}
#endif // __cpp_lib_stacktrace
TEST(std_test, optional) {
#ifdef __cpp_lib_optional
EXPECT_EQ(fmt::format("{}", std::optional<int>{}), "none");