c++stdformat

How to declare a std::formatter for a user-defined type?


Imagine a very simple Point class:

class Point final
{
private:
    std::int32_t m_x;
    std::int32_t m_y;

public:
    explicit Point(const std::int32_t x = 0, const std::int32_t y = 0)
        : m_x(x), m_y(y)
    {}

    // Are these necessary?
    // Point(const Point& other) = default;
    // Point(Point&& other) = default;
    // Point& operator=(const Point& other) = default;
    // Point& operator=(Point&& other) = default;

    friend struct std::formatter<Point>;
};

template<>
struct std::formatter<Point>
    : std::formatter<std::string>
{
    std::format_context::iterator
    format(const Point& p, std::format_context& ctx)
    const
    {
         const std::string s = std::format("x:{}, y:{}", p.m_x, p.m_y);
         const std::format_context::iterator x = formatter<std::string>::format(s, ctx);
         return x;
    }
};

Now, if I try to format a Point, it works well:

class Dummy final:
{
    static void
    print()
    {
        Point p{2, 3};
        const std::string s = std::format("Point: {}", p);
        std::cout << s;
    }
};

However, if I want to format a container of type Point, I cannot make it work.

class Dummy final:
{
    static void
    print()
    {
        Point p{2, 3};
        const std::string s = std::format("Point: {}", p);
        std::cout << s;
                
        std::vector<Point> v;
        v.push_back(Point{3, 4});  // should I use emplace_back() instead?
        v.push_back(Point{4, 5});
        const std::string s2 = std::format("{}", v);
        std::cout << s2;
    }
};

What is wrong with my implementation of template<> struct std::formatter<Point>?

Finally, if this question was better posted to "Code Review", please let me know.


Solution

  • The std::formatter used to format ranges is as follows:

    template<ranges::input_range R, class charT>
        requires (format_kind<R> != range_format::disabled) &&
                 formattable<ranges::range_reference_t<R>, charT>
      struct formatter<R, charT> : range-default-formatter<format_kind<R>, R, charT> { };
    

    which requires that the reference type of the range satisfies std::formattable, which is Point. Unfortunately, in your example, Point does not satisfy std::formattable<char>.

    The reason is that std::formattable has the following definition:

    template<class T, class Context,
             class Formatter = typename Context::template formatter_type<remove_const_t<T>>>
      concept formattable-with =                // exposition only
        semiregular<Formatter> &&
        requires(Formatter& f, const Formatter& cf, T&& t, Context fc,
                 basic_format_parse_context<typename Context::char_type> pc)
        {
          { f.parse(pc) } -> same_as<typename decltype(pc)::iterator>;
          { cf.format(t, fc) } -> same_as<typename Context::iterator>;
        };
    
    template<class T, class charT>
      concept formattable =
        formattable-with<remove_reference_t<T>, basic_format_context<fmt-iter-for<charT>, charT>>;
    

    where fmt-iter-for<charT> is an unspecified type that models output_iterator<const charT&>, i.e., a type that can be written to charT.

    In libc++, it is simply defined as char* for simplicity. Now noted this part:

    { cf.format(t, fc) } -> same_as<typename Context::iterator>;
    

    For the const object cf of formatter<Point>, we require that the expression cf.format(p, fc) is well-formed, where fc is a object of basic_format_context<char*, char>.

    However, in your example, the type of fc is explicitly specified as std::format_context, which it is defined as basic_format_context<back_insert_iterator<__format::__output_buffer<char>>, char> in libc++. Since the latter cannot be converted from basic_format_context<char*, char>, the constraint is not satisfied.

    So, in order to make Point satisfy the std::formattable, you can turn the format() into a template function:

    template<>
    struct std::formatter<Point>
        : std::formatter<std::string>
    {
        template<class FormatContext>
          typename FormatContext::iterator
        format(const Point& p, FormatContext& ctx) const
        {
          // ...
        }
    };
    

    This makes the format library automatically support ranges whose elements are of type Point.

    Demo