Compare commits

...

4 Commits

Author SHA1 Message Date
egorkaa
630aafeb70
Merge b059132e13d045c8f8217649cc8517e2eac846d7 into 17500e457b198c37c4374d142fa7a1d36e4651fe 2026-07-29 09:43:47 -07:00
rimathia
17500e457b
doc extension concerning format_as and general implementations (#4857) 2026-07-29 09:43:31 -07:00
Victor Zverovich
26c01df3bd Handle const-qualified named arguments
A named argument that arrives const-qualified was not recognized by
is_named_arg, silently dropping its name. This happens both when a
named argument is passed through an intermediate function returning
const T& (https://github.com/fmtlib/fmt/issues/4866) and in fmt's own
compiled format path, which passes arguments as const T&.

Make is_named_arg and is_static_named_arg see through top-level const so
the name is preserved instead of dropped.
2026-07-29 09:20:23 -07:00
nikita
b059132e13 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.
2026-07-20 17:29:16 +03:00
6 changed files with 85 additions and 9 deletions

View File

@ -13,6 +13,10 @@ jobs:
${{matrix.shared && 'Shared' || ''}} ${{matrix.shared && 'Shared' || ''}}
${{matrix.cxxflags_extra && 'Sanitize' || ''}} ${{matrix.cxxflags_extra && 'Sanitize' || ''}}
runs-on: ${{ matrix.os || 'ubuntu-22.04' }} runs-on: ${{ matrix.os || 'ubuntu-22.04' }}
env:
# Run tests in a non-UTC timezone. glibc localtime/strftime honor TZ, so
# there is no need for timedatectl, which fails without systemd-timedated.
TZ: Europe/Kyiv
strategy: strategy:
matrix: matrix:
cxx: [g++-4.9, g++-11, clang++-3.6, clang++-11] cxx: [g++-4.9, g++-11, clang++-3.6, clang++-11]
@ -75,15 +79,14 @@ jobs:
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set timezone
run: sudo timedatectl set-timezone 'Europe/Kyiv'
- name: Install GCC 4.9 - name: Install GCC 4.9
run: | run: |
sudo apt update sudo apt update
sudo apt install libatomic1 libc6-dev libgomp1 libitm1 libmpc3 sudo apt install libatomic1 libc6-dev libgomp1 libitm1 libmpc3
# https://launchpad.net/ubuntu/xenial/amd64/g++-4.9/4.9.3-13ubuntu2 # https://launchpad.net/ubuntu/xenial/amd64/g++-4.9/4.9.3-13ubuntu2
wget --no-verbose \ # launchpad periodically returns 503s, so retry to avoid flaky CI.
wget --no-verbose --tries=5 --waitretry=10 \
--retry-connrefused --retry-on-http-error=503 \
http://launchpadlibrarian.net/230069137/libmpfr4_3.1.3-2_amd64.deb \ http://launchpadlibrarian.net/230069137/libmpfr4_3.1.3-2_amd64.deb \
http://launchpadlibrarian.net/253728424/libasan1_4.9.3-13ubuntu2_amd64.deb \ http://launchpadlibrarian.net/253728424/libasan1_4.9.3-13ubuntu2_amd64.deb \
http://launchpadlibrarian.net/445346135/libubsan0_5.4.0-6ubuntu1~16.04.12_amd64.deb \ http://launchpadlibrarian.net/445346135/libubsan0_5.4.0-6ubuntu1~16.04.12_amd64.deb \
@ -114,7 +117,9 @@ jobs:
sudo apt update sudo apt update
sudo apt install libtinfo5 sudo apt install libtinfo5
# https://code.launchpad.net/ubuntu/xenial/amd64/clang-3.6/1:3.6.2-3ubuntu2 # https://code.launchpad.net/ubuntu/xenial/amd64/clang-3.6/1:3.6.2-3ubuntu2
wget --no-verbose \ # launchpad periodically returns 503s, so retry to avoid flaky CI.
wget --no-verbose --tries=5 --waitretry=10 \
--retry-connrefused --retry-on-http-error=503 \
http://launchpadlibrarian.net/230019046/libffi6_3.2.1-4_amd64.deb \ http://launchpadlibrarian.net/230019046/libffi6_3.2.1-4_amd64.deb \
http://launchpadlibrarian.net/445346109/libasan2_5.4.0-6ubuntu1~16.04.12_amd64.deb \ http://launchpadlibrarian.net/445346109/libasan2_5.4.0-6ubuntu1~16.04.12_amd64.deb \
http://launchpadlibrarian.net/445346135/libubsan0_5.4.0-6ubuntu1~16.04.12_amd64.deb \ http://launchpadlibrarian.net/445346135/libubsan0_5.4.0-6ubuntu1~16.04.12_amd64.deb \

View File

@ -92,6 +92,10 @@ Use `format_as` if you want to make your type formattable as some other
type with the same format specifiers. The `format_as` function should type with the same format specifiers. The `format_as` function should
take an object of your type and return an object of a formattable type. take an object of your type and return an object of a formattable type.
It should be defined in the same namespace as your type. It should be defined in the same namespace as your type.
`format_as` cannot be used when a type also matches another `formatter`
specialization, such as the range `formatter`, because the specializations
would be ambiguous. Disable the conflicting specialization, if possible,
or provide an explicit `formatter` specialization instead.
Example ([run](https://godbolt.org/z/nvME4arz8)): Example ([run](https://godbolt.org/z/nvME4arz8)):

View File

@ -1059,6 +1059,10 @@ template <typename T> struct is_static_named_arg : std::false_type {};
template <typename T, typename Char> template <typename T, typename Char>
struct is_named_arg<named_arg<T, Char>> : std::true_type {}; struct is_named_arg<named_arg<T, Char>> : std::true_type {};
template <typename T> struct is_named_arg<const T> : is_named_arg<T> {};
template <typename T>
struct is_static_named_arg<const T> : is_static_named_arg<T> {};
template <typename T, typename Char = char> struct named_arg : view { template <typename T, typename Char = char> struct named_arg : view {
const Char* name; const Char* name;
const T& value; const T& value;

View File

@ -620,6 +620,8 @@ struct formatter<
T, char, T, char,
typename std::enable_if<std::is_base_of<std::exception, T>::value>::type> { typename std::enable_if<std::is_base_of<std::exception, T>::value>::type> {
private: private:
format_specs specs_;
detail::arg_ref<char> width_ref_;
bool with_typename_ = false; bool with_typename_ = false;
public: public:
@ -627,7 +629,14 @@ struct formatter<
auto it = ctx.begin(); auto it = ctx.begin();
auto end = ctx.end(); auto end = ctx.end();
if (it == end || *it == '}') return it; 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; ++it;
with_typename_ = FMT_USE_RTTI != 0; with_typename_ = FMT_USE_RTTI != 0;
} }
@ -637,7 +646,20 @@ struct formatter<
template <typename Context> template <typename Context>
auto format(const std::exception& ex, Context& ctx) const auto format(const std::exception& ex, Context& ctx) const
-> decltype(ctx.out()) { -> 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 <typename Context>
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: private:
@ -675,13 +697,13 @@ template <> struct formatter<std::exception_ptr> : formatter<std::exception> {
template <typename FormatContext> template <typename FormatContext>
auto format(const std::exception_ptr& ep, FormatContext& ctx) const auto format(const std::exception_ptr& ep, FormatContext& ctx) const
-> decltype(ctx.out()) { -> decltype(ctx.out()) {
if (!ep) return detail::write(ctx.out(), string_view("none")); if (!ep) return this->write_padded(ctx, string_view("none"));
try { try {
std::rethrow_exception(ep); std::rethrow_exception(ep);
} catch (const std::exception& e) { } catch (const std::exception& e) {
return formatter<std::exception>::format(e, ctx); return formatter<std::exception>::format(e, ctx);
} catch (...) { } catch (...) {
return detail::write(ctx.out(), string_view("unknown exception")); return this->write_padded(ctx, string_view("unknown exception"));
} }
} }
}; };

View File

@ -163,6 +163,10 @@ TEST(compile_test, named) {
fmt::format(FMT_COMPILE("{a0} {a1}"), "a0"_a = 41, "a1"_a = 43)); fmt::format(FMT_COMPILE("{a0} {a1}"), "a0"_a = 41, "a1"_a = 43));
EXPECT_EQ("41 43", EXPECT_EQ("41 43",
fmt::format(FMT_COMPILE("{a1} {a0}"), "a0"_a = 43, "a1"_a = 41)); fmt::format(FMT_COMPILE("{a1} {a0}"), "a0"_a = 43, "a1"_a = 41));
// A statically-named argument with a format spec compiles to spec_field,
// which passes the argument to make_format_args as const (#4866).
EXPECT_EQ("4.2", fmt::format(FMT_COMPILE("{arg:3.1f}"), "arg"_a = 4.2));
# endif # endif
} }

View File

@ -465,6 +465,43 @@ TEST(std_test, exception_ptr) {
#endif // FMT_USE_RTTI #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 #if FMT_USE_RTTI
TEST(std_test, type_info) { TEST(std_test, type_info) {
EXPECT_EQ(fmt::format("{}", typeid(std::runtime_error)), EXPECT_EQ(fmt::format("{}", typeid(std::runtime_error)),