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
.
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:
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 mapR::key_type
is valid and a type then it's a setYou 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
Note that as of the time of writing (April 9, 2025) libstdc++ (the standard library used by GCC) has not yet implemented this functionality.