1. Syntax
The target expression is a customization point and it determinates
where the content goes to as well as the return type of the whole above expression.
The library provides many options to be used
as the target, and you can
create your own.
However, for convenience, most code samples in this tutorial use to_string
:
#include <strf/to_string.hpp>
void sample() {
int x = 200;
std::string str = strf::to_string(x, " in hexadecimal is ", strf::hex(x));
assert(str == "200 in hexadecimal is c8");
}
You can see that there is no format string here, as there is in printf
.
Instead, format functions ( like hex
above ) specify formatting.
The expression strf::hex(x)
is equivalent to strf::fmt(x).hex()
.
The return of strf::fmt(x)
is an object containing the value of x
in addition to
format information which can be edited with member ( format ) functions
following the
named parameter idiom
, like this, for example: strf::fmt(255).hex().p(4).fill(U'.') > 10
To use a translation tools like
gettext,
you need to use the tr
function,
which employs what is called here as the tr-string:
auto s = strf::to_string.tr("{} in hexadecimal is {}", x, strf::hex(x));
The reserve
, no_reseve
and reserve_calc
functions are only available for some
targets, to_string
being one of them.
Using reserve(space)
causes the destination to reserve enough space
to store space
characters. reserve_calc()
has the same effect,
except that it calculates the number of characters for you.
2. Facets
The with
function receives facet objects, which can also be passed
together with ( but always before of ) the arguments to be printed.
Facets complement format functions, in the sense that they also change how things are stringified.
Note, we are not talking here about the facets used in std::ostream
.
In Strf, facets have a different design ( more based on static rather
than dynamic polymorphism ),
and are not necessarily related to localization.
An example of a facet is the lettercase
enumeration:
namespace strf {
enum class lettercase { /* ... */ };
constexpr lettercase lowercase = /* ... */;
constexpr lettercase mixedcase = /* ... */;
constexpr lettercase uppercase = /* ... */;
}
It affects numeric and boolean values:
auto str_uppercase = strf::to_string.with(strf::uppercase)
( true, ' ', *strf::hex(0xab), ' ', 1.0e+50 );
auto str_mixedcase = strf::to_string.with(strf::mixedcase)
( true, ' ', *strf::hex(0xab), ' ', 1.0e+50 );
assert(str_uppercase == "TRUE 0XAB 1E+50");
assert(str_mixedcase == "True 0xAB 1e+50");
2.1. The conceptualization of facets
In this library, the term facet always refers to types. Hence, for example,
the type strf::lettercase
is a facet, whereas strf::uppercase
is a facet value
( or you could call it a facet object, which make more sense when the facet is a class or struct ).
In addition, a facet is always associated to one ( and only one ) facet category,
and several facets can belong to the same category.
For each facet category there is class or struct that defines it.
By convention, its name has the “_c” suffix.
Also, it has a public static member function named get_default()
that
returns the default facet value of the category.
For example, the category of strf::lettercase
is
strf::lettercase_c
,
and strf::lettercase_c::get_default()
returns strf::lowercase
.
The facet_traits
struct template provides way to programmatically obtain the category
a given facet.
Informaly ( perhaps in future it will be formal thanks to C++20 Concepts ),
for each facet category there is a list of requirements a type
must satisfy to be a facet of the category. In the case of strf::lettercase_c
,
the requirement is, well, simply to be the strf::lettercase
type.
( but others facet categories can be more elaborated ).
The design of facets varies a lot according to their categories.
But all facets currently available in the library are small
( in terms of sizeof
) and have fast copy constructors.
In addition, most of them can be instantiated as constexpr
values.
Since Strf is designed for extensibility, if you ever decide to add a new printable type, you can also create new facet categories for it, as well as making it (or not) affected by the some of the existing ones.
You can see a list of the current facets categories here.
2.2. Constrained facets
You can constrain a facet object to a specific group of input types:
auto str = strf::to_string
.with(strf::constrain<std::is_floating_point>(strf::uppercase))
( true, ' '*strf::hex(0xab), ' ', 1.0e+50 );
assert(str == "true 0xab 1E+50");
, or to a group of arguments:
auto str = strf::to_string
( true, ' ', 1.0e+50, " / "
, strf::with(strf::uppercase) (true, ' ', 1.0e+50, " / ")
, true, ' ', 1.0e+50 );
assert(str == "true 1e+50 / TRUE 1E+50 / true 1e+50 );
When there are multiple facets objects of the same category, the order matters. The later one wins:
auto fa = strf::mixedcase;
auto fb = strf::constrain<std::is_floating_point>(strf::uppercase);
using namespace strf;
auto str_ab = to_string .with(fa, fb) (true, ' ', *hex(0xab), ' ', 1e+9);
auto str_ba = to_string .with(fb, fa) (true, ' ', *hex(0xab), ' ', 1e+9);
// In str_ab, fb overrides fa, but only for floating points
// In str_ba, ba overrides fb for all types, so fb has no effect.
assert(str_ab == "True 0xAB 1E+9");
assert(str_ba == "True 0xAB 1e+9");
You can see that the template argument passed to constrain
is a
UnaryTypeTrait, i.e., a type template with a static
constexpr boolean member variable named value
. The type the library
passes to this UnaryTypeTrait is called the
representative type of the printable type, which is usually
the same as the printable type itself, but not always.
For example, in the case of strings, it is
strf::string_input_tag<char_type>
.
The representative of each printable type is documented
in the API reference, in the section
"List of printable types",
but it can also be obtained programmatically, with
representative_of_printable
type alias template.
2.3. Facets packs
To avoid retyping all the facets objects that you commonly use,
you can store them into a facets_pack
,
which you can create with the pack
function template:
constexpr auto my_facets = strf::pack
( strf::mixedcase
, strf::constrain<strf::is_bool>(strf::uppercase)
, strf::numpunct<10>{3}.thousands_sep(U'.').decimal_point(U',')
, strf::numpunct<16>{4}.thousands_sep(U'\'')
, strf::windows_1252<char> );
auto str1 = strf::to_string.with(my_facets) (/* ... */);
// ...
auto str2 = strf::to_string.with(my_facets) (/* ... */);
// ...
The facets_pack
class template is designed more similarly to std::tuple
than to std::locale
.
It stores all the facets objects by value, and accessing one them (
with the strf::get_facet
function template ) is just as fast as
calling a trivial getter function.
Any value that can be passed to the with
function, can also be passed to pack
,
and vice-versa. This means a facets_pack
can contain another facets_pack
.
So the expression:
target.with(f1, f2, f3, f4, f5) (/* args... */);
is equivalent to
target.with(strf::pack(f1, strf::pack(f2, f3), f4), f5) (/* args... */);
, which, by the way, is also equivalent to:
target.with(f1).with(f2).with(f3).with(f4).with(f5) (/* args... */);
3. Locales
Strf is a locale-independent library. When you don’t specify any facet
object, everything is printed as in the "C" locale.
However, the header <strf/locale.hpp>
provides the function locale_numpunct
that returns a numpunct<10>
object that reflects the numeric punctuation of
the current locale ( decimal point, thousands separator and digits grouping ).
locale_numpunct()
is not thread safe — it should not be called while another
thread is modifing the gloabl locale — but once the returned value
is stored into a numpunct<10>
object, that object is not affected anymore when
the locale changes.
#include <strf/locale.hpp>
#include <strf/to_string.hpp>
void sample() {
if (setlocale(LC_NUMERIC, "de_DE")) {
const auto punct_de = strf::locale_numpunct();
auto str = strf::to_string.with(punct_de) (*strf::fixed(10000.5))
assert(str == "10.000,5");
// Changing locale does not affect punct_de
setlocale(LC_NUMERIC, "C");
auto str2 = strf::to_string.with(punct_de) (*strf::fixed(20000.5));
assert(str2 == "20.000,5");
}
}
Strf does not use std::numpunct
for reasons explained in
another document.
4. Other destinations
Up to here, we only covered things that influence the
content to be printed, not where it is printed.
The quick_reference provides a
list of target expressions
that can be used instead to to_string
.
However, you can also use the classes that derive
from the destination
abstract class template
(listed here).
Each of the target expressions relies in one of them
internally, and they can be used directly instead of
the target expression. This approach is more verbose,
but it has some advantages.
The table below show some examples. Note a pattern there:
all these classes have a finish
member function that returns
the same as the compact expressions used on the left column.
It is important to call finish
even if you don’t need
the returned value, because it flushes the content remained in
the internal buffer ( though you can also call flush()
).
Compact form | Equivalent long form |
---|---|
|
|
|
|
|
|
|
|
The first advantage of the longer form is that you don’t have to pass all the arguments in a single statement. Thus, some of the statements may be inside conditionals or loops, and some of them may use different facets than others:
strf::string_maker dst;
std::to(dst) (arg1, arg2);
if (/* some condition */) {
std::to(dst).with(f1, f2) (arg3, arg4);
}
while (/* ... */) {
// ...
std::to(dst).with(f3, f4) (arg5, arg5);
}
std::string result = dst.finish();
The second reason is naturally to
separate concerns:
you can have one piece of code concerned only
in what is printed, like a functions
that writes to a strf::destination<char>&
:
void get_message(strf::destination<char>& dst)
{
strf::to(dst) ("Hello");
// write stuffs to dst ...
}
, while another part of the code (that instantiates the destination object) decides where the content goes.
I know, there is actually nothing really innovative in that design — it’s it’s just plain OO,
and it’s how peolple already basically do with std::basic_ostream
.
So, you may question: if Strf can write to std::basic_ostream
as well
( which it can ), why not just keep using std::basic_ostream
instead of start using strf::destination
?
I would say the main reason is that strf::destination
is more suitable
to be specialized. As a result, it has more specializations — like one that
write to char*
, which you don’t have for std::ostream
. You can also easily
create your own, as explained in another tutorial.
In case you use {fmt} or std::format
, the header
<strf/iterator.hpp>
defines an output iterator adapter
so that you can also write to strf::destination
with fmt::format_to
( or std::format_to
).