The destination
class template
Creating a new output type involves defining a concrete class
that derives from destination
.
Having done that, one can write things to objects
of such type with the to
function template,
using the basic usage syntax
of the library:
strf::destination</*char type*/>& dst = /*...*/;
strf::to(dst) (/* arguments to be printed */);
But before learning how to implement such class,
it makes sense to first undestand how one uses the
destination
interface, i.e., how to write things
to it.
destination
is a simple class template: it
contains a boolean — which indicates whether
the state is "good" or "bad" — and two pointers; one of them points
to the end of buffer, and the other to the position where the
next character shall be written. They are returned by the
buffer_end
and
buffer_ptr
functions respectively.
Contrary to what is common in output streams abstractions,
where you need to use high-level functions to insert content ( like
sputc
in std::basic_streambuf
, or
write
in std::basic_ostream
), in destination
, one can write things directly to
its buffer.
if (dst.buffer_space() < 5) {
dst.recycle();
}
memcpy(dst.buffer_ptr(), "hello", 5);
dst.advance(5);
As demonstrated above, before writting anything to buffer_ptr()
, one
must check whether there is enough space,
and if not, one must call the recycle
function
( or flush
, which calls recycle
).
This is the only pure virtual function in destination
;
its job is to flush the content written so far and reset the position of
buffer_ptr()
and buffer_end()
. After recycle
is called,
the buffer’s space ( buffer_end() - buffer_ptr()
) is
guaranteed to be at least min_destination_buffer_size
( which is currently 64
but may be greater in future versions ).
This is a postcondition even when the state is "bad".
The state may change from "good" to "bad" in recycle
,
but never from "bad" to "good".
The "bad" state implies that writting
anything in the range [buffer_ptr(), buffer_end()
) has no relevent
side effect, though the behaviour is still defined, i.e.
the range must be a valid accessible memory area.
Sometimes, garbage_buff
is used to handle the bad state.
After writing to buffer_ptr()
, it is necessary to advance
the buffer pointer, otherwise the content will be overwritten
next time something is written in buffer_ptr()
.
This is can be done with the
advance
or the advance_to
member function, as shown above.
Now, we can see how a typical implementation would look like:
class my_destination: public strf::destination<char> {
public:
my_destination(/*...*/)
: strf::destination<char>{buff, sizeof(buff)}
// ...
{
/*...*/
}
my_destination(const my_destination&) = delete;
~my_destination();
void recycle() override;
void finish();
private:
bool print(const char* str_begin, const char* str_end)
{ /*...*/ }
char buff[strf::min_destination_buffer_size];
};
The print
member function above represents the code
that would send the content to the actual destination,
whatever it is. If print
never throws, then
recycle
could be implemented like below:
void my_destination::recycle()
{
if (good()) {
bool success = print(buff, buffer_ptr());
set_good(success);
}
set_buffer_ptr(buff);
}
Otherwise, it makes more sense to do:
void my_destination::recycle()
{
auto ptr = buffer_ptr();
set_buffer_ptr(buff);
if (good()) {
set_good(false);
bool success = print(buff, ptr);
set_good(success);
}
}
You may want to define a destructor that prints
what is left in the buffer. The issue here is that if print
throws
we must not propagate the exception ( since
destructors must not throw ).
my_destination::~my_destination()
{
if(good()) {
try {
print(buff, buffer_ptr());
} catch(…)
{
} // Need to silence the exception. Not good
}
}
That’s why it might be a good idea to create a member function to do this final flush:
void my_destination::finish()
{
bool is_good = good();
set_good(false);
if (is_good) {
print(buff, (buffer_ptr() - buff));
}
}
finish()
is supposed to be called after all content is written:
Almost
all destination classes of this library
have a finish
function ( the only exception is
discarder.
So you may want to follow the convention.
Another reason for creating finish
is that may return a value,
which is something that destructors can’t do.
How to create a target expression
There are several expressions that can be used as
the prefix in the basic usage syntax
.
Each of them causes the content to be printed into a different destination.
Perhaps you want to create your own. For example, if you use Qt,
you may want to create a toQString
"destination",
intended to create a QString
object ( in the same way as
to_string
creates
std::string
objects ).
This section explains how you can do that.
The first step, which involves most of the work, is
to create a class that derives from destination
,
which is covered in previous section.
Sometimes it makes sense to actually create two of them;
one having a constructor that receives the size
while the other does not, as explained soon.
The second step is to create a class that satisfies the requirements of DestinationCreator or SizedDestinationCreator or both. It acts as a factory ( or something analogous to that ) of the class(es) you defined in step 1. SizedDestinationCreator is for the case when the constructor of your destination class requires the number of characters to be printed ( because it needs to allocate memory or something ). DestinationCreator is for when it does not need that information.
The third and final step is to define the "target expression".
It must be an expression ( a function call or a constexpr value )
whose type is an instance of one the printing_syntax
class template.
The sample below illustrates the above steps:
// some type that is able to receive text
class foo { /* ... */ };
// step 1: define your destination class
class foo_writer: strf::destination<char> {
public:
explicit foo_writer(foo&);
foo_writer(const foo_writer&) = delete;
void recycle() override;
auto finish() -> /* ... */;
//...
};
// step 2: define the destination creator
class foo_writer_creator {
public:
using destination_type = foo_writer;
using char_type = char;
foo_writer_creator(foo& f): f_(f) {}
foo_writer_creator(const foo_writer_creator&) = default;
foo& create() const { return f_; }
private:
foo& f_;
}
// step3: define the expression that creates a printing_syntax object
auto to(foo& dst) {
strf::printing_syntax<foo_writer_creator> x{dst};
// x contains a member object of type foo_writer_creator
// initialized with dst
return x;
}
Examples
-
examples/toQString.cpp defines a constexpr value named
toQSting
that is analogous tostrf::to_string
, except that it creates aQString
( from Qt framework ) instead of astd::string
. -
examples/appendQString.cpp defines a function
append
used to append content into aQString
object