User-Defined Extractors and Inserters in C++
- C++ Input/Output Models
- The Stream Inserter Operator <<
- The Stream Extractor Operator >>
- From Here
Whether we're reading objects from files or sending them across networks, we want the process to be as painless and transparent as possible. Ideally, we'd like to be able to change the source from which we're getting an object or the destination where we're sending an objectwithout major changes to the code in either case. Further, we want the processes of reading, writing, storing, and retrieving objects, whether built-in or user-defined, to be basically the same. In C++, this kind of transparency is supported using two important input/output models: the file model and the stream model.
This article explores a few aspects of object-oriented stream formatting and object translation. In particular, we explain how to use and define stream inserters and extractors. The next article will focus on user-defined manipulators and iterators with user-defined objects. We want our user-defined objects to work with streams just as easily as the built-in datatypes do. In the same manner that built-in datatypes can be formatted in various ways for the stream by using manipulators, we want to use manipulators to control the formatting of our user-defined objects.
With the inserters, extractors, and manipulators properly defined for our user-defined objects, we can take full advantage of the C++ file model and stream model.
C++ Input/Output Models
For our examples, we'll employ two scaled-down and simplified versions of user-defined objects that we use in curriculum-planning software at Ctest Laboratories:
The course object has a start time, an end time, a course description, and a set of days on which the course is offered.
The Schedule object is simply a collection of course objects.
It's often useful to represent these objects in an XML or HTML format. Because our planning software is implemented partly in C++ and partly in Prolog, it's also useful for us to represent these objects as horn clauses. We overload stream inserters and stream extractors and define manipulators to translate our objects to XML, HTML, or horn clauses, depending on the state of the stream into which the objects are being inserted or from which they're being extracted.
File Model
In the file model, each input or output device is addressed as though it were a file and the operations performed on the device are the kinds of operations that are performed on files. That is, devices are opened, read, written, and closed. For instance, Listing 1 opens a printer device named /dev/lp0 and writes one of our user-defined objects that's referenced by Schedule[N]:
Listing 1 Writing a User-Defined Object to a Printer Device
int main(int argc, char argv[]) { vector<course> Schedule; ... ofstream DegreePlan(open("/dev/lp0",O_WRONLY)); ... DegreePlan.write(Schedule[N]) ... DegreePlan.close(); }
The constructor is used to open the printer named /dev/lp0, and the write() and close() methods are called on the device as though it were a file. Network cards, serial ports, sound cards, etc. are all addressed as files in the file model.
NOTE
Because this code is very UNIX-oriented, it may not work on Windows systems. The device-naming convention in particular is UNIX-oriented, but also the concept of treating a printer as a file.
Stream Model
The second important input/output processing model is the stream model. In the stream model, objects move through input or output streams as anonymous sequences of bytes. The program opens some device as a file and connects it to a data stream. Objects are then inserted into or extracted from the stream. The programmer doesn't have to focus on the details of handling a specific input or output device. Each device is treated as a file. As long as the programmer is familiar with the commonly used file interface primitives (open, read, write, and close), the I/O can be accomplished easily.
Further, the programmer doesn't have to worry about the details of how a particular object is implemented because during the insertion or extraction process objects are translated from their complex forms to or from a generic sequence of bytes. The insertion process translates the object from its native format to a generic sequence of bytes and the extraction process translates the generic sequences of bytes back into the object's native form. The user of our course object or Schedule object should not have to worry about the internal structure of the objects. As long as those objects support the stream interface, the programmer will be able to insert those objects into and extract those objects from the stream, regardless of what kind of file (device) the stream is connected to.
Remember, our goal is to easily and transparently move various kinds of objects, user-defined or built-in, from one place to another or between various kinds of devices, using the same interface. The interfaces we'll focus on here are the stream inserter operator << and the stream extractor operator >>. In the next article, we'll look at the stream manipulators and the stream iterators.
In Listing 2, we use the stream inserter operator << to insert a collection of course objects into a stream named DegreePlan that's connected to a printer:
Listing 2 Writing a Vector of User-Defined Objects to a Stream That's Connected to a Printer
int main (int argc,char *argv[]) { vector<course> Schedule; course Course; ... ofstream DegreePlan(open(´´/dev/lp0",O_WRONLY)); ... for(int N =0:N < Schedule.size();N++) { DegreePlan << Schedule[N] << endl; } ... DegreePlan.close(); }