This post is three parts journey, one part self-indulgence, and one part
this-might-be-useful-to-you. In short: I ported fmt::join
to work with
<format>
. You can find it at
barometz/nofmt-join.
At work, we're hoping to switch some things from the excellent
{fmt} library to the now standardized <format>
. The one
largely inspired the other, so the porting burden won't be too much, and we get
to drop a dependency in a hundred different builds - some of which in turn
depend on each other, so there's some transitive lock-in with the version.
Package management mistakes aside, <format>
lacks one thing I love from fmt:
fmt::join
.
What is fmt::join
?
It looks a lot like Python's
str.join
at first
glance:
1 2 3 |
|
Except it also works with other types fmt knows how to format:
1 2 3 |
|
That by itself is convenient but not too exciting - you can do it in about ten lines:
join.cpp (28 lines)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
But what makes fmt::join
special is this:
1 2 3 |
|
That's right: you get to apply a format specifier (:02x
in the example) to
each of the joined elements. Nothing's actually converted to a string before it
passes through fmt::format
or fmt::print
.
<format>
& join
So <format>
doesn't include the join
function template. It's tremendously
useful and not entirely trivial to write. It wasn't included in the original
<format>
proposal
- maybe the author wanted to limit the scope to ease acceptance, or maybe they
didn't want a conflict with an older proposal for
std::join.
At any rate, I decided I wanted to port fmt::join
to work with <format>
.
Porting
The proof of concept
With this sort of thing it's helpful to start with a proof of concept - don't
worry too much about being tidy or having a well-organized commit history, just
get the thing running. So I pulled up a checkout of fmt I already had, and
copied the fmt::join
function template itself. That doesn't build, clearly, so
I copied the type it returns:
1 2 3 4 5 6 |
|
It turns out that fmt::join
doesn't do all that much - it takes your
parameters and turns it into a view of the range (delimited by begin
and
end
) plus a view of the separator. The real work happens in the formatter
defined for arg_join
, which I'll get to later.
Copying the formatter isn't enough, though - there are still dependencies on other fmt internals. Some of those provide C++ library features which were added after C++11, while others are just fmt-internal. The former I can replace with whatever's available in C++20 (since that's my porting target). Others, I copy over until it builds.
For real
Satisfied that everything fits together, I check out the latest release of fmt - 8.1.1 at the time - and get to work:
- Copy all the chunks I copied during my proof of concept into my own header.
- Add comments to each function and struct to indicate what file and line it's from.
- Commit that as a baseline.
- Make all the changes I made before to get it to compile in its new context.
- Build.
- .. doesn't work. Drat, still missing more bits.
- Rebase to modify the first commit,
goto
2.
After a little while of this I realized 8.1.1's fmt::join
is more tightly
coupled with the fmt internals than whatever I'd previously been working with,
and so...
For real, for real.
I went back to fmt 7.1.3, the latest release of fmt 7. This turned out to be
much simpler to port than what I'd started with: apart from arg_join
and its
formatter, fmt::join
7.1.3 had no dependencies that I couldn't easily replace
with C++ standard library functions.
- Copy
arg_join
,fmt::formatter<arg_join<...>>
, andfmt::join
fromfmt/format.h
. - Commit that as a baseline.
- Update everything to use the C++ standard library and adapt it to its new namespace.
- Commit that.
The end result, including tests and CI, is at barometz/nofmt-join.
How it works
As mentioned above, the "real work" happens in the formatter for arg_join
:
std::formatter for arg_join (23 lines)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
The core of which looks a lot like join.cpp
above:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Next up: fmt 8
There's a reason fmt 8's fmt::join
is less easy to port: it uses more
fmt-internal machinery which (if I'm reading the comments correctly) is intended
to improve compilation times. That sounds pretty good to me, so I do want to
get back to that at some
point. I would want to demonstrate the benefit in that case, because the
current implementation of nofmt::join
is pleasantly small.