Last Update:
Formatting Custom types with std::format from C++20
Table of Contents
std::format
is a large and powerful addition in C++20 that allows us to format text into strings efficiently. It adds Python-style formatting with safety and ease of use.
This article will show you how to implement custom formatters that fit into this new std::format
architecture.
Updated in Nov 2023: reflect the const
ness of the format()
function, clarified in LWG issue 3636.
Quick Introduction to std::format
Here’s the Hello World example:
#include <format>
#include <iostream>
#include <chrono>
int main() {
auto ym = std::chrono::year { 2022 } / std::chrono::July;
std::string msg = std::format("{:*^10}\n{:*>10}\nin{}!", "hello", "world", ym);
std::cout << msg;
}
Play at Compiler Explorer.
The output:
**hello***
*****world
in2022/Jul!
As you can see, we have argument placeholders that are expanded and formatted into a std::string
object. What’s more, we have various specifiers to control the output (type, length, precision, fill chars, etc.). We can also use empty placeholder {}
, which provides a default output for a given type (for example, even std::chrono
types are supported!). Later, we can output that string to a stream object.
Read more about the design and feature in a separate blog post: An Extraterrestrial Guide to C++20 Text Formatting - C++ Stories.
Existing formatters
By default, std::format
supports the following types:
char
,wchar_t
- string types - including
std::basic_string
,std::basic_string_view
, character arrays, string literals - arithmetic types
- and pointers:
void*
,const void*
andnullptr_t
This is defined in the standard by formatter
, see in the spec [format.formatter.spec]:
When you call:
std::cout << std::format("10 = {}, 42 = {:10}\n", 10, 42);
The call will create two formatters, one for each argument. They are responsible for parsing the format specifier and the formatting the value into the output.
The specializations for formatters:
template<> struct formatter<char, char>;
template<> struct formatter<char, wchar_t>;
template<> struct formatter<wchar_t, wchar_t>;
For each charT
, the string type specializations.
template<> struct formatter<charT*, charT>;
template<> struct formatter<const charT*, charT>;
template<size_t N> struct formatter<const charT[N], charT>;
template<class traits, class Allocator>
struct formatter<basic_string<charT, traits, Allocator>, charT>;
template<class traits>
struct formatter<basic_string_view<charT, traits>, charT>;
For each charT
, for each cv-unqualified arithmetic type ArithmeticT
other than char
, wchar_t
, char8_t
, char16_t
, or char32_t
, a specialization:
template<> struct formatter<ArithmeticT, charT>;
For each charT
, the pointer type specializations:
template<> struct formatter<nullptr_t, charT>;
template<> struct formatter<void*, charT>;
template<> struct formatter<const void*, charT>;
For example, if you want to print a pointer:
int val = 10;
std::cout << std::format("val = {}, &val = {}\n", val, &val);
It won’t work, and you’ll get a compiler error (not short, but at least descriptive) that:
auto std::make_format_args<std::format_context,int,int*>(const int &,int *const &)'
was being compiled and failed to find the required specializations...
This is because we tried to print int*
but the library only supports void*
. We can fix this by writing:
int val = 10;
std::cout << std::format("val = {}, &val = {}\n", val, static_cast<void*>(&val));
And the output can be (MSVC, x64, Debug):
val = 10, &val = 0xf5e64ff2c4
In the {fmt}
library, there’s even a utility, but it’s not in the Standard.
template<typename T> auto fmt::ptr(T p) -> const void*
Ok, but how about custom types then?
For streams, you could override operator <<
, and it worked. Is this also that simple for std::format
?
Let’s have a look.
This article started as a preview for Patrons, sometimes even months before the publication. If you want to get extra content, previews, free ebooks and access to our Discord server, join the C++ Stories Premium membership or see more information.
Custom formatters
With std::format
, the main idea is to provide a custom specialization for the formatter
for your type.
To create a formatter, we can use the following code:
template <>
struct std::formatter<MyType> {
constexpr auto parse(std::format_parse_context& ctx) {
return /* */;
}
auto format(const MyType& obj, std::format_context& ctx) const {
return std::format_to(ctx.out(), /* */);
}
};
Here are the main requirements for those functions (from the Standard):
Expression | Return type | Requirement |
---|---|---|
g.parse(pc) |
PC::iterator |
Parses format-spec ([format.string]) for type T in the range [pc.begin(), pc.end()) until the first unmatched character. Throws format_error unless the whole range is parsed or the unmatched character is }. Note: This allows formatters to emit meaningful error messages. Stores the parsed format specifiers in *this and returns an iterator past the end of the parsed range. |
f.format(t, fc) |
FC::iterator |
Formats t according to the specifiers stored in *this , writes the output to fc.out() and returns an iterator past the end of the output range. The output shall only depend on t , fc.locale() , and the range [pc.begin(), pc.end()) from the last call to f.parse(pc) . |
Where:
f
is a value of type (possiblyconst
) F, (in other words: theformat
function should be a const member function)g
is an lvalue of type F.
This is more code that we used to write for operator <<
, and sounds more complex, so let’s try to decipher the Standard.
Single Values
For a start, let’s take a simple wrapper type with a single value:
struct Index {
unsigned int id_{ 0 };
};
And then we can write the following formatter:
template <>
struct std::formatter<Index> {
// for debugging only
formatter() { std::cout << "formatter<Index>()\n"; }
constexpr auto parse(std::format_parse_context& ctx) {
return ctx.begin();
}
auto format(const Index& id, std::format_context& ctx) const {
return std::format_to(ctx.out(), "{}", id.id_);
}
};
Use case:
Index id{ 100 };
std::cout << std::format("id {}\n", id);
std::cout << std::format("id duplicated {0} {0}\n", id);
We have the following output:
formatter<Index>()
id 100
formatter<Index>()
formatter<Index>()
id duplicated 100 100
As you can see, even for a duplicated argument {0}
, two formatters are created, not one.
The parse()
function takes the context and gets the format spec for a given argument.
For example:
"{0}" // ctx.begin() points to `}`
"{0:d}" // ctx.begin() points to `d`, begin-end is "d}"
"{:hello}" // ctx.begin points to 'h' and begin-end is "hello}"
The parse()
function has to return the iterator to the closing bracket, so we need to find it or assume it’s at the position of ctx.begin()
.
In a case of {:hello}
returning begin()
will not point to }
and thus, you’ll get some runtime error - an exception will be thrown. So be careful!
For a simple case with just one value we can rely on the standard implementation and reuse it:
template <>
struct std::formatter<Index> : std::formatter<int> {
auto format(const Index& id, std::format_context& ctx) const {
return std::formatter<int>::format(id.id_, ctx);
}
};
Now, our code will work and parse standard specifiers:
Index id{ 100 };
std::cout << std::format("id {:*<11d}\n", id);
std::cout << std::format("id {:*^11d}\n", id);
output:
id 100********
id ****100****
Multiple Values
How about cases where we’d like to show multiple values:
struct Color {
uint8_t r{ 0 };
uint8_t g{ 0 };
uint8_t b{ 0 };
};
To create a formatter, we can use the following code:
template <>
struct std::formatter<Color> {
constexpr auto parse(std::format_parse_context& ctx) {
return ctx.begin();
}
auto format(const Color& col, std::format_context& ctx) const {
return std::format_to(ctx.out(), "({}, {}, {})", col.r, col.g, col.b);
}
};
This supports only fixed output format and no additional format specifiers.
We can, however rely on the predefined string_view
formatter:
template <>
struct std::formatter<Color> : std::formatter<string_view> {
auto format(const Color& col, std::format_context& ctx) const {
std::string temp;
std::format_to(std::back_inserter(temp), "({}, {}, {})",
col.r, col.g, col.b);
return std::formatter<string_view>::format(temp, ctx);
}
};
We don’t have to implement the parse()
function with the above code. Inside format()
, we output the color values to a temporary buffer, and then we reuse the underlying formatter to output the final string.
Similarly, if your object holds a container of values, you can write the following code:
template <>
struct std::formatter<YourType> : std::formatter<string_view> {
auto format(const YourType& obj, std::format_context& ctx) const {
std::string temp;
std::format_to(std::back_inserter(temp), "{} - ", obj.GetName());
for (const auto& elem : obj.GetValues())
std::format_to(std::back_inserter(temp), "{}, ", elem);
return std::formatter<string_view>::format(temp, ctx);
}
};
The formatter above will print obj.GetName()
and then followed by elements from the obj.GetValues()
container. Since we inherit from the string_view
formatter class, the standard format specifiers also apply here.
The format()
function should be const
Thanks to a watchful reader, I improved my article regarding the constness of the format()
function.
When I published the article in July 2022, it was unclear from the specification if the function should be const or not.
But there was a fix, applied in November 2022, which corrected this wording:
Issue 3636: formatter::format should be const-qualified
According to Victor Zverovich, his intent was that f.parse(pc) should modify the state of f, but f.format(u, fc) should merely read f’s state to support format string compilation where formatter objects are immutable and therefore the format function must be const-qualified.
Extending the formatter with parse()
function
But how about a custom parsing function?
The main idea is that we can parse the format string and then store some state in *this
, then we can use the information in the format call.
Let’s try:
template <>
struct std::formatter<Color> {
constexpr auto parse(std::format_parse_context& ctx){
auto pos = ctx.begin();
while (pos != ctx.end() && *pos != '}') {
if (*pos == 'h' || *pos == 'H')
isHex_ = true;
++pos;
}
return pos; // expect `}` at this position, otherwise,
// it's error! exception!
}
auto format(const Color& col, std::format_context& ctx) const {
if (isHex_) {
uint32_t val = col.r << 16 | col.g << 8 | col.b;
return std::format_to(ctx.out(), "#{:x}", val);
}
return std::format_to(ctx.out(), "({}, {}, {})", col.r, col.g, col.b);
}
bool isHex_{ false };
};
And the test:
std::cout << std::format("col {}\n", Color{ 100, 200, 255 });
std::cout << std::format("col {:h}\n", Color{ 100, 200, 255 });
the output:
col (100, 200, 255)
col #64c8ff
Summary
To provide support for custom types and std::format
we have to implement a specialization for std::formatter
. This class has to expose parse()
function and format()
(const member function). The first one is responsible for parsing the format specifier and storing additional data in *this
if needed. The latter function outputs the values into the out
buffer provided by the formatting context.
While implementing a formatter might be tricker than operator <<
, it gives a lot of options and flexibility. For simple cases, we can also rely on inheritance and reuse functionality from existing formatters.
Play with the code for this article at Compiler Explorer.
On Visual Studio 2022 version 17.2 and Visual Studio 2019 version 16.11.14 you can use std:c++20
flag, but before those versions, use /std:latest
(as it was still under development). As of November 2023, GCC 13 and Clang 15 implement this feature.
References
- API Reference — fmt 8.1.0 documentation
- Formatting user-defined types with {fmt} library - in many cases, we can just replace
fmt::
withstd::
, so looking at the documentation for fmt is very handy. - An Extraterrestrial Guide to C++20 Text Formatting - C++ Stories
- C++20 - The Complete Guide by Nicolai M. Josuttis - tricks with inheriting existing types and reusing their format function.
- MSVC’s STL Completes /std:c++20 - C++ Team Blog
- Issue 3636: formatter::format should be const-qualified - clarification of constness for the
format()
function.
I've prepared a valuable bonus for you!
Learn all major features of recent C++ Standards on my Reference Cards!
Check it out here: