Fix FMT_COMPILE with ranges, tuples and maps

nested_format_specs::write formatted the body of a padded value into a
temporary buffer using a context of the same type as the outer one. This
only works when the outer context writes to an appender, so FMT_COMPILE
and format_to with other output iterators failed to compile even when no
width was given.

Use a buffer context that takes arguments and locale from the outer
context when the outer context doesn't write to an appender.

Fixes #4949.
This commit is contained in:
Mayank Jaiswal 2026-10-05 21:19:09 +05:30
parent fdf31252e6
commit b903e4fcbf
3 changed files with 34 additions and 12 deletions

View File

@ -610,7 +610,8 @@ template <typename T> class basic_appender;
using appender = basic_appender<char>;
class context;
template <typename OutputIt, typename Char> class generic_context;
template <typename OutputIt, typename Char, typename ArgContext = void>
class generic_context;
template <typename Char> class parse_context;
// Longer aliases for C++20 compatibility.

View File

@ -4149,9 +4149,14 @@ template <typename Char> class nested_format_specs {
handle_dynamic_spec(specs.dynamic_width(), specs.width, width_ref_, ctx);
if (specs.width == 0) return f.write_body(ctx, static_cast<T&&>(values)...);
// FormatContext can only be reused if it writes to an appender (#4949).
using buffer_context = conditional_t<
std::is_same<decltype(ctx.out()), basic_appender<Char>>::value,
FormatContext,
generic_context<basic_appender<Char>, Char, FormatContext>>;
auto buf = basic_memory_buffer<Char>();
auto buffer_ctx =
FormatContext(basic_appender<Char>(buf), ctx.args(), ctx.locale());
buffer_context(basic_appender<Char>(buf), ctx.args(), ctx.locale());
f.write_body(buffer_ctx, static_cast<T&&>(values)...);
return detail::write<Char>(
ctx.out(), basic_string_view<Char>(buf.data(), buf.size()), specs);
@ -4168,11 +4173,16 @@ FMT_BEGIN_EXPORT
// A generic formatting context with custom output iterator and character
// (code unit) support. Char is the format string code unit type which can be
// different from OutputIt::value_type.
template <typename OutputIt, typename Char> class generic_context {
// different from OutputIt::value_type. ArgContext is the context the arguments
// are typed on; void means the context itself.
template <typename OutputIt, typename Char, typename ArgContext>
class generic_context {
private:
using arg_context = conditional_t<std::is_void<ArgContext>::value,
generic_context, ArgContext>;
OutputIt out_;
basic_format_args<generic_context> args_;
basic_format_args<arg_context> args_;
locale_ref loc_;
public:
@ -4180,27 +4190,24 @@ template <typename OutputIt, typename Char> class generic_context {
using iterator = OutputIt;
enum { builtin_types = FMT_BUILTIN_TYPES };
constexpr generic_context(OutputIt out,
basic_format_args<generic_context> args,
constexpr generic_context(OutputIt out, basic_format_args<arg_context> args,
locale_ref loc = {})
: out_(out), args_(args), loc_(loc) {}
generic_context(generic_context&&) = default;
generic_context(const generic_context&) = delete;
void operator=(const generic_context&) = delete;
constexpr auto arg(int id) const -> basic_format_arg<generic_context> {
constexpr auto arg(int id) const -> basic_format_arg<arg_context> {
return args_.get(id);
}
auto arg(basic_string_view<Char> name) const
-> basic_format_arg<generic_context> {
-> basic_format_arg<arg_context> {
return args_.get(name);
}
constexpr auto arg_id(basic_string_view<Char> name) const -> int {
return args_.get_id(name);
}
auto args() const -> const basic_format_args<generic_context>& {
return args_;
}
auto args() const -> const basic_format_args<arg_context>& { return args_; }
constexpr auto out() const -> iterator { return out_; }

View File

@ -247,6 +247,20 @@ TEST(compile_test, output_iterators) {
EXPECT_EQ("42.000043", s2.str());
}
TEST(compile_test, format_range) {
// https://github.com/fmtlib/fmt/issues/4949
std::array<int, 3> values{1, 2, 3};
EXPECT_EQ("array=[1, 2, 3]", fmt::format(FMT_COMPILE("array={}"), values));
EXPECT_EQ("***********[1, 2, 3]",
fmt::format(FMT_COMPILE("{:*>20}"), values));
EXPECT_EQ("*****[ 1, 2, 3]",
fmt::format(FMT_COMPILE("{:*>20:{}}"), values, 3));
std::list<char> out;
fmt::format_to(std::back_inserter(out), FMT_COMPILE("{:*>20}"), values);
EXPECT_EQ("***********[1, 2, 3]", std::string(out.begin(), out.end()));
}
# if FMT_USE_CONSTEVAL
TEST(compile_test, constexpr_formatted_size) {
FMT_CONSTEXPR20 size_t size = fmt::formatted_size(FMT_COMPILE("{}"), 42);