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.

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* and nullptr_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 months ago. If you want to get exlusive content, early previews, bonus materials and access to Discord server, join
the C++ Stories Premium membership.

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) {
        return std::format_to(ctx.out(), /* */);
    }
};

Here are the main requirements for those functions (from the Standard):

Expression Return type Requirement
f.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).

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) {
        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) {
        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) {
        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) {
        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) {
        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.

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) {
        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(). 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 July 2022, GCC doesn’t implement this feature. Clang 14 has an experimental internal implementation, but it’s not yet exposed.

References