Experience with std::format
Intro
With the recent release of GCC 13 and Clang 17, <format>
is finally avaiable to the most of us.
I’m using GCC and got some hand-on experience with it.
I’ve been a long time user of fmtlib,
the library which the standard <format>
is based on.
So it’s natural for me to try to use <format>
the way I use <fmt/core.h>
;
can it be done by simply replacing fmt::
with std::
?
Formatting User Type
Printing user types has been a common task. In the past it is done by overloading operator<<
.
With fmtlib, it is done by providing a formatter class specialization.
The example I’ve been using since 2019 is:
// the type I want to format
struct Point {
int x, y;
};
namespace fmt {
// formatter for it
template<>
struct formatter<Point> {
template <class Context>
auto parse(Context& ctx) { return ctx.begin(); }
template <class Context>
auto format(const Point& p, Context& ctx) {
return format_to(ctx.out(), "Point({}, {})", p.x, p.y);
}
};
}
Does it work if I simply change namespace fmt
to namespace std
?
Unfortunately, no. The first error I got is:
C:/msys64/ucrt64/include/c++/13.1.0/format:3595:47: error: call to non-'constexpr' function 'auto std::formatter<Point>::parse(Context&) [with Context = std::basic_format_parse_context<char>]' 3595 | this->_M_pc.advance_to(__f.parse(this->_M_pc));
It seems quite clear - the compiler wants parse
to be constexpr
.
This makes sense, as it may want to do some compile-time validation.
No problem. So I modified my code to be:
template <class Context>
constexpr auto parse(Context& ctx) { return ctx.begin(); }
// ^^^^^^^^^
and compile again.
It still doesn’t work. And there are walls of error texts.
Thankfully, as C++ programmers we’re already used to dealing with walls of error text. Here was my then throught process:
The direct error is:
C:/msys64/ucrt64/include/c++/13.1.0/format:3241:38: error: no matching function for call to 'std::basic_format_arg<std::basic_format_context<std::__format::_Sink_iter<char>, char> >::basic_format_arg(Point&)'
This doesn’t tell me much. So I keep reading.
Somewhere down the line:
C:/msys64/ucrt64/include/c++/13.1.0/format:2996:9: note: template argument deduction/substitution failed:
C:/msys64/ucrt64/include/c++/13.1.0/format:2996:9: note: constraints not satisfied
...
C:/msys64/ucrt64/include/c++/13.1.0/format:2201:20: note: the required expression '__cf.format(__t, __fc)' is invalid
2201 | { __cf.format(__t, __fc) } -> same_as<typename _Context::iterator>;
I assume __cf
is my formatter, __t
is a Point
, and __fc
is the formatter context.
But how is its return type not iterator though?
Keep reading, and I found
cc1plus.exe: note: set '-fconcepts-diagnostics-depth=' to at least 2 for more detail
So I compile again with -fconcepts-diagnostics-depth=2
. This time it includes a new error:
C:/msys64/ucrt64/include/c++/13.1.0/format:2201:20: error: passing 'const std::formatter<Point>' as 'this' argument discards qualifiers [-fpermissive]
And that’s why! The member function format(...)
needs to be const
. Now everything compiles.
namespace std {
template<>
struct formatter<Point> {
template <class Context>
constexpr auto parse(Context& ctx) { return ctx.begin(); }
template <class Context>
auto format(const Point& p, Context& ctx) const {
return format_to(ctx.out(), "Point({}, {})", p.x, p.y);
}
};
}
To be fair, after this I checked the fmtlib documentation again,
and the up-to-date user type formatter example does include constexpr
and const
.
I guess I’ll just have to keep these in mind from now on.
Format String Forwarding
While std::format
is here, std::print
is not. This means that we have to write
std::cout << std::format("The answer is {}\n", 42);
for now.
Still, it is desirable to provide a poor man’s print
to make it convenient:
template<class FmtString, class ...Args>
void print(FmtString fmt, Args&&... args) {
std::cout << std::format(fmt, std::forward<Args>(args)...);
}
int main() {
print("This answer is {}\n", 42);
}
Unfortunately, the above code doesn’t compile. The error is:
formatter.cpp: In instantiation of 'void print(FmtString, Args&& ...) [with FmtString = const char*; Args = {int}]':
formatter.cpp:32:10: required from here
formatter.cpp:26:29: in 'constexpr' expansion of 'std::basic_format_string<char, int>(fmt)'
formatter.cpp:26:29: error: 'fmt' is not a constant expression
26 | std::cout << std::format(fmt, std::forward<Args>(args)...);
|
This one took a little bit of digging. It turns out std::format
only supports compile-time format string,
as it is mandated to perform format string checking at compile time.
The way it does this is to have a format string type that depends on the arguments (format_string<Args...>
),
so it can “remember” the argument types. The constructor of format_string
is consteval
and it cannot read through an “outside” const char*
, which our print
essentially does, hence the error.
The make our print
work:
template<class ...Args>
void print(std::format_string<Args...> fmt, Args&&... args) {
std::cout << std::format(fmt, std::forward<Args>(args)...);
}
Afterword
It is great to have std::format
available, even though it is quite restricted.
If any of the following is required:
- C++17 or before
- Dynamic format string
print
Then only fmtlib can fufill the requirements.
For reasons above, I expect fmtlib to continue to be widely used in the near future.