This is a brief comparison between Strf version 0.15 and {fmt} version 8.0.1.

1. Capabilities in general

1.1. {fmt}

Some things that are directly supported by {fmt}, but not by Strf:

  • Printing date and time ( link )

  • Printing any type that can be printed by std::ostream ( link )

  • Printing std::tuples ( link )

  • Specifying colors ( link )

  • Writing to output iterators ( link )

  • Named arguments in the format string. ( link )

  • Alternative functions that follow the std::printf format string syntax. ( link )

1.2. Strf

Some things that are directly supported by Strf, but not by {fmt}:

  • Character encoding conversion

auto str = strf::to_string( "aaa-"
                          , strf::conv(u"bbb-")
                          , strf::conv(U"ccc-")
                          , strf::conv(L"ddd") );

assert(str   ==  "aaa-bbb-ccc-ddd");
auto str_utf8 = strf::to_u8string
        ( strf::conv("--\xA4--", strf::iso_8859_1<char>)
        , strf::conv("--\xA4--", strf::iso_8859_15<char>));
assert(str_utf8 == u8"--\u00A4----\u20AC--");
  • Printing char32_t

char32_t ch = 0x20AC;
assert(strf::to_string.with(strf::utf<char>)         (ch) == "\xE2\x82\xAC");
assert(strf::to_string.with(strf::iso_8859_15<char>) (ch) == "\xA4");
assert(strf::to_string.with(strf::iso_8859_1<char>)  (ch) == "?");
  • Aligning a group of arguments as if they were one

auto str = strf::to_string
    ("---", strf::join_center(15) ("abc", "def", 123), "---");
assert(str == "---   abcdef123   ---");
  • When printing range, it is possible to apply formatting:

int array[] = { 250, 251, 252 };
auto str = strf::to_string
    ( "["
    , *strf::fmt_separated_range(array, " / ").fill('.').hex() > 6
    , "]");
assert(str == "[..0xfa / ..0xfb / ..0xfc]");
  • When printing range, it is possible to transform the elements:

std::map<int, const char*> table = { {1, "one"}, {2, "two"}, {1000, "a thousand"} };
auto op = [](auto p) {
    // transforming a std::pair into somthing printable
    return strf::join(p.first, " -> '", p.second, '\'');
};
auto str = strf::to_string('[', strf::separated_range(table, "; ", op), ']');
assert(str == "[1 -> 'one'; 2 -> 'two'; 1000 -> 'a thousand']");

2. Floating-points

Both libraries are capable of "dynamic precision", i.e. printing a floating-point value with the minimum amount of digits that ensures that it can be exactly recovered by a parser. The difference is that {fmt} only does this when none of formatting flags 'e', 'E', 'f', 'F', 'g' or 'G' is used. Strf does it whenever precision is not specified.

There is also a small difference on how they do the "sing-aware zero-padding", i.e. inserting zeros after the sign and before the digits, but only when the value is not NaN nor Infinit. This behaviour is activated with the zero flag ('0') in {fmt}, and with the pad0 function in Strf. The difference is that in Strf this zero-padding is decoupled from alignment formatting, so that you can specify both independently:

auto result = strf::to_string(strf::pad0(-1.5, 8).fill('_') > 12);
assert(result == "____-00001.5");

The last difference has to do with the long double type, which in some systems has more than 64 bits ( on linux, it has 80 bits on x86 and x86-64 arquitecture ). Currently, Strf only supports 32 and 64 bits floating-points, so trying to print a long double value causes a compilation error.

I’m not sure how {fmt} handles larger floating-point types, but I noticed that the "dynamic precision" doesn’t work:

fmt::print(stdout, "{}\n", sqrt(2.0));  // prints 1.4142135623730951 (ok)
fmt::print(stdout, "{}\n", sqrtl(2.0)); // prints 1.41421

3. Width calculation

The C++ Standard specifies that to calculate the width of a string, std::format shall identify each grapheme cluster in it and assume its width is equal to the width of its leading codepoint, which in turn is equal to 1 or 2, according to a certain table.[1]

Strf is expected to implement that behaviour in version 0.16, and I think {fmt} will problably do it even earlier. Currently, the best that Strf does is to count the total number of codepoints, while {fmt} is a step ahead and take into account the width of each individual codepoint.

However, one advatange of Strf is that the width calculation is customizable: you can choose a less accurate but faster algorithm if you want.

4. Extensibility

4.1. Adding printable types

It may be a little bit more difficult to learn how to add a printable type in Strf than how it is in {fmt}. There are more things to learn, more concepts to grasp. However, once have this knowledge, you will find it easier to deal with the real cases scenarious.

For instance, the {fmt} documentation provides an example of how to do it with a struct named point that contains two double variables. If you compare it with the snippet below, which does the similar thing in Strf, you may find that the Strf’s way is more complicated, since the code is larger and it requires more specific knowledge about the library. But you must take into account that this sample supports all formatting options that would be expected in a real case scenario: all the floating-point formatting options, as well the alignment formatting options, while the {fmt} sample handles only the 'f' or 'g' format flags. Also, the code below is generic, in the sense that it works with all characters types as well as all character encodings:

template <typename FloatT>
struct point{ FloatT x, y; };

template <typename FloatT>
struct strf::print_traits<point<FloatT>> {
    using forwarded_type = point<FloatT>;
    using formatters = strf::tag<strf::alignment_formatter, strf::float_formatter>; (1)

    template <typename CharT, typename Preview, typename FPack, typename... T>
    constexpr static auto make_printer_input
        ( strf::tag<CharT>
        , Preview& preview
        , const FPack& fp
        , strf::value_with_formatters<T...> arg ) noexcept
    {
        point<FloatT> p = arg.value();
        auto arg2 = strf::join
            ( (CharT)'('
            , strf::fmt(p.x).set_float_format(arg.get_float_format()) (2)
            , strf::conv(", ")                                        (3)
            , strf::fmt(p.y).set_float_format(arg.get_float_format()) (4)
            , (CharT)')' )
            .set_alignment_format(arg.get_alignment_format());        (5)
        return strf::make_printer_input<CharT>(preview, fp, arg2);
    }
};
1 This line defines what are the formatting options applicable to point<FloatT> : alignment as well as floating-point formatting. You can, of course, also create your own formatters.
2 Here we forward the floating-point formatting to p.x.
3 Converting the string ", " to whatever the destination encoding is.
4 Forwarding floating-point formatting to p.y.
5 Applying the alignment formatting.

However, it must be acknowledged that this example is facilitated by the fact that it is possible to convert a point into another value ( a join object ) that the library already knows how to print. If this was not case, we needed to implement a printer class that do things in a more low-level way. This is explained in the documentation. It starts with a simple case, and gradually moves towards more challenging examples.

4.2. Adding destinations

If you want Strf to print to an alternative destination, you need to create a class that derives from strf::basic_outbuff. Having an object of such type, you can print things to it with the strf::to(strf::basic_outbuff<CharT>&) function template.

In the case of {fmt}, you need to have (or create) a type that satisfies the OutputIterator requirements. With that, you can use the fmt::print_to and fmt::print_to_n function templates.

However, in Strf you can go a bit further and create your own destination expression to be used in the basic usage syntax of the library. For example, suppose a codebase uses a string class of its own instead of std::string. Suppose it’s name is xstring: just like there is strf::to_string, it is possible to define a destination expression named to_xstring intended to create xstring objects. If desirable, it is possible to implement it in such a way that the size of the content is always pre-calculated before the the xstring object is constructed, so that the adequate amount of memory can be allocated.

This is all explained in this guide.

5. Error handling policy

Neither {fmt} nor Strf ever change the value of errno.

{fmt} throws an exception when it founds someting wrong at run-time.

Strf does not throw, but it also doesn’t prevent exceptions to propagate from whatever it depends on, like the language runtime or user extensions. So an exception may arise when writing to a std::streambuf or std::string, for example.

Instead of throwing, Strf’s policy is to print the replacement character U'\uFFFD' ( or '?', depending on the charset ) indicating where the error occured. This can happen when parsing the tr-string or in charset conversion or sanitization. Optionally, you can set a callback to be invoked in such errors ( see tr_error_notifier_c and invalid_seq_notifier_c ) which can then throw an exception, if that’s what you want.

In addition to that, depending on the destination, the return type of the basic usage syntax may contain an error indication. For example, when writing to a char*, the returned object contains a bool member truncated that tells whether the destination memory is too small.

6. Numeric punctuation

{fmt}'s and strf’s ways of applying punctuation to integer and floating-point arguments are analogous.

In {fmt}, you pass a std::locale object as an argument, before the format string, then use the 'L' format flag for those arguments that shall conform to the std::numpunct facet installed in that std::locale object.

In strf, you pass a strf::numpunct object to the with function, and apply the punct or operator! format function in those arguments you want to be punctuated.

So the basic difference is that while {fmt} uses std::locale and std::numpunct, Strf has its own facets arquitecture. The rationale and advantages for this are explained in another article.

7. Multilingual support

Strf has some extra advantages over {fmt} when developing an application that needs to provides multilingual support:

Less translations errors

Since the programmer is commonly not same person who translates the messages and messages can be ambiguous, translation mistakes can happen. So, Strf allows you to add comments in the tr-string to prevent misinterpretations.

Less syntax errors

The syntax of tr-string is less error-prone than the {fmt}'s format string. It is true that {fmt} can detect syntax error at compile-time with FMT_STRING or FMT_COMPILE, but it is very difficult ( if not impossible ) to use such macros in multilingual programs, since the format strings are then likely to be evaluated at run-time.

Reusability

In Strf, translation is decoupled from formatting. You can use the same tr-string multiple times with different format options. You can also joins or other "special" input types to reuse a tr-string:

// returns "Failed to connect to server {}" translated to some language
const char* tr_failed_to_connect_to_server_X();

// ...
strf::to(dest).tr(tr_failed_to_connect_to_server_X(), "some_server_name.com");

// Now passing an ip address.
// No need to create a new tr-string "Failed to connect to server {}.{}.{}.{}"
std::uint8_t ip[4];
// ...
strf::to(dest).tr( tr_failed_to_connect_to_server_X()
                 , strf::join(ip[0], '.', ip[1], '.', ip[2], '.', ip[3]) );
         // or   , strf::separated_range(ip, ".");

8. Performance

If you look at the benchmarks, you can see that the performances of Strf and {fmt} depend on several things, like what you are printing, how you do it, what are the formatting options, the compiler, the destination type, etc. There are situations where {fmt} is faster, and others when others where it is Strf.

However it is possible to take some general conclusions:

  • Strf is faster when not using tr-string ( than when it is ).

  • {fmt} is faster when using FMT_COMPILE macro ( than when it isn’t ), except in fmt::format_to_n.

When it comes to writting to char*, {fmt} provides fmt::format_to and fmt::format_to_n. The former is faster, but it does not take into account the size of the destination, which may be necessary in some situations. Strf has strf::to and strf::to_range. They both take into account the destinatination’s size, and their only difference is that strf::to_range does not append the additional termination character ('\0').

The benchmarks results leads to conclude:

  • strf::to is faster than fmt::format_to when macro FMT_COMPILE is not used.

  • strf::to is faster than fmt::format_to_n,

  • When using FMT_COMPILE, fmt::format_to is faster than strf::to when no formatting option is applied.

  • When formatting options are applied, strf::to is faster than fmt::format_to ( with and without FMT_COMPILE ) when not using tr-string

When comparing strf::to_string against fmt::format, we conclude:

  • strf::to_string without tr-string is faster than fmt::format ( with and without FMT_COMPILE )

  • strf::to_string with tr-string is usually faster than fmt::format without FMT_COMPILE.

  • When formatting options are applied, strf::to_string is faster than `fmt::format.

Of course, it’s very possible to be exceptions for the above conclusions, since these benchmarks are far of covering all possible situations.


1. http://eel.is/c++draft/format.string.std#11