From b059132e13d045c8f8217649cc8517e2eac846d7 Mon Sep 17 00:00:00 2001 From: nikita Date: Mon, 20 Jul 2026 13:47:35 +0000 Subject: [PATCH] Support width and alignment in std::exception formatter The formatter for std::exception (and std::exception_ptr) previously accepted only the optional 't' type-name specifier, so standard fill, align and width specifiers were rejected with "unknown format specifier". This made it impossible to pad or align exception messages, e.g. when laying them out in a log column. Parse the standard fill/align/width specifiers (as the std::filesystem:: path formatter already does) and apply them when writing the message. Dynamic width ({:{}}) and the 'none'/'unknown exception' cases for exception_ptr are handled too. Existing behavior ({}, {:t}, nested exception unwinding) is unchanged. --- include/fmt/std.h | 30 ++++++++++++++++++++++++++---- test/std-test.cc | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/include/fmt/std.h b/include/fmt/std.h index 379b186efbb2..ef36a70a4e9e 100644 --- a/include/fmt/std.h +++ b/include/fmt/std.h @@ -620,6 +620,8 @@ struct formatter< T, char, typename std::enable_if::value>::type> { private: + format_specs specs_; + detail::arg_ref width_ref_; bool with_typename_ = false; public: @@ -627,7 +629,14 @@ struct formatter< auto it = ctx.begin(); auto end = ctx.end(); if (it == end || *it == '}') return it; - if (*it == 't') { + + it = detail::parse_align(it, end, specs_); + if (it == end) return it; + + char c = *it; + if ((c >= '0' && c <= '9') || c == '{') + it = detail::parse_width(it, end, specs_, width_ref_, ctx); + if (it != end && *it == 't') { ++it; with_typename_ = FMT_USE_RTTI != 0; } @@ -637,7 +646,20 @@ struct formatter< template auto format(const std::exception& ex, Context& ctx) const -> decltype(ctx.out()) { - return write(ctx.out(), ex); + auto buf = memory_buffer(); + write(appender(buf), ex); + return write_padded(ctx, string_view(buf.data(), buf.size())); + } + + protected: + // Applies the parsed fill/align/width to an already-formatted message. + template + auto write_padded(Context& ctx, string_view message) const + -> decltype(ctx.out()) { + auto specs = specs_; + detail::handle_dynamic_spec(specs.dynamic_width(), specs.width, width_ref_, + ctx); + return detail::write(ctx.out(), message, specs); } private: @@ -675,13 +697,13 @@ template <> struct formatter : formatter { template auto format(const std::exception_ptr& ep, FormatContext& ctx) const -> decltype(ctx.out()) { - if (!ep) return detail::write(ctx.out(), string_view("none")); + if (!ep) return this->write_padded(ctx, string_view("none")); try { std::rethrow_exception(ep); } catch (const std::exception& e) { return formatter::format(e, ctx); } catch (...) { - return detail::write(ctx.out(), string_view("unknown exception")); + return this->write_padded(ctx, string_view("unknown exception")); } } }; diff --git a/test/std-test.cc b/test/std-test.cc index 6fc92938a1c3..bba264afeda1 100644 --- a/test/std-test.cc +++ b/test/std-test.cc @@ -465,6 +465,43 @@ TEST(std_test, exception_ptr) { #endif // FMT_USE_RTTI } +TEST(std_test, exception_align) { + // The exception formatter supports standard width, fill and alignment. + auto ex = std::runtime_error("boom"); + EXPECT_EQ(fmt::format("{:>8}", ex), " boom"); + EXPECT_EQ(fmt::format("{:<8}", ex), "boom "); + EXPECT_EQ(fmt::format("{:^8}", ex), " boom "); + EXPECT_EQ(fmt::format("{:*>8}", ex), "****boom"); + EXPECT_EQ(fmt::format("{:{}}", ex, 8), "boom "); + // A width smaller than the message leaves the message untouched. + EXPECT_EQ(fmt::format("{:>2}", ex), "boom"); + +#if FMT_USE_RTTI + // Alignment combines with the 't' (type name) presentation type. + try { + using namespace my_ns1::my_ns2; + throw my_exception("oops"); + } catch (const std::exception& e) { + auto base = fmt::format("{:t}", e); + auto padded = fmt::format("{:*<40t}", e); + EXPECT_EQ(padded.size(), 40u); + EXPECT_EQ(padded.substr(0, base.size()), base); + EXPECT_EQ(padded.substr(base.size()), std::string(40 - base.size(), '*')); + } +#endif // FMT_USE_RTTI + + // exception_ptr honors the same specifiers, for both the empty ("none") + // and the non-empty case. + std::exception_ptr enull = nullptr; + EXPECT_EQ(fmt::format("{:>8}", enull), " none"); + std::exception_ptr ep; + try { + throw std::runtime_error("bang"); + } catch (...) { + ep = std::current_exception(); + } + EXPECT_EQ(fmt::format("{:>8}", ep), " bang"); +} #if FMT_USE_RTTI TEST(std_test, type_info) { EXPECT_EQ(fmt::format("{}", typeid(std::runtime_error)),