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.
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
.