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
orFMT_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 infmt::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 thanfmt::format_to
when macroFMT_COMPILE
is not used. -
strf::to
is faster thanfmt::format_to_n
, -
When using
FMT_COMPILE
,fmt::format_to
is faster thanstrf::to
when no formatting option is applied. -
When formatting options are applied,
strf::to
is faster thanfmt::format_to
( with and withoutFMT_COMPILE
) when not using tr-string
When comparing strf::to_string
against fmt::format
,
we conclude:
-
strf::to_string
without tr-string is faster thanfmt::format
( with and withoutFMT_COMPILE
) -
strf::to_string
with tr-string is usually faster thanfmt::format
withoutFMT_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.