How to forward formatting information from std::format

623 Views Asked by At

I am trying to use c++20's std::format on msvc.
When I try to define a format for a custom type Person, I hope to be able to use the external std::format("{:x}{:*^10}", person) in "{:x}{:*^10}" is forwarded to std::formatter<Person>::fomat and used directly in std::formatter<Person>::fomat
for example:

#include <format>
#include <iostream>
#include <string>
#include <string_view>

class Person {
public:
    int age;
    std::string name;

};

template <>
struct std::formatter<Person> : std::formatter<std::string> {
    template <typename FormatContext>
    auto format(const Person& p, FormatContext& ctx) {
        // I hope that the "forwarded format" here is "{:x}{:*^10}" in the format information of ::std::format in main. \
          This code is of course not runnable. I want to know How can I use an external format in formatter<Person>::foaramt like this
        return format_to(ctx.out(),"forwarded format",p.age,p.name);
    }
};

int main() {
    Person person{ 30,"John Doe" };
    std::cout << std::format("{:x}{:*^10}", p) << '\n';
    return 0;
}

I hope to be able to use the forwarded formatting information "{:x}{:*^10}" directly in std::formatter::format, Instead of changing std::foramt("{:x}{:*^10}",person) to std::format("{:x}{:*^10}",person.age,person.name)

2

There are 2 best solutions below

3
On BEST ANSWER

You can define a string_view variable in std::formatter<Person> to store formatting information.

Then, calculate the format string for members in the parse() function and assign it to this variable, and use it in the format() function.

(For simplicity, some compile-time checks inside the parse function are omitted)

template <>
struct std::formatter<Person> {
  std::string_view fmt_str; // formatting information

  template<typename ParseContext>
  constexpr auto parse(ParseContext& ctx) {
    auto begin = ctx.begin();                       // ctx is "x}{:*^10}"
    auto it = std::formatter<int>{}.parse(ctx);     // now, it point to "}{:*^10}"
    it = std::ranges::find(it + 1, ctx.end(), ':'); // now, it point to ":*^10}"
    ctx.advance_to(it + 1);                         // now, ctx is "*^10}"
    it = std::formatter<std::string>{}.parse(ctx);  // now, it point to "}"
    fmt_str = {begin - 2, it + 1};                  // now fmt_str is "{:x}{:*^10}"
    return it;
  }

  template <typename FormatContext>
  auto format(const Person& p, FormatContext& ctx) const {
    return std::vformat_to(ctx.out(), fmt_str, std::make_format_args(p.age, p.name));
  }
};

Demo

12
On

This is a bad goal:

std::format("{:x}{:*^10}", p)

This is an expression that looks like it's formatting two arguments, but is actually formatting only one. Sure, you can make it work, but... this is incredibly confusing.

A better goal would instead to make this work:

std::format("{:{x}{*^10}}", p)

That is, you only have one replacement-field, whose format-spec is {x}{*^10}. This is more consistent with the way the rest of the library works.

And then we can do all the things we need to do more directly, using formatter<int> and formatter<string>:

template <>
struct std::formatter<Person> {
  std::formatter<int> age;
  std::formatter<std::string> name;


  template<typename ParseContext>
  constexpr auto parse(ParseContext& ctx) {
    auto it = ctx.begin();
    if (it == ctx.end() || *it == '}') {
      if (age.parse(ctx) != it || name.parse(ctx) != it) {
        throw std::format_error("bad");
      }
      return it;
    }

    if (*it != '{') throw std::format_error("bad");
    ctx.advance_to(it + 1);
    it = age.parse(ctx);
    if (it == ctx.end() || *it != '}') throw std::format_error("bad");
    ++it;
    if (it == ctx.end() || *it != '{') throw std::format_error("bad");
    ctx.advance_to(it + 1);
    it = name.parse(ctx);
    if (it == ctx.end() || *it != '}') throw std::format_error("bad");
    return it + 1;
  }

  template <typename FormatContext>
  auto format(const Person& p, FormatContext& ctx) const {
    ctx.advance_to(age.format(p.age, ctx));
    ctx.advance_to(name.format(p.name, ctx));
    return ctx.out();
  }
};

Demo.

This is still using the underlying int and string formatters with their respective specs and ensuring that works properly, in an efficient way (i.e. they are only parsed one time, not twice).

See my CppCon 2022 talk for more examples of doing this sort of thing, also in more depth (the talk has an example of formatting std::tuple<Ts...>, of which this is a special case).