format_it: Iterator based string formatting

by Malte Skarupke

format_it is my attempt at writing string formatting that is good enough that it could make it into the C++ standard. It’s not quite there yet but I haven’t gotten to work on it in a few months, so I will publish what I have so far. It is fast (much faster than using snprintf to write to the stack), type safe, memory safe, doesn’t truncate, extensible, backwards compatible for std::ostream and somewhat composable.

The syntax is a mix of printf, ostream and varargs. I’ll just show an example:

// format "100 and 0.5" into 1024 bytes of stack memory
fmt::stack_format<1024> format("%0 and %1", 100, 0.5f);

// print "Hello World" into 1024 bytes of stack memory
fmt::stack_print<1024> print("Hello", "World");

// prints "100 and 0.5\nHello World\n"
auto format_to_cout = fmt::make_format_it(std::ostreambuf_iterator<char>(std::cout));
*format_to_cout++ = format;
*format_to_cout++ = '\n';
*format_to_cout++ = print;
*format_to_cout++ = '\n';

Where the first and second struct mimic snprintf, and the third example introcues the formatting iterator that they use behind the scenes.

First one clarification: I promised memory safety and no truncation, but I am writing to a fixed size buffer on the stack. The way that I guarantee safety is that there is a heap fallback in there. As long as you don’t need the heap fallback that code is much faster than snprintf. For the case where the heap fallback is used, I can construct both cases where it’s faster or slower than snprintf. When explaining the concept of a heap fallback to C++ programmers, they inevitably get angry at me, (even if it’s faster than snprintf) which is why it’s optional. The core of the library is the iterator that I use in the third example, which in the example writes straight to std::cout without formatting to temporary stack or heap memory.

Here’s another example:

std::vector<int> numbers = { 1, 10, 100 };
std::string a_string;
auto format_to_string = fmt::make_format_it(std::back_inserter(a_string));
// writes "{ 1, 10, 100 }" into the string
*format_to_string++ = numbers;
// also possible:
format_to_string.format("\n%0: %1\n", numbers.size(), numbers);
// that wrote "\n3: { 1, 10, 100 }\n" to the string. so the string is now
// {1, 10, 100 }
// 3: { 1, 10, 100 }
//

This formats into a string. As you can see the iterator has more functions than just the iterator assignment. The format() function is the core of the library. The stack_format<> struct from above just uses this. Besides this other available functions are print(), print_separated() and printpacked():


// prints "1 2 3"
fmt::cout.print(1, 2, 3).print('\n');
// prints "1, 2, 3"
fmt::cout.print_separated(", ", 1, 2, 3).print('\n');
// prints "123"
fmt::cout.printpacked(1, 2, 3).print('\n');

Where fmt::cout is a format_it iterator that wraps std::cout.

Since the iterator can write to everything, you might wonder why I even have the stack_format<> struct from my first example. The reason is simple: This has to be faster than snprintf. With all the downsides of std::stringstream, the main reason why C++ programmers don’t use it is that snprintf is faster. You can use format_it to write to a stringstream, but it would remain slower than snprintf. The default behavior, to use stack_format<> can be several times faster than snprintf, and it has to be for people to start using this.

You may have already noticed that this can print more than snprintf or std::ostream can print: It can print containers like the vector that I used in a previous example. It can also print vectors of vectors. If you include the “format_stl.hpp” header it can print all stl containers as well as std::pair and std::tuple. And it’s pretty easy to make it print your structs. But before going into extending format_it, I would like to illustrate one more benefit of it being an iterator: You can use the standard algorithms on it:

std::vector<int> numbers = { 1, 10, 100 };
// prints "{ 1, 10, 100 }"
fmt::cout.print(numbers);
// prints "110100"
std::copy(numbers.begin(), numbers.end(), fmt::cout);
// prints
// 1
// 10
// 100
std::copy(numbers.begin(), numbers.end(), fmt::with_separator(fmt::cout, "\n"));
// prints
// 1
// a
// 64
std::transform(numbers.begin(), numbers.end(), fmt::with_separator(fmt::cout, "\n"), &fmt::hex<int>);

This makes it pretty composable, but this is also one area where I’m not yet happy with what I have. I can explain why at the end.

Another function that I use in there is fmt::hex which prints an integer as hexadecimal. There’s also fmt::upperhex, fmt::oct and fmt::boolalpha. For floating point numbers there is fmt::precise_float, fmt::nodrift_float and fmt::float_as_fixed. There’s also fmt::pad_int and fmt::pad_float as well as fmt::pad_left, fmt::pad_right and fmt::pad_both. (which work on all types) All of these do roughly what they say. precise_float allows you to say how many digits you want, nodrift_float determines the necessary number of digits by itself.

But finally let’s talk about how to make it possible to print your struct. If your struct can print to a std::ostream, then format_it can already print it. Alternatively you can print your struct by writing a format() function in your namespace:

struct print_through_ostream
{
    int id = 0;
    std::string name = "hank";
};

std::ostream & operator<<(std::ostream & lhs, const print_through_ostream & rhs)
{
    return lhs << "id: " << rhs.id << ", name: " << rhs.name;
}

struct print_through_format_it
{
    int id = 0;
    std::string name = "hank";
};
template<typename C, typename It>
fmt::format_it<C, It> format(fmt::format_it<C, It> it, const print_through_format_it & self)
{
    return it.format("id: %0, name: %1", self.id, self.name);
    // alternatively:
    //return it.printpacked("id: ", self.id, ", name: ", self.name);
    // alternatively:
    // *it++ = "id: ";
    // *it++ = self.id;
    // *it++ = ", name: ";
    // *it++ = self.name;
    // return it;
}

std::string print_both(const print_through_ostream & first, const print_through_format_it & second)
{
    std::string out;
    auto it = fmt::make_format_it(std::back_inserter(out));
    it.print_separated('\n', first, second);
    return out;
}

In this example I use both the method of printing to std::ostream, and the method of formatting directly. In the template format_it<C, It> the argument C is the character type, meaning char or wchar_t or u32char_t or whatever else you want to print as, and the argument It is the output iterator. You never need to know what your output iterator is. It could be a std::back_insert_iterator or a std::ostreambuf_iterator or just a char *. You simply need to forward this argument.

Printing to std::ostream has a virtual method call, but it is the only way to print structs that use the pimpl pattern. The fact that format_it supports this means that all of your old structs can already print. But I like to use the function that prints directly to format_it because you have all the format functions available. (that being said I showed you how to make them available for a std::ostream in my first example: it’s just one (long) line of code)

If you are not satisfied with either the format method or printing to std::ostream because you have some complex overload resolution problem, there is a third way: There is a struct called fmt::formatter<> which allows you to do SFINAE tricks. I try to avoid SFINAE because of the compilation time overhead and I don’t use it in the code that ships with the library. But struct overload rules are a bit more powerful than function overload rules, so if you want to use them feel free to specialize that struct.

So in summary: Compared to snprintf this is

  • Faster
  • Memory Safe
  • Type Safe
  • Doesn’t Truncate
  • Extensible
  • Works with std::ostream
  • Composes better
  • Compiles more slowly

That last point is the main downside of this, but ultimately there isn’t much you can do: The reason for snprintf compiling faster is that it drops type information. And that’s why snprintf is the source of so many errors and why snprintf is not extensible.

The main benefit of this over stringstream is that it is faster and that you can use printf style syntax, which is useful in several situations. For example some people use this for translation, where you can have the string “Hello %0, how’s it going?” in English and “Hallo %0, wie gehts?” in German.

OK so why am I not happy with this yet? It’s not stl quality yet. One issue is how it doesn’t compose quite as well as I would like it to. For example there are four different ways to print values with a separator. Every one is used for different situations. I feel like there should be a better way though. And code should only be in the standard library if it composes well and is easily extensible. I think I already beat stringstream in that regard, but the iterator based approach could be more powerful still.

I also wanted to explore the idea of getting some simple scripting in there. I don’t remember where I have this from, but I saw that someone had support for “How is the Commander? Is %{0 male:he, female:she} recovering?” Which is useful for translation because in German that would have to be “Wie geht es %{0 male:dem Kommandanten, female: der Kommandantin}? Erholt %{0 male:er, female:sie} sich?” Which for example the Mass Effect series always gets wrong. The printf style syntax already is a simple form of scripting (because you can move text around) and I feel like adding a tiny layer of text replacement on top of that could improve things a lot. This has to be in the core feature set because users can’t add this. But I need to come up with a good design first. Maybe text replacement is enough, but maybe users should be able to write their own macros. (and maybe I should skip all this because this should be handled at a higher level. But I have seen enough poor translations where a sentence was gender neutral in English but required a different sentence in German depending on if your character was male or female, that I think it would be valuable if this was a basic feature)

Finally performance numbers. I ran each of these lines two million times to get nice big numbers:

Printing strings
fmt::stack_format<1024>(“[%0, %1, %2]”, “foo”, “bar”, “baz”); 182 ms
snprintf(buffer, 1024, “[%s, %s, %s]”, “foo”, “bar”, “baz”); 408 ms
std::stringstream() << ‘[‘ << “foo” << “, ” << “bar” << “, ” << “baz” << ‘]’; 1444 ms
Printing ints
fmt::stack_format<1024>(“[%0, %1, %2]”, i, i * 2, i / 2); 267 ms
snprintf(buffer, 1024, “[%d, %d, %d]”, i, i * 2, i / 2); 397 ms
std::stringstream() << ‘[‘ << i << “, ” << i * 2 << “, ” << i / 2 << ‘]’; 1598 ms
Printing floats
fmt::stack_format<1024>(“[%0, %1, %2]”, static_cast<float>(i), i * 2.0f, i / 2.0f); 1125 ms
snprintf(buffer, 1024, “[%g, %g, %g]”, static_cast<float>(i), i * 2.0f, i / 2.0f); 3198 ms
std::stringstream() << ‘[‘ << static_cast<float>(i) << “, ” << i * 2.0f << “, ” << i / 2.0f << ‘]’; 5473 ms

The floating point code is so much faster because I use Google’s double-conversion library. The other code is faster because I keep type information and because printf is such a huge beast. The printf code is a giant monster that handles a ton of possible flags and combinations. In my code I handle things like hexadecimal printing as extensions which don’t need to be part of the core format_it. I think printf could speed up, but since it’s written in C it has to drop type information and can never be as fast as C++ code. The code used for the profiling is at the bottom of this file.

And once again here is the link to the library. Feel free to take this and to build something standard quality with this. I haven’t had time recently and I don’t think I will have time to finish this for at least the next year.