The destination class template

Creating a new output type involves defining a concrete class that derives from destination. 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::destination</*char type*/>& dest = /*...*/;
strf::to(dest) (/* arguments to be printed */);

So the first to learn is how destination works. It is a simple class template that 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-lever functions to insert content ( like sputc in std::basic_streambuf, or write in std::basic_ostream ), in destination you can write things directly to buffer_ptr(). After that, you shall call the advance or the advance_to function to update the buffer pointer. For instance:

if (dest.buffer_space() < 5) {
    dest.recycle();
}
memcpy(dest.buffer_ptr(), "hello", 5);
dest.advance(5);

As done above, before writting anything to buffer_ptr(), one must check whether there is enough space, and if not, one must call the recycle function. 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() so that the space ( buffer_end() - buffer_ptr() ) 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 [buffer_ptr(), buffer_end()) doesn’t have any relevent side effect, though the behaviour is still defined, i.e. the range [buffer_ptr(), buffer_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_destination: public strf::destination<char> {
public:
    my_destination(/*...*/)
        : strf::destination<char>{buff, sizeof(buff)}
        // ...
    {
        /*...*/
    }

    my_destination(const my_destination&) = delete;
    ~my_destination();

    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_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:

my_destination dest{ /*...*/ };

strf::to(dest) (/*...*/);
strf::to(dest) (/*...*/);
some_function(dest);
strf::to(dest) (/*...*/);

dest.finish();

Almost all classes of this library that derive from destination have a finish function ( the only exception is discarded_destination. 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 creates 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 destination. 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 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 "destination expression". It must be an expression ( a function call or a constexpr value ) whose type is an instance of one the class templates below, having the class created in step 2 as the template parameter.

  • destination_no_reserve: Its template argument must be DestinationCreator, and it has the following effect when writing the arguments ( when its member function operator() or tr is called ):

    typename your_destination_creator::destination_type dest{creator.create()};
    // ... write content in dest ...
    return dest.finish();

    , where:

    • your_destination_creator is the template argument ( and the type defined in step 2). It must be be DestinationCreator.

    • creator is an object of type your_destination_creator.

  • destination_calc_size: Its template argument must be SizedDestinationCreator, and it has the following effect when writing the arguments:

    std::size_t size = /* calculate size ... */;
    typename you_destination_creator::sized_destination_type dest{creator.create(size)};
    // ... write content in dest ...
    return dest.finish();
  • destination_with_given_size: the factory must be SizedDestinationCreator, and it has the same effect as of destination_calc_size, except that the size is not calculated but is instead passed to its the constructor. In most cases, it does’t make sense to opt for destination_with_given_size. The reason why it was created is to define the return type the reserve function.

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