C++20: {fmt} and std::format

In this post I will show a very nice open source library that lets the programmers create formatted text in a very simple, type-safe and translatable way: {fmt}

{fmt} is also the implementation of reference for the C++20 standard std::format (the {fmt}‘s author [https://www.zverovich.net/] submitted his paper to the standard committee) but until this moment (June, 2021) only Microsoft Visual Studio implements it. So I will describe the standalone library for now.

These are the main features we can find in {fmt}:

  • We can specify the format and the variables to be formatted, similar to C printf family.
  • The {fmt} functions do not require the programmer to match the type of the format specifier to the actual values the are being formatted.
  • With {fmt}, programmers can optionally specify the order of values to be formatted. This feature is very very useful when internationalizing texts where the order of the parameters is not always the same as in the original language.
  • Some format validations can be done in compile time.
  • It is based on variadic templates but there are lower-level functions on this library that fail over to C varargs.
  • Its performance its way ahead to C++ std::ostream family.

To test {fmt}, I used Godbolt’s Compiler Explorer.

Hello world

This is a very simple “Hello world” program using {fmt}:

#include <fmt/core.h>

int main()
{
    fmt::print("Hello world using {{fmt}}\n");
}

fmt::print() is a helper function that prints formatted text using the standard output (similar to what std::cout does).

As you can see, I did not specify any variables to be formatted BUT I wrote {{fmt}} instead of {fmt} because {{ and }} are, for the {fmt} functions, escape sequences for { and } respectively, which when used as individual characters, contain a format specifier, an order sequence, or simply mean that will be replaced by a parameterized value.

Using simple variable replacements

For example I want to print out two values, I can do something like:

int age = 16;
std::string name = "Ariana";

fmt::print("Hello, my name is {} and I'm {} years old", name, age);

This will print:

Hello, my name is Ariana and I'm 16 years old.

fmt::print() replaced the {} with the respective variable values. Notice also that I used std::string and it worked seamlessly. Using printf(), the programmers should get the const char* representation of the string to be displayed.

If the programmers specify less variables than {}, the compiler (or runtime) returns an error. More on this below.

If the programmers specify more variables than {}, the extra ones are simply ignored by fmt::print() or fmt::format().

Defining arguments order

The programmers can also specify the order the parameters will be replaced:

void show_numbers_name(bool ascending)
{
    std::string_view format_spec = ascending ? "{0}, {1}, {2}\n" : "{2}, {1}, {0}\n";
    
    auto one = "one";
    auto two = "two";
    auto three = "three";

    fmt::print(format_spec, one, two, three);
}

int main()
{
    show_numbers_name(true);
    show_numbers_name(false);
}

As you can see, the {} can contain a number that tells {fmt} the number of the argument that will be used in such position when formatting the output.

Formatting containers

Containers can easily printed out including #include <fmt/ranges.h>

std::vector<int> my_vec = { 10, 20, 30, 40, 50 };
fmt::print("{}\n", my_vec);

Format errors

There are two ways that {fmt} uses to process errors:

If there is an error in the format, a runtime exception is thrown, for example:

int main()
{
    try
    {
        fmt::print("{}, {}, {}\n", 1, 2);
    }
    catch (const fmt::format_error& ex)
    {
        fmt::print("Exception: {}\n", ex.what());
    }
}

In the example above, I say that I have three parameters but only provided two variables, so a fmt::format_error exception is thrown.

But if the format specifier is always constant, we can specify the format correctness in runtime in this way:

#include <fmt/core.h>
#include <fmt/compile.h>

int main()
{
    fmt::print(FMT_COMPILE("{}, {}, {}\n"), 1, 2);
}

FMT_COMPILE is a macro found in <fmt/compile.h> that performs the format validation in compile-time, thus in this case, a compile-time error is produced.

Custom types

To format your custom types, you must to create a template specialization of the class template fmt::formatter and implement the methods parse() and format(), as in this example:

// My custom type
struct person
{
    std::string first_name;
    std::string last_name;
    size_t social_id;
};

// fmt::formatter full template specialization
template <>
struct fmt::formatter<person>
{
    // Parses the format specifier, if needed (in my case, only return an iterator to the context)
    constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); }

    // Actual formatting. The second parameter is the format specifier and the next parameters are the actual values from my custom type
    template <typename FormatContext>
    auto format(const person& p, FormatContext& ctx) {
        return format_to(
            ctx.out(), 
            "[{}] {}, {}", p.social_id, p.last_name, p.first_name);
    }
};

int main()
{
    fmt::print("User: {}\n", person { "Juana", "Azurduy", 23423421 });
}

Neat, huh?

Links

{fmt} GitHub page: https://github.com/fmtlib/fmt

{fmt} API reference: https://fmt.dev/latest/api.html

Compiler explorer: https://godbolt.org/