diff --git a/doc/api.md b/doc/api.md index c46304e0..091bd4fa 100644 --- a/doc/api.md +++ b/doc/api.md @@ -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&) ::: ptr(const std::shared_ptr&) diff --git a/include/fmt/std.h b/include/fmt/std.h index 2bab00f2..ffb19e36 100644 --- a/include/fmt/std.h +++ b/include/fmt/std.h @@ -11,6 +11,10 @@ #include "format.h" #include "ostream.h" +#if FMT_HAS_INCLUDE() +# include +#endif + #ifndef FMT_MODULE # include # include @@ -45,12 +49,12 @@ # if FMT_CPLUSPLUS > 202002L && FMT_HAS_INCLUDE() # include # endif +# if FMT_CPLUSPLUS >= 202302L && FMT_HAS_INCLUDE() && \ + defined(__cpp_lib_stacktrace) +# include +# endif #endif // FMT_MODULE -#if FMT_HAS_INCLUDE() -# include -#endif - // GCC 4 does not support FMT_HAS_INCLUDE. #if FMT_HAS_INCLUDE() || defined(__GLIBCXX__) # include @@ -467,6 +471,69 @@ template <> struct formatter { }; #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( + reinterpret_cast(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(out, source_line); + } + return out; +} +} // namespace detail + +FMT_EXPORT template <> struct formatter + : formatter { + template + 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::format(string_view(buf.data(), buf.size()), + ctx); + } +}; + +FMT_EXPORT template <> struct formatter + : formatter { + template + 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(out, index++); + out = detail::write(out, ' '); + out = detail::write_stacktrace_entry(out, entry); + } + return formatter::format(string_view(buf.data(), buf.size()), + ctx); + } +}; +#endif // __cpp_lib_stacktrace + #if FMT_CPP_LIB_VARIANT template struct is_variant_like { diff --git a/test/std-test.cc b/test/std-test.cc index eaf238b4..eb9e47e2 100644 --- a/test/std-test.cc +++ b/test/std-test.cc @@ -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{}), "none");