Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

c++

stdformat

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.

like image 272
Adrian McCarthy Avatar asked May 11 '26 14:05

Adrian McCarthy


1 Answers

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:

  • If R::key_type and R::mapped_type are defined and std::remove_cvref_t<std::range_reference_t<R>> is a specialization of std::pair or std::tuple with size 2 then it's a map
  • Otherwise if R::key_type is valid and a type then it's a set
  • Otherwise it's a sequence

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.

like image 149
Miles Budnek Avatar answered May 13 '26 02:05

Miles Budnek



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!