A non-exhaustive comparison between Strf 0.15.3 and {fmt} 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. 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.

4. 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, ".");

5. Width calculation

When alignment formatting is applied over a input string, the formatting library needs to estimate how wide that string is to determine how many fill characters it shall be print.

In old formatting libraries like printf such witdh is simply assumed to be equal to the string’s size. This is certainly not accurate if the string is enconded in UTF-8 or UTF-16, were multiple code units can represent a single codepoint and multiple codepoints can represent a single grapheme cluster. In addition, some codepoints are expected to have the double of the regular width, while some others are actually expected to be narrower.

The C++ Standard mandates std::format to take the width of each grapheme cluster as the width of its leading codepoint, which is 1 or 2 according to whether is within certain ranges.[1]. In Strf, this behaviour is implemented in the strf::std_width_calc, which is the default width calculation facet.

However there is obviouly a performance price for more accuracy. And that’s the advatange of Strf: width calculation is customizable. You can choose a less accurate but faster algorithm if you want. Or, you can try to implement one which is actually more accurate, or tailored to the environment the string is printed, i.e. that takes into account the language, the font, etc.

6. Extensibility

6.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.

6.2. Adding destinations

Both libraries support the usual destinations: FILE*, std::ostream&, std::string, and char*. In case you need to send the content to somewhere else, {fmt} provides the generic function fmt::format_to that writes to output iterators. This way, all you have to do is to create an adapter that conforms to the OutputIterator requirements and that writes to your desired target.

In Strf, what you do instead is to create a concrete class that derives from the strf::destination abstract class template. Having an object of such type, you can print things to it with the strf::to(strf::destination<CharT>&) function template.

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.

7. Polymorphic destination

But you are not only suppose to use strf::destination when you want to extend Strf, but also any time you want to write a code that creates a textual content ( or even a binary content ) without concerning about where the such content goes.

void write_some_text(strf::destination<char>& dest, /* ...args... */);

In that sense, strf::destination is similar to std::basic_ostream, but with several advantages.

  • It has better performance.

  • It’s easier to derive from.

  • It can be used in a freestanding environment

  • Among its specializations, you can find one ( actually two ) that can safely and efficiently write to char* ( which, by the way, it what the library uses internally ).

As far as I know, {fmt} doesn’t have anyting equivalent to strf::destination. You could, of course, write instead a generic code terms of output iterators using fmt::format_to or fmt::format_to_n. But that would force things to be template, which is not always possible or can lead to code-bloat

8. 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.


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