The customization point

Strf uses the tag_invoke pattern to define printable types. To make a type named your_type become printable, you overload tag_invoke like this:

namespace strf {

struct your_type_print_traits { /*...*/ }; // a PrintTraits type

your_type_print_traits tag_invoke(strf::print_traits_tag, const your_type&);

}

In most compilers you don’t need to implement this overload. It’s enough to just declare it, since the library just need to know the return type, which can obtained with decltype. Actually, even the declaration is only necessary if you want to match types that derive from or are implicitly convertible to your_type. Otherwise, you can just define the template specialization print_traits<your_type>, since this is the fallback the library uses when there is not matching overload.

Whatever approach you use, print_traits<your_type> or the return type of tag_invoke must satisfy PrintTraits.

Creating a PrintTraits class

According to the documentation, a PrintTraits class must contain a static template function named make_print_input that kind of tells the library how to print the value.

For example, let’s make std::complex printable:

namespace strf {

template <typename FloatT>
struct print_traits<std::complex<FloatT>> {
    using override_tag = std::complex<FloatT>;
    using forwarded_type = std::complex<FloatT>;

    template <typename CharT, typename Preview, typename FPack>
    static auto make_printer_input
        ( strf::tag<CharT> (1)
        , Preview& preview (2)
        , const FPack& fp  (3)
        , std::complex<FloatT> arg )(4)
    {
        auto arg2 =  strf::join
            ( static_cast<CharT>('('), arg.real()
            , static_cast<CharT>(','), arg.imag()
            , static_cast<CharT>(')') );

        return strf::make_printer_input<CharT>(preview, fp, arg2);
    }
};

} // namespace strf
1 This parameter aims to enable this function template to deduce CharT. In case you don’t to implement a generic make_printer_input supporting all character types, you can just use tag<char> ( or whatever character type you want to support ).
2 Preview is expected to be an instance of the print_preview class template ( but you only really need care to about it when creating a printer class )
3 FPack is expected to be an instance of the facets_pack class template. This arguments contains the facet objects.
4 The last argument passed to make_printer_input is the value to be printed.

What above code basically does is to tell the library to handle std::complex values in the same way that it would handle the join object created above ( arg2 ). That’s because the expression:

strf::make_printer_input<CharT>(preview, fp, foo)

is equivalent to:

overrider.make_printer_input(tag<CharT>{}, preview, fp, foo)

, where overrider is a facet object of the print_override_c category. If it is the default facet, then such expression is equivalent to:

foo_print_traits::make_printer_input(tag<CharT>{}, preview, fp, foo)

where foo_print_traits is the return type of tag_invoke(strf::print_traits_tag{}, foo)

make_printer_input function does not actually print the value, it just returns an object that the library knows how to print. Make sure that this returned object does not contain any dangling reference. For example, if the join in the above example contained a reference to any local object, we would get undefined behaviour.

Anyway, in sometimes it is not possible to use this tactic, i.e. to simply convert the value to another type that is already printable. A further section explains how to deal with such cases.

Creating and using facets

In some cases you may want to create a new facet category for the new printable type. For example, suppose we want to turn the following enumeration into a facet that enables the user to select which form the complex numbers shall be printed in:

enum class complex_form { vector, algebric, polar };
Same complex number printed in different forms

complex_form::vector

(3, 4)

complex_form::algebric

(3 + i*4)

complex_form::polar

5∠ 0.9272952180016122

Too turn complex_form into a facet, i.e. to make it satisfy the Facet requirement, we do the following:

struct complex_form_c {
    static constexpr complex_form get_default() noexcept {
        return complex_form::vector;
    }
};

namespace strf {

template <> struct facet_traits<complex_form> {
    using category = complex_form_c;
};

} // namespace strf;

Now let’s reimplement make_printer_input to take into account this new facet:

template <typename FloatT>
std::pair<FloatT, FloatT> complex_coordinates
    ( std::complex<FloatT> x, complex_form form ) noexcept;

namespace strf {

template <typename FloatT>
struct print_traits<std::complex<FloatT>> {
    using override_tag = std::complex<FloatT>;
    using forwarded_type = std::complex<FloatT>;

    template <typename CharT, typename Preview, typename FPack>
    static auto make_printer_input
        ( strf::tag<CharT>
        , Preview& preview
        , const FPack& fp
        , std::complex<FloatT> arg)
    {
        complex_form form = strf::use_facet<complex_form_c, std::complex<FloatT>>(fp);
        auto v = ::complex_coordinates(arg, form);
        unsigned has_brackets = form != complex_form::polar;
        auto arg2 = strf::join
            ( strf::multi(static_cast<CharT>('('), has_brackets)
            , v.first
            , strf::conv(middle_string(form), strf::utf<char16_t>)
            , v.second
            , strf::multi(static_cast<CharT>(')'), has_brackets) );

        return strf::make_printer_input<CharT>(preview, fp, arg2);
    }

private:

    static const char16_t* middle_string(complex_form form)
    {
        switch(form) {
            case complex_form::algebric: return u" + i*";
            case complex_form::polar: return u"\u2220 "; // the angle character ∠
            default: return u", ";
        }
    }
};

} // namespace strf

Its first line gives us the complex_form value:

complex_form form = strf::use_facet<complex_form_c, std::complex<FloatT>>(fp);

use_facet is used to extract a facet object from a facets_pack object. The first template parameter is the facet category. The second is the usually printable type and it only has effect when there is any constrained facets of the given category in the the facets_pack object. The effect is that use_facet only returns the value inside a constrained facet when Filter<Tag>::value is true , where Filter is the template parameter of the constrained facet, and Tag is the second template parameter used in use_facet ( which is std::complex<FloatT> in this case ).

Next, we evaluate the floating-point values to be printed. We can’t just use arg.real() and arg.imag() as before, since that would be incorrect in the polar form. Let’s just assume the correct values are calculated in a function named complex_coordinates whose implementation is not the point here:

std::pair<FloatT,FloatT> v = ::complex_coordinates(arg, form);

If we want to the parenthesis to not be printed in the polar form, we can achieve that using the multi format function. It causes a character to be printed n times, where n in our case is either 0 or 1.

unsigned has_brackets = form != complex_form::polar;
auto arg2 = strf::join
    ( strf::multi(static_cast<CharT>('('), has_brackets)
    /* ... */
    , strf::multi(static_cast<CharT>(')'), has_brackets) );

Note that it is not possible to use instead if-else blocks like this:

if (form != complex_form::polar) {
    auto j1 = strf::join
            ( static_cast<CharT>('(')
            , v.first
            , strf::conv(middle_string(form), strf::utf<char16_t>)
            , v.second
            , static_cast<CharT>(')') );
    return strf::make_printer_input<CharT>(preview, fp, j1);
}
auto j2 = strf::join
    ( v.first
    , strf::conv(middle_string(form), strf::utf<char16_t>)
    , v.second );
return strf::make_printer_input<CharT>(preview, fp, j2); // different return type !

That wouldn’t compile since j1 and j2 have different types.

At last, we need to select a different middle string for each form. No big deal here, we just created a fuction middle_string to handle that. But what may have caught your eye is that the string is passed to the conv function. The code wouldn’t compile without it, unless when CharT is the same as the string’s character type, and even in this case, there is the risk of the destination encoding differing from the one used in the string ( especially if we were using a char string, instead of a char16_t string as we did above ).

auto arg2 = strf::join
    ( /* ... */
    , /* ... */
    , strf::conv(middle_string(form), strf::utf<char16_t>)
    , /* ... */
    , /* ... */ );

Now you are ready to go:

void sample()
{
    auto str = strf::to_string(std::complex<double>(3, 4));
    assert(str == "(3, 4)");

    str = strf::to_string.with(complex_form::algebric) (std::complex<double>(3, 4));
    assert(str == "(3 + i*4)");
}

Adding format functions

Format functions are defined in classes that comply with the Formatter requirements. If you want to add format functions you need to create a formatter class and/or select one or some of those provided by the library. Then, in your PrinterTraits class, you need to define a member formatters as a type alias to tag<Fmts...>, where Fmts... are the Formatter types you want to enable.

There are formatters that make sense for std::complex: the alignment_formatter and the float_formatter. So let’s select them:

namespace strf {

template <typename FloatT>
struct print_traits<std::complex<FloatT>> {
    // …​
    using formatters = strf::tag<alignment_formatter, float_formatter>;
    // …​
};

} // namespace strf

After that, whenever a value x is a std::complex, expressions like +strf::fmt(x) and *strf::sci(x) > 20 and right(x, 20, '_').sci() are all well-formed, and the type of strf::fmt(x) is value_with_formatters<print_traits<std::complex<…​>>, Fmts...>, where Fmts... are the types you used in to define the formatters type alias.

Though well-formed, they are still not printable. To make them printable, we need to overload make_printer_input member function template:

namespace strf {

template <typename FloatT>
struct print_traits<std::complex<FloatT>> {

    // ...

    template <typename CharT, typename Preview, typename FPack>
    static auto make_printer_input
        ( strf::tag<CharT>
        , Preview& preview
        , const FPack& fp
        , std::complex<FloatT> arg)
    {
        // handles value without formatting
        // ( same as before )
    }

    template < typename CharT, typename Preview, typename FPack, typename... T>
    static auto make_printer_input
        ( strf::tag<CharT>
        , Preview& preview
        , const FPack& fp
        , strf::value_with_formatters<T...> arg )
    {
        // handles value with formatting

        auto form = strf::use_facet<complex_form_c, std::complex<FloatT>>(fp);
        auto v = ::complex_coordinates(arg.value(), form);
        unsigned has_brackets = form != complex_form::polar;
        auto arg2 = strf::join
            ( strf::multi(static_cast<CharT>('('), has_brackets)
            , strf::fmt(v.first).set_float_format(arg.get_float_format())
            , strf::conv(middle_string(form), strf::utf<char16_t>)
            , strf::fmt(v.second).set_float_format(arg.get_float_format())
            , strf::multi(static_cast<CharT>(')'), has_brackets) );
        auto arg3 = arg2.set_alignment_format(arg.get_alignment_format());
        return strf::make_printer_input<CharT>(preview, fp, arg3);
    }
};

} // namespace strf

Instead of taking a raw std::complex<Float>, the new overload takes a value_with_formatters<T...> which matches the return type of the format functions. Note that we need to add that template parameter pack because the Formatters types in value_with_formatters may change as some format functions are used. For example:

std::complex<double> x;

auto arg1 = strf::fmt(x);
auto arg2 = strf::fmt(x).sci();
auto arg3 = strf::fmt(x).sci() > 10;

// arg1, arg2 and arg3 have different types:
static_assert(! std::is_same_v(decltype(arg1), decltype(arg2)));
static_assert(! std::is_same_v(decltype(arg2), decltype(arg3)));

We can keep the old make_printer_input ( that takes std::complex without formatting ), but we could also remove it. Because when the expression below is not well-formed:

PrintTraits::make_printer_input(tag<CharT>{}, preview, fp, x)

, and the type of x is not an instance value_with_formatters, then the library invokes the following instead:

PrintTraits::make_printer_input(tag<CharT>{}, preview, fp, strf::fmt(x))

Anyway, let’s examine the new function. You can see there are few changes from the original. The first one is that we need to use value() function to extract the std::complex value:

        auto v = ::complex_coordinates(arg.value(), form);

Second, we re-apply the floating-point format the each floating-point value:

        auto arg2 = strf::join
            ( /* …​ */
            , strf::fmt(v.first).set_float_format(arg.get_float_format())
            , /* …​ */
            , strf::fmt(v.second).set_float_format(arg.get_float_format())
            , /* …​ */ );

Third, we apply the alignment format to the join:

        auto arg3 = arg2.set_alignment_format(arg.get_alignment_format());

Creating format functions

But what if you don’t want just to enable existing format functions to your printable type, but also create new ones ?

In a previous section we created a facet that specifies the complex number form (vector, algebric or polar). Now, let’s suppose we want create format functions for the same purpose.

This means we need to create a Formatter class, which we will name here as std_complex_formatter. It is required to have a member type template named fn where the format functions are defined. The template parameter is used in the return type of the format functions:

struct std_complex_formatter {

    enum class complex_form_fmt {
        vector   = (int)complex_form::vector,
        algebric = (int)complex_form::algebric,
        polar    = (int)complex_form::polar,
        from_facet = 1 + std::max({vector, algebric, polar})
    };

    template <class T>
    class fn
    {
    public:

        fn() = default;

        template <class U>
        constexpr fn(const fn<U>& u) noexcept
            : form_(u.form())
        {
        }
        constexpr T&& vector() && noexcept
        {
            form_ = complex_form_fmt::vector;
            return static_cast<T&&>(*this);
        }
        constexpr T&& algebric() && noexcept
        {
            form_ = complex_form_fmt::algebric;
            return static_cast<T&&>(*this);
        }
        constexpr T&& polar() && noexcept
        {
            form_ = complex_form_fmt::polar;
            return static_cast<T&&>(*this);
        }
        constexpr complex_form form(complex_form f) const
        {
            return form_ == complex_form_fmt::from_facet ? f : static_cast<complex_form>(form_);
        }
        constexpr complex_form_fmt form() const
        {
            return form_;
        }

    private:

        complex_form_fmt form_ = complex_form_fmt::from_facet;
    };
};

vector(), algebric() and polar() are the format functions we are creating. std_complex_formatter is designed to work in conjuction with the complex_form facet that we defined previously. So if none of its format function is called, the form defined by the facet object is taken.

The static_cast expressions above work because fn<T> is supposed to be a base class of T ( yes, it’s the CRTP ). Not only that, T is expected to be an instance of value_with_formatters that has std_complex_formatter as one of its template arguments.

In our PrintTraits class, there are only two small modifications: formatters and the first line of make_printer_input:

namespace strf {

template <typename FloatT>
struct print_traits<std::complex<FloatT>> {

    // ...

    using formatters = strf::tag
        < std_complex_formatter
        , strf::alignment_formatter
        , strf::float_formatter >;

    template <typename CharT, typename Preview, typename FPack>
    static auto make_printer_input
        ( strf::tag<CharT>
        , Preview& preview
        , const FPack& fp
        , std::complex<FloatT> arg)
    {
        // same as before
        //...
    }

    template < typename CharT, typename Preview, typename FPack, typename... T>
    static auto make_printer_input
        ( strf::tag<CharT>
        , Preview& preview
        , const FPack& fp
        , strf::value_with_formatters<T...> arg )
    {
        auto form = arg.form(strf::use_facet<complex_form_c, std::complex<FloatT>>(fp));

        // same as before
        //...
    }
};

} // namespace strf

Now, we a are ready to play:

void sample()
{
    std::complex<double> x{3, 4};

    auto str = strf::to_u16string .with(complex_form::algebric)
        ( x, u" == ", strf::sci(x).p(5).polar() );

    assert(str == u"(3 + i*4) == 5.00000e+00∠ 9.27295e-01");
}

However, you may find that std_complex_formatter::fn is incomplete because we only create format functions that are non-const and use the && ref-qualifier. Shouldn’t we overload them for the other cases as well ? They would be necessary in situation like this:

const auto fmt1 = strf::fmt(std::complex<double>{3, 4});
auto fmt2 = fmt1.polar(); // error: no polar() for const lvalue
fmt2.algebric();          // error: no algebric() for non-const lvalue

So, for the sake of completeness, below goes polar() overloaded for both rvalues and lvalues:

struct std_complex_formatter {
    // ...

    template <class T>
    class fn
    {
    public:

        // ...
        constexpr explicit fn(complex_form_fmt f) noexcept
            : form_(f)
        {
        }

        constexpr T&& polar() && noexcept
        {
            // ( same as before )
        }
        constexpr T& polar() & noexcept
        {
            form_ = complex_form_fmt::polar;
            return static_cast<T&>(*this);
        }
        constexpr T polar() const & noexcept
        {
            return T{ static_cast<const T&>(*this)
                    , strf::tag<std_complex_formatter> {}
                    , complex_form_fmt::polar };
        }
        // ( vector and algebric are analogous )

        // ...
    };
};

Since the const version of polar() can’t modify the current object, it instead returns a new one where each base class subobject is initialized with (copied from) the correponding base class subobject of this object, except the std_complex_formatter::fn<T> one, which is initialized instead with complex_form_fmt::polar. This is why we also need to add that constructor that has a complex_form_fmt parameter. The value_with_formtters constructor used above is documented here.

And its done! I think is a pretty complete example of how to make std::complex printable. You can see the complete implementation here.

The low-level way

Sometimes, when creating a PrinterTraits class, it is not possible possible to make its make_printer_input member function just return strf::make_printer_input(…​) as we did in the previous sections.

So let’s see another approach to make std::complex printable — the low-level way. First, let’s redefine print_traits<std::complex<…​>>:

namespace strf {
template <typename FloatT>
struct print_traits<std::complex<FloatT>>
{
    using override_tag = std::complex<FloatT>;
    using forwarded_type = std::complex<FloatT>;
    using formatters = strf::tag< …​ /*same as before*/>;

    // make_print_input that handles unformatted values
    template <typename CharT, typename Preview, typename FPack>
    static auto make_printer_input
        ( strf::tag<CharT>
        , Preview& preview
        , const FPack& fp
        , std::complex<FloatT> arg )
        -> strf::usual_printer_input
            < CharT, Preview, FPack, std::complex<FloatT>
            , std_complex_printer<CharT, FloatT> >
    {
        return {preview, fp, arg};
    }

    …​ /* omitting the make_print_input overload that handles formatted values */ …​
};
} // namespace strf

The return type of make_printer_input must aways be a PrinterInput type, and the usual_printer_input class template is syntatic sugar to achieve that. Most of the work lies in creating the class — or, more likely, the class template — used in its fifth template parameter, named here as std_complex_printer. It must be a concrete class that derives from printer<CharT>, or that is convertible to const printer<CharT>&. It must also be constructible from the return type of our make_printer_input member function:

template <typename CharT, typename FloatT>
class std_complex_printer: public strf::printer<CharT> {
public:

    template <typename... T>
    explicit std_complex_printer(strf::usual_printer_input<T...>);

    void print_to(strf::basic_outbuff<CharT>& dest) const override;

private:

    template <typename Preview, typename WCalc>
    void preview_(Preview& preview, const WCalc& wcalc) const;

    strf::dynamic_charset<CharT> charset_;
    strf::numpunct<10> numpunct_;
    strf::lettercase lettercase_;
    complex_form form_;
    std::pair<FloatT, FloatT> coordinates_;

    static constexpr char32_t anglechar_ = 0x2220;
};

The print_to member function is responsible for writing the content:

template <typename CharT, typename FloatT>
void std_complex_printer<CharT, FloatT>::print_to(strf::basic_outbuff<CharT>& dest) const
{
    auto print = strf::to(dest).with(lettercase_, numpunct_, charset_);
    if (form_ == complex_form::polar) {
        print(coordinates_.first, U'\u2220', static_cast<CharT>(' ') );
        print(coordinates_.second );
    } else {
        print((CharT)'(', coordinates_.first);
        print(strf::conv(form_ == complex_form::algebric ? " + i*" : ", ") );
        print(coordinates_.second, (CharT)')');
    }
}

Now let’s look the constructor:

template <typename CharT, typename FloatT>
template <typename... T>
inline std_complex_printer<CharT, FloatT>::std_complex_printer
    ( strf::usual_printer_input<T...> x )
    : charset_(strf::use_facet<strf::charset_c<CharT>, void>(x.facets))
    , numpunct_(strf::use_facet<strf::numpunct_c<10>, FloatT>(x.facets))
    , lettercase_(strf::use_facet<strf::lettercase_c, FloatT>(x.facets))
    , form_(strf::use_facet<complex_form_c, std::complex<FloatT>>(x.facets))
    , coordinates_(::complex_coordinates(form_, x.arg))
{
    auto wcalc = strf::use_facet< strf::width_calculator_c
                                , std::complex<FloatT> >(x.facets);
    preview_(x.preview, wcalc);
}

The member variables charset_, numpunct_ and lettercase_ are facet objects. The reason why I did not instead just store a copy of x.facets as member variable is because its type would need to be another template parameter, one that would change often — every time the facets are different — thus causing some code bloat.

Usually the second template argument in use_facet is the input type, which here is std::comple<FloatT>. However, I decided that it makes more sense to use FloatT for the numeric punctuation and letter case. There is no strict rule for that.

The type you choose to extract the charset facet object makes no difference since charset facets are not constrainable.

Now, in addition to initialize the object, the constructor must do another thing. usual_printer_input contains a print_preview reference, named preview. When the first template argument of this print_preview is preview_size::yes, then we must inform the size of the content that print_to writes. Actually, let me rephare that, because it’s a little bit tricky: our constructor must inform a size s that ensures that print_to does not call dest.recycle() if dest.space() >= s, where dest is the argument passed to print_to.

However, you only need to be that cautious when your print_to function directly calls dest.recycle(), which is only the case when you write things directly to dest.pointer(). If you need to go that low-level, you may want to read this document to understand how the class template basic_outbuff works.

Now, if this is too complicated, you can just instead define your constructor with this:

using preview_type = typename strf::usual_printer_input<T...>::preview_type;
static_assert(! preview_type::size_required);

That would prevent your printable type to work when the feature reserve_calc is used, which may not be a problem in many cases.

When the second template argument of this print_preview is preview_width::yes, then we must inform x.preview the width. This happens only when your printable type is used it in an aligned join. So if you don’t care about supporting that situation, you can just do:

using preview_type = typename strf::usual_printer_input<T…​>::preview_type;
static_assert(! preview_type::width_required);

// or, if you don’t want to preview the size either:
static_assert(preview_type::nothing_required);

Fortunately, in our case, previewing the size and width is not that difficult. For the floating point values, we can use the global function template preview. The rest of the content we can easily calculate manually:

template <typename CharT, typename FloatT>
template <typename Preview, typename WidthCalc>
void std_complex_printer<CharT, FloatT>::preview_(Preview& pp, const WidthCalc& wcalc) const
{
    // preview the size and/or width of the floating-point values:
    auto facets = strf::pack(lettercase_, numpunct_, charset_);
    strf::preview<CharT>(pp, facets, coordinates_.first, coordinates_.second);

    // preview the other characters:
    switch (form_) {
        case complex_form::algebric:
            pp.subtract_width(7);
            pp.add_size(7);
            break;

        case complex_form::vector:
            pp.subtract_width(4);
            pp.add_size(4);
            break;

        default:
            assert(form_ == complex_form::polar);
            if (pp.remaining_width() > 0) {
                pp.subtract_width(wcalc.char_width(strf::utf32<char32_t>, anglechar_));
                pp.subtract_width(1);
            }
            pp.add_size(charset_.encoded_char_size(anglechar_));
            pp.add_size(1);
    }
}

To calculate the size of the angle character, that is used in the polar form, we need to use the charset facet object. To calculate its width, we use the width_calculator_c facet category. And the width of ASCII characters is always assumed to be equal to 1 in Strf.

You can see the use of add_size and subtract_width functions. When calculating the width is potentially expensive, it may worth to check the return of remaining_width — if its not greater than zero, there is no further need to call subract_width.

With this, are ready with our std_complex_printer class template. But, of course, we are not done yet, since it only handles std::complex values without formatting. We need to create another printer — you could name fmt_std_complex_printer — to print formatted values, which is naturally a little bit more complex. However, it’s basically the same idea: print_to prints the content and the constructor previews it. Here is a full implementation: examples/std_complex_printer.cpp.