Search code examples
c++fmtcustom-formatting

How do I efficiently forward the formatting arguments when implementing a formatter for a custom type


I am trying to write a custom formatter to help me print vectors. I am attempting to maintain the format specifiers so that each item in the vector is formatted the same way.

I have taken most of my inspiration from this tutorial, and the docs

#include <fmt/core.h>
#include <fmt/format.h>

template<typename ValueType>
struct fmt::formatter<std::vector<ValueType>> : fmt::formatter<ValueType>
{
    std::string formatString;

    // Hack, copy the original format text into a std::string
    constexpr auto parse(format_parse_context& ctx)
    {
        formatString= "{:";
        for (auto iter = std::begin(ctx); iter  != std::end(ctx); ++iter) {
            char c = *iter;{
                formatString += c;
            }
            if (c == '}') {
                return iter;
            }
        }
        return std::end(ctx);
    }

    template <typename FormatContext>
    auto format(const std::vector<ValueType>& container, FormatContext& context)
    {
        auto&& out = context.out();
        format_to(out, "{{");

        typename std::vector<ValueType>::size_type count = 0;
        const typename std::vector<ValueType>::size_type size = container.size();
        for (const auto& item : container) {
            // Use the copied format string, but really want to delegate the formatting to superclass...
            format_to(out, formatString, item);
            if (++count < size) {
                format_to(out, ", ");
            }
        }

        return format_to(out, "}}");
    }
};

int main()
{
    fmt::print("{:.3f}\n", std::vector{ 0.0, 321.123, 654398.4328, -0.0000000000012345, 2374651273.7236457862345});
    fmt::print("{:.1e}\n", std::vector{ 0.0, 321.123, 654398.4328, -0.0000000000012345, 2374651273.7236457862345});
    return 0;
}

Which outputs:

{0.000, 321.123, 654398.433, -0.000, 2374651273.724}
{0.0e+00, 3.2e+02, 6.5e+05, -1.2e-12, 2.4e+09}

It seems overly clunky and inefficient to copy the format string just so I can feed it back into another fmt::format call, especially when the extended class : fmt::formatter<ValueType> is already providing us with a perfectly valid parse function internally, (which I have re-implemented in this example just to get the desired output in a hacky way).

I really want to remove the custom parse implementation and replace the line

format_to(out, formatString, item);

with

format_to(out, fmt::formatter<ValueType>::format(item, context))

except it isn't valid/doesn't compile.

What is the correct way to do this?


Note: I am completely aware of the pointlessness of extending the type in my example, and that I could have it as a local variable however I am trying to reuse the functionality of the class, so extending it feels like the right direction, even if I haven't found the solution yet.


A list of all the other examples I have found which haven't helped me yet:

  • String formatting the cool way with C++20 std::format() (only works for directly returning a single call to the super implementation, but it doesn't require re-implementing parse or creating a new format string)
  • Custom format specifier with {fmt} for custom class (Discards formatting or requires manual re-implementation of formatting)
  • The tutorial I mostly copied (Almost what I want, but still creating a new format string instead of reusing the one originally specified by the user)
  • The Docs (Again, either suggests re-implementing the parse function and recalling format with a new format string, despite the caller having already specified one and there being an existing parse function for the type...)

Solution

  • You can get rid of your parse implementation and use the inherited function, and use fmt::formatter<ValueType>::format(item, context) inside your format to output each item (godbolt demo):

    #include <fmt/core.h>
    #include <fmt/format.h>
    
    template<typename ValueType>
    struct fmt::formatter<std::vector<ValueType>> : fmt::formatter<ValueType>
    {
        template <typename FormatContext>
        auto format(const std::vector<ValueType>& container, FormatContext& context)
        {
            auto&& out = context.out();
            format_to(out, "{{");
    
            bool first = true;
            for (const auto& item : container) {
                if (first) {
                    first = false;
                } else {
                    format_to(out, ", ");
                }
                fmt::formatter<ValueType>::format(item, context);
            }
    
            return format_to(out, "}}");
        }
    };
    
    int main()
    {
        fmt::print("{:.3f}\n", std::vector{ 0.0, 321.123, 654398.4328, -0.0000000000012345, 2374651273.7236457862345});
        fmt::print("{:.1e}\n", std::vector{ 0.0, 321.123, 654398.4328, -0.0000000000012345, 2374651273.7236457862345});
        return 0;
    }
    

    You can see other examples of this pattern in the docs under Formatting User-defined Types.