c++c++20stdformat

What is the correct way to implement a custom std::formatter in C++20?


I've looked at several articles on custom formatters for the C++20 formatting library, but many seem subtly incorrect or fail to compile on my toolchain. Below is an example I believe is correct followed by 3 specific questions.

#include <print>

struct CustomType {
    int a;
};

template<>
struct std::formatter<CustomType> : std::formatter<int> {

    // use format specifiers for int
    using std::formatter<int>::parse;

    auto format(CustomType const& x, auto & ctx) const {
        auto out = ctx.out();
        out = std::format_to(out, "[");

        ctx.advance_to(out);
        out = std::formatter<int>::format(x.a, ctx);

        return std::format_to(out, "]");
    }
};

int main() {
    std::println("{:>2}", CustomType{1});
    return 0;
}

Are these statements correct?

  1. The format method should be templated on the format context type instead of using std::format_context, as many online examples suggest.
  2. The format method should be const qualified.
  3. The output iterator needs to be manually updated when using format_to(). Most examples omit this and seem to assume a std::back_insert_iterator, but as far as I can tell the standard does not mandate that even for std::format_context.

Solution

  • The format method should be templated on the format context type instead of using std::format_context, as many online examples suggest.

    Yes.

    The format method should be const qualified.

    Yes.

    The output iterator needs to be manually updated when using format_to(). Most examples omit this and seem to assume a std::back_insert_iterator, but as far as I can tell the standard does not mandate that even for std::format_context.

    Yes. While the library tends to use the same, one iterator type for all char types, this isn't the only thing you can do - and if format is ever instantiated with a context that has an iterator type for which position is important - like, say, char* - then if you don't advance_to, your next format will overwrite your first one (e.g. the FMT_COMPILE approach in {fmt} doesn't wrap the iterator, so you can create a situation where the output iterator is char* and then this happens).

    For this reason it might be less error prone to:

    ctx.advance_to(std::format_to(ctx.out(), "["));
    ctx.advance_to(std::formatter<int>::format(x.a, ctx));
    return std::format_to(ctx.out(), "]");
    

    Although that's admittedly more tedious. It'd be nice if there was better library support for this sort of thing, but I'm not sure what that would look like.