Portability


Template Classes for the iostreams Library

Randal Kamradt


Randy has been programming since 1984, and is working for Bordart Automation where he develops CD-ROM based applications for libraries. His special interests include object-oriented programming/design and C/C++ programming in general.

One of the nicer additions that C++ made to C is the iostreams facility. Even if you only use C++ as "a better C," iostream provides an elegant method of I/O and allows easy extensions for user-defined types. This ease of extension only works one way, though, as creating new stream types that work with existing code can be fairly complicated, and requires a complete understanding of the streams hierarchy.

When I first got my hands on a C++ compiler I wanted to create a streams class that would work with a compact disk. My compact disk driver provided basic read routines, and I already had a class that I used as an interface to the driver. To provide seamless integration with existing code I needed to create new classes that fit in with the existing iostreams hierarchy. (See the sidebar "iostreams Hierarchy" for a discussion of this class and virtual inheritance.)

The first step was to copy the definitions of the ifstream, fstreambase, and filebuf classes and rename them to cdstream, cdstreambase, and cdbuf classes. (I was not concerned with output classes since I was working with CDs.) Then I changed the open parameters and constructor parameters to suit the class I used to interface with the CD driver. Within the cdbuf class I replaced the file-handle integer with a pointer to the CD interface class, and added appropriate new and delete statements in the open and close routines of cdbuf. Finally, I replaced the calls to the global open, close, read, and seek with ones that used the CD class pointer. After doing all that and then fixing the few bugs I let creep in, I was convinced that there had to be a better way.

A few months after creating these classes I was faced with doing it again. By this time we had a new version of Borland C++ with templates. After thinking about it for a while, I decided that templates could be the better way I wanted. By creating generic versions of the classes I had copied before, I could create an endless number of different stream types for anything that resembled a stream. In this article, I will present these template classes, and a serial port stream class as an example. These classes were compiled and tested with Borland C++ v3.1. The portability of the templates depends on consistency in the iostreams facility across compilers.

Template Classes

To avoide duplicating classes unnecessarily, I created templates for the classes. (See the sidebar "Templates" for more information.) The classes I made templates of included filebuf, fstreambase, fstream, ifstream, and ofstream, I call the template classes tbuf<T>, tstreambase<T>, tstream<T>, itstream<T>, and otstream<T>. The <T> suffix indicates a templeate class. The replaceable type is a basic class that has open, close, read, write, and seek member functions. (I will call this class T when referrring to this replaceable type.) When creating a class for an entity that does not have one or more of these functions, you can either create a dummy function that does nothing, or one that calls an error routine. See Listing 1 for all template class definitions.

The first template class, tbuf<T> is the most complex. It contains one object of the variable type, and controls the operation of that object. tbuf<T>'s open, close, and seek member functions directly call T's open, close, and seek member functions. The overflow and underflow member functions call read and write, along with setting the various buffer pointers. It also has the ability to attach an already open T to itself. tbuf<T> is derived from streambuf, which provides it's interface. Some of the member functions of tbuf<T> override the virtual functions of streambuf. Since ios contains a pointer to streambuf, ios has access to these virtual functions, and is able to read and write tbuf<T>.

The second template class, tstreambase<T> contains a tbuf<T>. It is derived (virtually) from ios. In its constructor tstreambase<T> initializes ios with a pointer to its tbuf<T> object. It can also open its tbuf<T> object if called with the necessary parameters. Otherwise, it has an open, close, and attach call that map directly to the tbuf<T> open, close, and attach member function.

The last set of template classes are tstream<T>, itstream<T>, and otstream<T>. These are multiply derived from istream/ostream and tstreambase<T>. They are shell classes that simply combine the capabilities of the two inherited classes. The only thing necessary in the definition is the duplication of the constructors, and an open and rdbuf member function, that calls the tstreambase open and rdbuf member functions. The open function is redefined to give default mode values to itstream<T> and otstream<T>. The rdbuf function is redefined to avoid ambiguities with ios which contains its own rdbuf function.

Note that when creating a deep hierarchy, constructors need to be defined for all classes, even if they don't change from the base class's constructor. Duplicating numerous constructors, or constructors with long parameter lists, can be a nuisance. There are four constructors for itstream<T>/otstream<T> that are duplicates of the tstreambase<T> constructors:

tstream()
tstream(const char *name, int mode, int prot)
tstream(T &f)
tstream(T &f, char *buffer, int length)
The default constructor, tstream() initializes the buffer using default parameters for buffer size. The stream is considered closed. tstream(const char *name, int mode, int prot) initializes the buffer, and opens the named stream. tstream(T &f) initializes the buffer with the T parameter. tstream(T &f, char *buffer, int length) initializes the buffer with T, and sets the buffer to the char * with the length specified. These four constructors are duplicated in the three classes itstream<T>, otstream<T>, and tstream<T>. In all of these cases, the parameters are passed on directly to tstreambase<T>'s constructors.

The constructors for tstreambase<T> call the constructors either for the default tbuf<T> constructor, or in the case of constructors tstream(T &f) and tstream(T &f, char *buffer, int length) it passes the parameters to tbuf<T>'s constructors. It then calls the ios init function to set ios pointer to its T data member. For constructor tstream (const char *name, int mode, int prot), it calls T's open function.

The tbuf<T> class has three constructors:

tbuf()
tbuf(T &f)
tbuf(T &f, char *buffer, int length)
tbuf(), the default constructor, builds a buffer of default size. The T stream is considered closed. tbuf(T &f) builds a default buffer, but attaches T as its stream. The T stream is considered open for read or write. tbuf(T &f, char *buffer, int length) builds a buffer using the buffer and length parameters, and attaches T as its stream.

SerialStream and ComBuffer classes

As an example of how to use these templates, I decided to use a serial port stream. In MS-DOS you can access the serial port as a stream, but it is not interrupt driven, so it is nearly useless without special drivers. The serial port stream is divided into two classes, SerialStream, and ComBuffer. The ComBuffer provides an interrupt-driven circular buffer for input, and passes output directly to the port. The Serial-Stream class uses the ComBuffer class to do its I/O, and has the correct interface to plug into the template. I split the classes in two in case there was more then one stream per port. However, ComBuffer needed to be mapped directly to a port.

The only instance of ComBuffer is a static global (see Listing 2 and Listing 3) CommPorts which is an array of two, one for each port. Since it is a static global, it is initialized before main. It uses the static data member initPort to ensure the correct port number is initialized. In the constructor and destructor for ComBuffer I included a print statement to visually assert the order of the construction. ComBuffer is not a safe or complete class that could be used anywhere in a program, so I made the definition private to the module. I could alternatively have made the definition private to the SerialStream class definition.

The SerialStream class uses ComBuffer to communicate with the physical ports, via a pointer. When a SerialStream is opened, the name parameter is decoded to give the port number. The port number is used as an index into the CommPorts array, and the pointer to that index is saved. During reading and writing the request is passed on to ComBuffer via that pointer. Only one character is read or written at a time. This inefficiency does not concern me, since the iostream should be unbuffered, and should only request one character at a time anyway.

Any class that is to be used in the streams templates must meet certain requirements. First it needs a default constructor. This constructor should leave the stream in a closed state. It needs a open function that takes a const char *, and two ints for parameters. This is perhaps the most extreme restriction, as not all streams will be able to map these parameters. For streams that take fewer parameters, as my SerialStream class does, it is easy enough to ignore the rest. For a class that needs more information to get started, this can be a problem. One alternative could be to use the name parameter to pass in additional information:

x.open("COM1, INT=7, SPEED=2400",ios::in);
Although this appears sloppy, and requires character translation in the open function, it is not without precedent. Another alternative is to access the T class directly:

x.open("COM1",ios::in);
x.rdbuf()->fd()->setspeed(1200);
x.rdbuf()->fd()->setint(7);
The stream class must also define a close function that takes no parameters. A read and write function that takes a char * and an int, as well as a seek function that takes a long and a ios::seek_dir type must be present. Finally, a const void * conversion operator needs to return the open/close state, as the this pointer or a null pointer. The open, close, read, write, and seek function can all be dummied up if not needed. If seek is dummied, you need to make sure the stream is unbuffered.

I mentioned previously that the portability of the templates depends on how similarly different libraries implement the internals of the iostreams classes. This code was made with the Borland C++ v3.1 libraries in mind, and might need to be changed for other implementations. In the header files, Borland mentions minor differences with the AT&T library, so I assume that the templates will work as well under AT&T and any other vendor that follows them. If the internals of iostreams are not under discussion in the ANSI X6J16 commitee, then perhaps vendors should include a set of templates similar to these in the C++ library to allow different streams types to be portable from one implementation to another.