The customization point

For every printable type, there must be a class or struct satisfying the PrintableDef type requirements.

It can be defined as a specialization of the printable_def struct template, which is enough to make the library associate it to your printable type:

template <>
struct strf::printable_def<your_type>
{ /* ... */ };

However, if you also want to match the types that derive from or are implicitly convertible to your_type, then you need overload the get_printable_def function like this:

namespace strf {

strf::printable_def<your_type> get_printable_def(strf::printable_tag, const your_type&);

}

In all modern compilers, it’s enough to just declate this overload without any implementation, since the library actually only cares about the return type, which it obtains with decltype.

How to implement a PrintableDef type

Here is an example for how to make std::complex printable:

namespace strf {

template <typename FloatT>
struct printable_def<std::complex<FloatT>> {
    using representative = std::complex<FloatT>;   (1)
    using forwarded_type = std::complex<FloatT>;   (2)

    template <typename CharT, typename Pre, typename FPack>
    static auto make_printer
        ( strf::tag<CharT>           (3)
        , Pre* pre                   (4)
        , const FPack& facets        (5)
        , std::complex<FloatT> arg ) (6)
    {
        auto arg2 =  strf::join
            ( (CharT)'(', arg.real()
            , (CharT)',', (CharT)' '
            , arg.imag(), (CharT)')' );

        return strf::make_printer<CharT>(pre, facets, arg2);
    }
};

} // namespace strf
1 This is the type that is tested by the UnaryTypeTraits that parameterize constrained facets. It is usually the printable type itself. When not defined, it is assumed to the same as template paramenter of printable_def.
2 This defines the type the library internally uses to forward the value to be printed. It must be implicitly convertible to representative. This is usually the same as representative or std::reference_wrapper<const representative>. I recomend not to define it as a reference type — use std::reference_wrapper instead. That’s because it is used as the type of member variables in some classes ( like value_and_format and others ), which would cause a violation of a certain C++ Core Guidelines, which in turn would cause clang-tidy and perhaps others analizes to emit warnings.
3 This parameter exists just to make things a little bit easier in case you don’t want to support all characters types, or if you want a different implementation for every supporterd character type. So, instead of having a CharT template parameter and using std::enable_if ( which you can still do if you want ), you could just create an overload of make_printer for each character type.
4 Pre will always be an instance of the premeasurements class template. pre will never be null.
5 FPack will always be an instance of the facets_pack class template. facets contains the facet objects.
6 The last parameter is the value to be printed.

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

strf::make_printer<CharT>(pre, facets, foo)

is equivalent to

foo_type_def::make_printer(tag<CharT>{}, pre, facets, foo)

, where foo_type_def would be the PrintableDef types corresponding to foo

( Actually, strf::make_printer may call something else if there is a overriding facet in facets. But you don’t have to thing about this now. )

Pay attention to not create dangling references. This would happen, for instance, if the join of the snippet above contained a pointer to any local variable. Note that make_printer function does not actually print the value, it just creates and returns an object that is further used to actually print the value.

And the result is:

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

Supporting format functions

Format functions are defined by FormatSpecifier classes, which are selected in the definition of the format_specifiers member type alias.

alignment_format_specifier and float_format_specifier are two FormatSpecifiers that make sense for std::complex, so we let’s select them as an example.

namespace strf {

template <typename FloatT>
struct printable_def<std::complex<FloatT>> {
    // …​
    using format_specifiers = strf::tag<alignment_format_specifier, float_format_specifier>;
    // …​
};

} // namespace strf

After that, given x, a value of type std::complex<…​>, the expression strf::fmt(x) is well defined, and its type is

value_and_format
    < printable_def<std::complex<…​>>,
    , alignment_format_specifier,
    , float_format_specifier >

Also, expressions like +strf::fmt(x) and *strf::sci(x) > 20 and right(x, 20, '_').sci() are all well-formed now. However, trying to print any of them fails to compile. That’s because we need to overload to overload make_printer to handle the new values.

namespace strf {

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

    // ...

    template <typename CharT, typename Pre, typename FPack>
    static auto make_printer
        ( strf::tag<CharT>
        , Pre* pre
        , const FPack& facets
        , std::complex<FloatT> arg)
    {
        // handles value without formatting
        // ( same as before )
    }

    template < typename CharT, typename Pre, typename FPack, typename... T>
    static auto make_printer
        ( strf::tag<CharT>
        , Pre* pre
        , const FPack& facets
        , strf::value_and_format<printable_def, T...> arg )
    {
        // handles value with formatting

        auto v = arg.value()
        auto arg2 = strf::join
            ( strf::multi(static_cast<CharT>('('), has_brackets)
            , strf::fmt(v.real()).set_float_format(arg.get_float_format())
            , (CharT)',', (CharT)' '
            , strf::fmt(v.imag()).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<CharT>(pre, facets, arg3);
    }
};

} // namespace strf

You can see that the new function has some are few differences from the original. The first one is that we use the value() function to extract the std::complex value:

        auto v = arg.value();

Second, we re-apply the floating-point format to the each ot the floating-point values:

        auto arg2 = strf::join
            ( /* …​ */
            , strf::fmt(v.real()).set_float_format(arg.get_float_format())
            , /* …​ */
            , strf::fmt(v.imag()).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());

The type list T... in value_and_format<printable_def, T...> does not necessarily exactly match the list of FormatSpecifiers used to define format_specifiers. That’s because some format functions may replace some of them:

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

For instance, in the above snippet, the operator> replaces alignment_format_specifier ( which is an alias to alignment_format_specifier_q<false> ) by alignment_format_specifier_q<true>.

Creating format functions

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

Now we we will create a new FormatSpecifier class, and name it std_complex_format_specifier.

A FormatSpecifier 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:

enum class complex_form { vector, algebric, polar };

struct std_complex_format_specifier {

    template <class T>
    class fn
    {
    public:

        fn() = default;

        template <class U>
        constexpr fn(const fn<U>& u) noexcept
            : form_(u.form())
        {
        }

        // format functions

        constexpr T&& vector() && noexcept
        {
            form_ = complex_form::vector;
            return static_cast<T&&>(*this);
        }
        constexpr T&& algebric() && noexcept
        {
            form_ = complex_form::algebric;
            return static_cast<T&&>(*this);
        }
        constexpr T&& polar() && noexcept
        {
            form_ = complex_form::polar;
            return static_cast<T&&>(*this);
        }

        // observer

        constexpr complex_form get_complex_form() const
        {
            return form_;
        }

    private:

        complex_form form_ = complex_form::from_facet;
    };
};

vector(), algebric() and polar() are the format functions we are creating.

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

Now let’s suppose we want the complex_form value to have the following effect on how the numbers are printed:

complex_form::vector

(3, 4)

complex_form::algebric

(3 + i*4)

complex_form::polar

5∠ 0.9272952180016122

In our PrintableDef class, there are only two modifications: First, the format_specifiers definition:

template <typename FloatT>
struct printable_def<std::complex<FloatT>> {
    // ...
    using format_specifiers = strf::tag
        < std_complex_format_specifier
        , strf::alignment_format_specifier
        , strf::float_format_specifier >;
    // ...

And second, the make_printer that has the value_and_format param:

namespace strf {

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

    // ...

    template < typename CharT, typename Pre, typename FPack, typename... T>
    static auto make_printer
        ( strf::tag<CharT>
        , Pre* pre
        , const FPack& facets
        , strf::value_and_format<T...> arg )
    {
        auto form = arg.get_complex_form();
        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)                (1)
             , strf::fmt(v.first).set_float_format(arg.get_float_format())
             , strf::unsafe_transcode(middle_string(form), strf::utf<char16_t>)  (2)
             , 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<CharT>(pre, facets, arg3);
    }

    private:  // ( some auxiliar functions )

    static std::pair<FloatT, FloatT> complex_coordinates
        ( std::complex<FloatT> x, complex_form form );

    static const char16_t* middle_string(complex_form form);
};

} // namespace strf
1 If we want 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.
2 The code wouldn’t compile without using unsafe_transcode or transcode, 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.
void sample()
{
    std::complex<double> x{3, 4};

    auto str = strf::to_u16string (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_format_specifier::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_format_specifier {
    // ...

    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_format_specifier> {}
                    , 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_format_specifier::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_and_format 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.

Creating a facet

Instead of creating the previous format functions, we could just turn the enumeration complex_form into a facet. This would just require some few lines:

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

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

Then, instead of calling get_complex_form(), make_printer would start with the line:

complex_form form = strf::get_facet<complex_form_c, std::complex<FloatT>>(facets);

get_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 get_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 get_facet ( which is std::complex<FloatT> in this case ).

This way, the complex form would be specified by passing complex_form value as a facet object, instead of calling a format funcion:

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

print instead of make_printer

Sometimes it is not possible to implement make_printer by simply returning the call of strf::make_printer on some other printable type as the we did before.

In this case the esiest solution is to instead define a print function instead of make_printer

template <typename FloatT>
struct strf::printable_def<std::complex<FloatT>> {
    using representative = std::complex<FloatT>;
    using forwarded_type = std::complex<FloatT>;

    template <typename CharT, typename FPack>
    static void print
        ( strf::destination<CharT>& dst
        , const FPack& facets
        , std::complex<FloatT> arg)
    {
        to(dst) .with(facets)
            ( (CharT)'(', arg.real()
            , strf::unsafe_transcode(", ")
            , arg.imag(), (CharT)')');
    }
    // \...
};

However, this approach has disadvantanges:

  • The printable type can not be used inside aligned joins. The following fails to compile:

    std::complex<double> x;
    strf::to(__some_dest__) ( strf::join_center(20) ("x=", x) );
  • reserve_calc also is not supported. The following fails to compile:

    std::complex<double> x;
    strf::to_string.reserve_calc()  (x);

Lower-level make_printer

Now we will reimplement printable_def<std::complex<…​>> with make_printer, but without taking advantage of the fact the it can just return strf::make_printer<CharT>(pre, facets, some_join).

The first thing to know is that make_printer has two responsibilities:

  • To calculate the size and/or with of the content to be printed.

  • To return a callable object that prints the content.

An approach which is simple, but may have bad performance, is the following:

    template <typename CharT, typename Pre, typename FPack>
    static auto make_printer
        ( strf::tag<CharT>
        , Pre* pre
        , const FPack& facets
        , std::complex<FloatT> arg )
    {
        // NOT IDEAL

        strf::measure
            ( pre, facets
            , (CharT)'(', arg.real(), (CharT)',', (CharT)' '
            , arg.imag(), (CharT)')' );

        return [=](strf::destination<CharT>) {
            to(dst) .with(facets)
                ( (CharT)'(', arg.real(), (CharT)',', (CharT)' '
                , arg.imag(), (CharT)')';
        }
    }

The reason to dislike the above solution, is that in order to measure the printing of a floating-point value , under the hood, it’s necessary to calculate the mantissa and exponent in the decimal base, which is not computationally cheap, and in order to print it, it is necessary to do this again. So we are wasting CPU cycles by doing the same thing twice.

This is how we could avoid this:

    template <typename CharT, typename PreMeasurements, typename FPack>
    static auto make_printer
        ( strf::tag<CharT>
        , PreMeasurements* pre
        , const FPack& facets
        , std::complex<FloatT> arg)
    {
        pre→add_width(4);
        pre→add_size(4);

        const auto write_real_coord = strf::make_printer<CharT>(pre, facets, arg.real());
        const auto write_imag_coord = strf::make_printer<CharT>(pre, facets, arg.imag());

        return [write_real_coord, write_imag_coord] (strf::destination<CharT>& dst)
               {
                   strf::to(dst) ((CharT)'(');
                   write_real_coord(dst);
                   strf::to(dst) ((CharT)',', (CharT)' ');
                   write_imag_coord(dst);
                   strf::to(dst) ((CharT)')');
               };
    }

Now, to support the format specifiers, I will add two overloads of make_printer — one that supports alignment formatting, and one that doesn’t.

First, the one that doesn’t:

    template < typename CharT, typename PreMeasurements, typename FPack
             , typename PrintableDef, typename FloatFmt >
    static auto make_printer
        ( strf::tag<CharT>
        , PreMeasurements* pre
        , const FPack& facets
        , const strf::value_and_format
            < PrintableDef
            , std_complex_format_specifier
            , FloatFmt
            , strf::alignment_format_specifier_q<false> >& arg ) (1)
    {
        const auto form = arg.get_complex_form();
        measure_without_coordinates<CharT>(pre, facets, form);

        const auto coordinates = complex_coordinates(form, arg.value());
        const auto float_fmt = arg.get_float_format();
        const auto coord1 = strf::fmt(coordinates.first).set_float_format(float_fmt);
        const auto coord2 = strf::fmt(coordinates.second).set_float_format(float_fmt);
        const auto write_coord1 = strf::make_printer<CharT>(pre, facets, coord1);
        const auto write_coord2 = strf::make_printer<CharT>(pre, facets, coord2);
        const auto charset = strf::get_facet<strf::charset_c<CharT>, representative>(facets);

        return [charset, form, write_coord1, write_coord2] (strf::destination<CharT>& dst)
            {
                constexpr char32_t anglechar = 0x2220;

                switch (form) {
                case complex_form::polar:
                    write_coord1(dst);
                    to(dst) (charset, anglechar, static_cast<CharT>(' '));
                    write_coord2(dst);
                    break;

                case complex_form::algebric:
                    to(dst) (static_cast<CharT>('('));
                    write_coord1(dst);
                    to(dst) (charset, strf::unsafe_transcode(" + i*"));
                    write_coord2(dst);
                    to(dst) (static_cast<CharT>(')'));
                    break;

                default:
                    assert(form == complex_form::vector);
                    to(dst) (static_cast<CharT>('('));
                    write_coord1(dst);
                    to(dst) (charset, strf::unsafe_transcode(", "));
                    write_coord2(dst);
                    to(dst) (static_cast<CharT>(')'));
                }
            };
    }
1 As noted earlier, the FormatSpecifiers types that parametrize the value_and_format template may change according to the format functions called. The FormatSpecifier for alignment is either alignment_format_specifier_q<false> or alignment_format_specifier_q<true>. alignment_format_specifier is a type alias to the former, which represents the default alignment format, which specifies that there is no alignment to handle.

Having implemented the above make_printer, now it is possible to print std::complex values inside aligned joins with any formatting options, except alignment formatting. To take advantage of that, the next make_printer overload ( that prints std::complex with alignment ) will do the following:

  1. remove the alignment from the value_and_format argument

  2. put the transformed value_and_format into a join

  3. apply the alignment to the join

    template < typename CharT, typename PreMeasurements
             , typename FPack, typename FloatFmt >
    static auto make_printer
        ( strf::tag<CharT>
        , PreMeasurements* pre
        , const FPack& facets
        , const strf::value_and_format
            < printable_def
            , std_complex_format_specifier
            , FloatFmt
            , strf::alignment_format_specifier_q<true> >& arg )
    {
        return strf::make_printer<CharT>
            ( pre
            , facets
            , strf::join(arg.clear_alignment_format())  (1)
                .set_alignment_format(arg.get_alignment_format()) );
    }
};
1 The clear_alignment_format() is a format function whose return type (in this case) is
value_and_format
    < printable_def
    , std_complex_format_specifier
    , FloatFmt
    , strf::alignment_format_specifier_q<false> >

You can see the complete implementation here.