The basic_outbuff
class template
Creating a new output type involves defining a concrete class
that derives from basic_outbuff.
Once this is done, one can write things to objects
of such type with the to
function template,
using the basic usage syntax
of the library:
strf::basic_outbuff</*char type*/>& dest = /*...*/;
strf::to(dest) (/* arguments to be printed */);
basic_outbuff 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
end
and
pointer
functions respectively.
Contrary to what is common in output streams abstractions,
where you need to use high-lever functions to insert content ( like
sputc
in std::basic_streambuf
, or
write
in std::basic_ostream
), in basic_outbuff
you can write things directly to
pointer()
and then calls the advance
or the advance_to
function to update
the pointer. For instance:
if (dest.space() < 5) {
dest.recycle();
}
strncpy(dest.pointer(), "hello", 5);
dest.advance(5);
Of course, before writting anything to pointer()
, one
needs to check whether there is enough space.
If not, one has to call the recycle
function, as done above.
This is the only pure virtual function in basic_outbuff
.
Its job is to flush the content written so far and reset the position of
pointer()
and end()
so that the space ( end() - pointer()
)
becames greater than or equal to min_space_after_recycle</*char type*/>()
.
This is a postcondition
even when the state is "bad". The "bad" state implies that writting
anything in the range [pointer(), end()
) doesn’t have any relevent
side effect, though the behaviour is still defined, i.e.
the range [pointer(), end()
) must be valid accessible memory area
( sometimes garbage_buff
is used to handle the bad state ).
The state can change from "good" to "bad" in recycle
,
but never from "bad" to "good".
A typical implementation would look like this:
class my_outbuff: public strf::outbuff {
public:
my_outbuff(/*...*/)
: strf::outbuff{buff, sizeof(buff)}
// ...
{
/*...*/
}
my_outbuff(const my_outbuff&) = delete;
~my_outbuff();
void recyle() override;
void finish();
private:
bool print(const char* str_begin, const char* str_end)
{ /*...*/ }
char buff[strf::min_space_after_recycle()];
};
Where the print
member function 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_outbuff::recycle()
{
if (good()) {
bool success = print(buff, pointer());
set_good(success);
}
set_pointer(buff);
}
Otherwise, it makes more sense to do:
void my_outbuff::recycle()
{
auto ptr = pointer();
set_pointer(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 ).
That’s why it might be a good idea to create a member function to do this final flush:
finish()
is supposed to be called after all content is written:
Almost
all classes of this library that derive from basic_outbuff
have a finish
function ( the only exception is
discarded_outbuff.
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 destination 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 is used to create
`std::string
objects ).
This section explain how you can do that.
The first step, which involves most of the work, is
to create a class that derives from basic_outbuff
.
The previous section provides some assistance on that.
Sometimes it makes sense to actually create two of them;
one having a constructor that receives the size and
the other not, as explained soon.
The second step is to create a class that satisfies the requirements of OutbuffCreator or SizedOutbuffCreator or both. It acts as a factory ( or something analogous to that ) of the class(es) you defined in step 1. SizedOutbuffCreator is for the case when the constructor of your outbuff class requires the number of characters to be printed ( because it needs to allocate memory or something ). OutbuffCreator is for when it does not need that information.
The third and final step is to define the "destination expression". It must be an expression ( a function call or a constexpr value ) whose type is an instance of one the following class templates:
-
destination_no_reserve
: can only be used when your factory ( step 2 ) is OutbuffCreator -
destination_calc_size
: the factory must be SizedOutbuffCreator
Where the class created in step 2 is the template parameter.
The major difference between them lies in the implementation of
operator()
and tr
member functions. In destination_no_reserve
it is something like this:
typename my_outbuff_creator::outbuff_type dest{creator.create()};
// ... write content in dest ...
return dest.finish();
Whereas in destination_calc_size
it is:
std::size_t size = /* calculate size ... */;
typename my_outbuff_creator::sized_outbuff_type dest{creator.create(size)};
// ... write content in dest ...
return dest.finish();
, where my_outbuff_creator
is the type defined in step2, and creator
is a private member object of that type.
The implementation of
destination_with_given_size
is similar to of destination_calc_size
.
The difference is that, instead of being calculated,
the size is passed to the
the constructor
and stored in a member variable.
However, in most cases, if any, it does’t make sense to opt for destination_with_given_size
.
The reason why it was created was to be used as the return type
the reserve
function.
The code below illustrates the above steps:
// some type that is able to receive text
class foo { /* ... */ };
// step 1: define your outbuff class
class foo_writer: strf::basic_outbuff<char> {
public:
explicit foo_writer(foo&);
void recycle() override;
auto finish() -> /* ... */;
//...
};
// step 2: define the outbuff creator
class foo_writer_creator {
public:
using outbuff_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 destination expression
auto to(foo& dest) {
strf::destination_no_reserve<foo_writer_creator> x{dest};
// x contains a member object of type foo_writer_creator
// initialized with dest
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