Fun with IO Streams

Written by Gordon Zeglinski

Introduction
C++ IO streams allow the developer to do things that are impossible using conventional C IO routines. For instance, using the socket streams from a few issues ago, one can write code that can read/write to a file or socket without regard to what it is reading/writing from. It's easy to imagine a case where one would like to "photo copy" a data stream. One such case is when the programmer wants to debug a inbound stream. Another is when simultaneous recording and processing of a inbound stream is desired, but the processing algorithm is also used where no duplication of the data stream is needed. In this issue, we'll look at creating a stream which can duplicate an istream.

Creating a Split Stream
When creating a stream that reads from a stream, the issue of double buffering arises. Before we tackle that issue, we'll do a little back tracking. Recall in the socket library, all the actual work is done in the buffer classes. Similarly, all the work in the "DupStream" library is done in its buffer class. It is possible to create unbuffered derivatives of streambuf. However, my experience with them was less than fun. Instead, I use a small buffer (1 character in DupStream).

We start by creating a new stream buffer class by subclassing streambuf. An interesting point is that the DupInBuff only needs to know the streambuf objects belonging to the source and copy stream. Next we create an istream subclass. Because the purpose is to duplicate classes derived from istream only, we only create an iDupStream object. Support to duplicate classes derived from ostream is left as an exercise for the reader. Even though the DupInBuf only works on streambuf objects, the constructor takes references to istream and ostream objects. It passes their streambuf instances to DupInBuf. Now that we know what the class definitions look like, we'll look at how they work.

Recall that overflow and underflow are called to send and fetch the contents of the buffer, respectively, to/from the streams source. We'll look at these two functions next. The overflow function is pretty straight forward. When called, it sends the contents of the buffer to the "input" streams buffer. It assumes that the "input" stream will accept all of its buffer's contents, a reasonable assumption considering the buffer is only 1 character long. As we see below, the underflow function is a bit more complex. When we need a character, we check the input stream to see if it has any data in its buffer waiting to be read. If not, we call in->sgetc to fetch some data from the ultimate producer (the stream's source). Because sgetc doesn't remove the character it returns from the buffer, we can reget the character later using sgetn. Next, we read in some characters from the input stream and write them out to the "mirroring stream". Notice that the code is written so that by changing a #define, we can pull more than 1 character at a time from the input stream. This may be appropriate in some circumstances but will result in problems for the socket stream or any other stream that will block the thread when a request for more characters than available is made.

That's all there is to it! The complete source to DupStream is included for anyone to use.

More Fun with Streams
When will it end! Creating a duplication stream is just one possibility. Suppose that we wanted to be able to send data to a stream. Sometimes this data is to be sent unencoded to the ultimate consumer. Other times, it needs to be processed before it's sent to the ultimate consumer. It is equally easy to write a new stream type that works along these same principles to allow the same function to do both tasks simply by changing the type of stream it's writing to. One example of such a stream would be a "gzip" stream. The ultimate consumer/producer could be a file or socket stream. The gzip stream would read/write compressed data from the ultimate producer/consumer.

Summary
C++ IO streams provide a highly flexible way of performing all kinds of IO operations. Creating new streams that process data as it passes to/from the destination/source is a fairly simple operation. The possibilities are only limited by the programmer's imagination.