Closed seanmiddleditch closed 5 years ago
You should be able to include just fmt/core.h
for the definition of formatter
. fmt/core.h
is pretty small (just 1 kLOC) and only includes a minimal set of headers which are likely to be transitively included in any nontrivial C++ code anyway.
And while I think that modules will largely solve the issue, I agree that having a solution for non-modularized code is still important. Hopefully fmt/core.h
helps with that.
format/core.h
is still pulling in these particularly troublesome headers:
#include <iterator>
#include <string>
The <iterator>
header pulls in the behemoth that is <algorithm>
on some vendors' implementations.
The <string>
header pulls in a huge chunk of the CRT and memory routines on some vendors' implementations.
If I don't use std::string
(and I don't) I do not want to pull in the heavy dependencies of that header and to every single TU that might do some formatting (and hence wants to have access to formatter<>
specializations).
Which is a real thing. Almost all of my string format calls in any project I've worked on in the last ~5 years have been in logging and fancy assert macros. The former is optimized to write into thread-safe fast-dispatch streaming buffers (e.g. minimal allocations even with many thousands of log statements so that log overhead is as low as possible) and the latter is written to never ever possibly allocate (because asserts need to trigger in contexts where allocations are verboten, e.g. allocator assertions!). That is, neither of them use std::string
or anything similar to it. :)
As to why avoiding those headers matters, see benchmarks like those at https://blog.magnum.graphics/backstage/array-view-implementations/. While it doesn't focus on <string>
at all, it does inspect perf for similar headers, and illustrates why some of us avoid the stdlib headers wherever we can. :)
FWIW, I discovered this doing build optimization. I've stripped out almost every stdlib header from our "foundation" library (containers, assertions, allocators, etc.) and got about a ~14% compile-time reduction across the whole project, not including what gains I could get if fmt
were smaller (since I'd have to do a ton of surgery to measure that).
There's also then the case of when I do want an actual implementation of fmt::format
, I want to get that without all the header bloat. That is trickier because of std::char_traits
, which is why formatxx ended up pulling in <string>
as well when I added wchar_t
support (though for my more recent code, I just use the compiler builtins directly and rely on fat headers and std::char_traits
only as a fallback for non-mainstream compilers).
I won't be opposed to having fmt/fwd.h
(maybe with a more readable name, e.g. fmt/forward.h
) provided that fmt/core.h
remains self-contained. It means that there will be some duplication between the two headers, but it's not critical since the forwarding header will be very small, something like
namespace fmt {
inline namespace v5 {
template <typename T, typename Char, typename Enable>
struct formatter;
}
}
Note that it will be of limited use because most of the formatter
specializations are defined in fmt/format.h
.
Closing the issue for now, but I'm open to a PR that adds a forward decl header despite its limited usefulness.
fmt/core.h
no longer depends on <iterator>
and <string>
(on libc++ and libstdc++): https://vitaut.net/posts/2024/faster-cpp-compile-times/
The specialization required for custom types in fmtlib requires having the definition of
fmt::formatter<>
, as you cannot specialize a template that isn't declared. Forward-declaring this in user code is troublesome because fmtlib using versioned inline namespaces and default template parameters.It is currently thus exceedingly fragile/difficult to write a custom formatter for a type without pulling in all ~3.6k lines of
<fmt/format.h>
and the massive set of standard library dependencies it pulls in, even if only a tiny fraction of TUs using the type's declaring header might ever want to actually format the custom type in the first place.It's exceedingly unclear whether C++ modules will fully address the problem. I'd suspect that modules would certainly help a large amount, at the very least. That might be enough for the ISO standard version of fmtlib's features assuming modules land in the same C++ version, but it doesn't really help us in the non-hypothetical here and now.
The only alternative right now is to make a separate
my_type_fmt.h
header that declares the formatting support separately from the main header, and then having to remember to pull that header in wherever needed.Compare this to using the IOStreams support, where a type's declaring header need only pull in the comparively svelte
<iosfwd>
, so long as it only relies on type templates. In fact, it's actually possible in completely legal C++ (with unconstrained templates) to avoid even<iosfwd>
!or even:
The fmtlib ADL debate would allow for a similar solution, in that all fmtlib-related types can be made dependent or full template parameters, e.g. something as light as:
Given the current template specialization-based API, though, the only potentially workable solution I can think is to offer a
fmt/fwd.h
analog toiosfwd
that declares at least the minimalstruct fmt::formatter<>
that uses must specialize. This header of course should be small and have the absolute minimum dependencies (read: none).To reiterate clearly: I think a
<fmt/fwd.h>
kind of header is a lame solution, but it's the only one I see without either switching fmtlib formatters to ADL or holding my breath for widespread adoption of nigh-mythical features like Modules. I'm happy to be proven wrong and be shown a better solution that can be adopted soon, of course. :)