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