fmtlib / fmt

A modern formatting library
https://fmt.dev
Other
20.73k stars 2.49k forks source link

Custom formatting without all of `format` #1046

Closed seanmiddleditch closed 5 years ago

seanmiddleditch commented 5 years ago

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

#include <iosfwd>
template <typename C>
auto& operator<<(std::basic_ostream<C>& os, my_type const& mt) {
  return os << mt.field;
}

or even:

// without so much as iosfwd
template <typename OutputStream>
auto& operator<<(OutputStream& os, my_type const& mt) {
  return os << mt.field;
}

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:

template<typename Formatter, typename Context>
void format_arg(Formatter& fmt, Context& ctx, my_type const& mt) {
   // or whatever the actual API would end up being
}

Given the current template specialization-based API, though, the only potentially workable solution I can think is to offer a fmt/fwd.h analog to iosfwd that declares at least the minimal struct fmt::formatter<> that uses must specialize. This header of course should be small and have the absolute minimum dependencies (read: none).

#include <fmt/fwd.h>
namespace custom {
   struct my_type {/*...*/};
}
namespace fmt {
  template<>
  struct formatter<custom::my_type> : formatter<something_similar> {
    template <typename FormatContext>
    auto format(my_type const& value, FormatContext& ctx) {
      // what a massive pain in the butt this all is, but
      // at least we don't have to include fmt/format.h
     return formatter<something_similar>::format(value, ctx);
    }
  };
}

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

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

vitaut commented 5 years ago

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.

seanmiddleditch commented 5 years ago

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

vitaut commented 5 years ago

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.

vitaut commented 5 years ago

Closing the issue for now, but I'm open to a PR that adds a forward decl header despite its limited usefulness.

vitaut commented 10 months ago

fmt/core.h no longer depends on <iterator> and <string> (on libc++ and libstdc++): https://vitaut.net/posts/2024/faster-cpp-compile-times/