Tutorial

2.1.4. Writing a container_device

Suppose you want to write a Device for reading from and writing to an STL container. In order for combined reading and writing to be useful, you will also need to support seeking within the container. There are several types of Devices which combine reading and writing; they differ according to whether there are two separate character sequences for input and output, or a single combined sequence, and whether there are separate position indicators for reading and writing or a single read/write position indicator. See Modes for details.

A narrow-character Device for read-write access to a single character sequence with a single position indicator is called a SeekableDevice. A typical SeekableDevice looks like this:

#include <iosfwd>                           // streamsize, seekdir
#include <boost/iostreams/categories.hpp>   // seekable_device_tag
#include <boost/iostreams/positioning.hpp>  // stream_offset

namespace io = boost::iostreams;

class my_device {
public:
    typedef char                 char_type;
    typedef seekable_device_tag  category;

    std::streamsize read(char* s, std::streamsize n)
    {
        // Read up to n characters from the underlying data source
        // into the buffer s, returning the number of characters
        // read; return -1 to indicate EOF
    }

    std::streamsize write(const char* s, std::streamsize n)
    {
        // Write up to n characters to the underlying 
        // data sink into the buffer s, returning the 
        // number of characters written
    }

    stream_offset seek(stream_offset off, std::ios_base::seekdir way)
    {
        // Seek to position off and return the new stream 
        // position. The argument way indicates how off is
        // interpretted:
        //    - std::ios_base::beg indicates an offset from the 
        //      sequence beginning 
        //    - std::ios_base::cur indicates an offset from the 
        //      current character position 
        //    - std::ios_base::end indicates an offset from the 
        //      sequence end 
    }

    /* Other members */
};

Here the member type char_type indicates the type of characters handled by my_source, which will almost always be char or wchar_t. The member type category indicates which of the fundamental i/o operations are supported by the device. The category tag seekable_tag indicates that read, write and the single-sequence version of seek are supported.

The type stream_offset is used by the Iostreams library to hold stream offsets.

You could also write the above example as follows:

#include <boost/iostreams/concepts.hpp>  // seekable_device

class my_source : public seekable_device {
public:
    std::streamsize read(char* s, std::streamsize n);
    std::streamsize write(const char* s, std::streamsize n);
    stream_offset seek(stream_offset off, std::ios_base::seekdir way);

    /* Other members */
};

Here seekable_device is a convenience base class which provides the member types char_type and category, as well as no-op implementations of member functions close and imbue, not needed here.

You're now ready to write your container_device. Again, let's assume your container's iterators are RandomAccessIterators.

#include <algorithm>                       // copy, min
#include <iosfwd>                          // streamsize
#include <boost/iostreams/categories.hpp>  // source_tag

namespace boost { namespace iostreams { namespace example {

template<typename Container>
class container_device {
public:
    typedef typename Container::value_type  char_type;
    typedef seekable_device_tag             category;
    container_device(Container& container)
        : container_(container), pos_(0)
        { }

    std::streamsize read(char_type* s, std::streamsize n)
    {
        using namespace std;
        streamsize amt = static_cast<streamsize>(container_.size() - pos_);
        streamsize result = (min)(n, amt);
        if (result != 0) {
            std::copy( container_.begin() + pos_, 
                       container_.begin() + pos_ + result, 
                       s );
            pos_ += result;
            return result;
        } else {
            return -1; // EOF
        }
    }
    std::streamsize write(const char_type* s, std::streamsize n)
    {
        using namespace std;
        streamsize result = 0;
        if (pos_ != container_.size()) {
            streamsize amt = 
                static_cast<streamsize>(container_.size() - pos_);
            result = (min)(n, amt);
            std::copy(s, s + result, container_.begin() + pos_);
            pos_ += result;
        }
        if (result < n) {
            container_.insert(container_.end(), s + result, s + n);
            pos_ = container_.size();
        }
        return n;
    }
    stream_offset seek(stream_offset off, std::ios_base::seekdir way)
    {
        using namespace std;

        // Determine new value of pos_
        stream_offset next;
        if (way == ios_base::beg) {
            next = off;
        } else if (way == ios_base::cur) {
            next = pos_ + off;
        } else if (way == ios_base::end) {
            next = container_.size() + off - 1;
        } else {
            throw ios_base::failure("bad seek direction");
        }

        // Check for errors
        if (next < 0 || next >= static_cast<stream_offset>(container_.size()))
            throw ios_base::failure("bad seek offset");

        pos_ = next;
        return pos_;
    }

    Container& container() { return container_; }
private:
    typedef typename Container::size_type   size_type;
    Container&  container_;
    size_type   pos_;
};

} } } // End namespace boost::iostreams:example

Here, note that

The implementation of write is a bit different from the implementation in container_sink. Rather than simply appending characters to the container, you first check whether the current character position is somewhere in the middle of the container. If it is, you attempt to satisfy the write request by overwriting existing characters in the container, starting at the current character position. If you reach the end of the container before the write request is satisfied, you insert the remaining characters at the end.

The implementation of seek is straightforward. First, you calculate the new character position based on off and way: if way is ios_base::beg, the new character position is simply off; if way is ios_base::cur, the new character position is pos_ + off; if way is ios_base::end, the new character position is container_.size() + off - 1. Next, you check whether the new character position is a valid offset, and throw an exception if it isn't. Instances of std::basic_streambuf are allowed to return -1 to indicate failure, but the policy of the Boost Iostreams library is that errors should be indicated by throwing an exception (see Exceptions). Finally, you set the new position and return it.

You can use a container_device as follows

#include <cassert>
#include <ios> // ios_base::beg
#include <string>
#include <boost/iostreams/stream.hpp>
#include <libs/iostreams/example/container_device.hpp>

namespace io = boost::iostreams;
namespace ex = boost::iostreams::example;

int main()
{
    using namespace std;
    typedef ex::container_device<string> string_device;

    string                     one, two;
    io::stream<string_device>  io(one);
    io << "Hello World!";
    io.flush();
    io.seekg(0, BOOST_IOS::beg); // seek to the beginning
    getline(io, two);
    assert(one == "Hello World!");
    assert(two == "Hello World!");
}