c++stdformat

std::format handles user-defined type if it's iterable‽


I updated some older code to use std::format, and was surprised to discover that it worked despite that fact that I had forgotten to provide a std::formatter specialization for that type.

I immediately made a small test program to try to reproduce this, but those always got a compile-time error as I expected.

After hours of debugging, I figured out that, if the custom type has public begin and end methods, the library will format the sequence as a comma-separated list enclosed in square brackets.

Q: Is this a standards-defined feature of std::format or an implementation bug? (Or something else?)

Here's a self-contained repro:

#include <array>
#include <print>

class MyType {
    public:
        MyType() : m_values{1, 2, 3, 4} {}

        using internal_type = std::array<int, 4>;
        using const_iterator = typename internal_type::const_iterator;

        const_iterator cbegin() const { return m_values.cbegin(); }
        const_iterator cend()   const { return m_values.cend();   }
        const_iterator begin()  const { return cbegin(); }
        const_iterator end()    const { return cend();   }

    private:
        internal_type m_values;
};

int main() {
    MyType foo;
    // Since MyType is a user-defined type, I would not
    // expect this print statement to compile without a
    // specialization of std::formatter, but because
    // it's iterable, it prints: "foo = [1, 2, 3, 4]\n".
    std::print("foo = {}\n", foo);
    return 0;
}

I'm using MS VC++ from Visual Studio 17.12.15 and compiling with /std:c++latest.


Solution

  • The standard library defines a std::formatter specialization for ranges starting in C++23:

    template< ranges::input_range R, class CharT >
        requires (std::format_kind<R> != std::range_format::disabled) &&
                  std::formattable<ranges::range_reference_t<R>, CharT>
    struct formatter<R, CharT>;
    

    There are a few different variations on this range formatter for sequences, sets, maps, and strings. The default rules for which one gets used are:

    You can control which one gets selected by default by specializing the std::format_kind template for your type, i.e.

    namespace std {
        template <>
        constexpr range_format format_kind<MyType> = range_format::set;
    }
    

    You can explicitly select the string (with or without quotes and escape sequences) or map format type via your format string using the s, ?s, and m specifiers, respctively:

    std::println("{:s}", std::array{'?', '\t', '?'});
    // prints ? ?
    std::println("{:?s}", std::array{'?', '\t', '?'});
    // prints "?\t?"
    std::println("{:m}", std::array{std::pair{'a', 1}, std::pair{'b', 2}});
    // should print {'a': 1, 'b': 2}, but seems to not work in the current libc++ at least
    

    In addition, the n format specifier can be used to omit the brackets for range types other than string. This can be combined with the m specifier as well:

    std::print("{:n}", std::views::iota(1, 5));
    // prints 1, 2, 3, 4
    std::print("{:nm}", std::array{std::pair{'a', 1}, std::pair{'b', 2}});
    // should print 'a': 1, 'b': 2
    

    Live Demo

    Note that as of the time of writing (April 9, 2025) libstdc++ (the standard library used by GCC) has not yet implemented this functionality.