1. Syntax

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() ).

examples
Compact form Equivalent long form

auto str = strf::to_string (args...);

strf::string_maker dst(size);
strf::to(dst) (args...);
auto str = dst.finish();

auto str = strf::to_string.reserve(size) (args...);

strf::pre_sized_string_maker dst(size);
strf::to(dst) (args...);
auto str = dst.finish();

auto result = strf::to(stdout) (args...);

strf::narrow_cfile_writer<char> dst(stdout);
strf::to(dst) (args...);
auto result = dst.finish();

char buf[200];
auto result = strf::to(buf) (args...);

char buf[200];
strf::cstr_destination dst(buf);
strf::to(dst) (args...);
auto result = dst.finish();

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