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