P.J. Plauger is senior editor of The C Users Journal. He is convenor of the ISO C standards committee, WG14, and active on the C++ committee, WG21. His latest books are The Standard C Library, and Programming on Purpose (three volumes), all published by Prentice-Hall. You can reach him at pjp@plauger.com.
Last month, I introduced the extensive set of classes in the Standard C++ library known collectively as iostreams. (See "Standard C: Introduction to Iostreams," CUJ, April 1994.) I begin this month with the foundation upon which all of iostreams is constructed, class ios. It is a large class big enough to command a header pretty much all to itself. You might begin by gazing for a spell at Listing 1, which shows one way to implement the header <ios>, home of class ios and a few close friends.
If you are an old hand at C++, you will find a lot not to recognize here. The draft C++ Standard introduces several innovations for which there is currently little or no existing practice. Besides, different compilers implement different interim dialects of the evolving standard. Strange #if logic is almost impossible to avoid for the near future. Moreover, I am presenting a notoriously unique implementation, as is my wont. This is not a history lesson so much as an exposition of the Standard C++ library in its own right.
If you are newer to C++, you will find a lot not to understand here. Class ios is bigger, more diffuse, and trickier than average. I can't always take the mysteries in lexical order, or cover them in detail, but I'll try to shed at least some light on all of them by the end of this installment.
Bitmask Types
For openers, you may notice several funny bits of notation in Listing 1:
Briefly, here's the story. The last set of integer synonyms (along with seek_dir) are widespread current practice. The draft C++ Standard allows optional overloading on this type for backward compatibility. The draft also introduces the preferred middle set as bitmask types enumerations overloaded to permit their manipulation as bit patterns. Without such overloading, you'd have to write all sorts of type casts, such as:
- Several enumerations are given "secret" names (_Fmtflags, _Iostate, and _Openmode).
- The macro _BITMASK somehow relates these secret names to more conventional type names (fmtflags, iostate, and open-mode).
- Two of these names nevertheless have still other integer type variants io_state and open_mode).
- One of these variants, io_state, overloads several member functions only if the macro _HAS_ENUM_OVERLOADING has a nonzero value.
cout.setf((fmtflags)(dec | showbase));But enumeration overloading is a fairly recent addition to the C++ language. If it's available, the enumeration _Iostate needs to have a slew of operators overloaded for it. Then iostate can be made a synonym for it with a typedef. And then the optional type io_state can be defined as a distinguishable integer type.If enumeration overloading is not available, however, the best course is to make iostate an integer type. That way you don't find yourself writing casts all the time. But then you can't easily distinguish io_state as yet another integer type, so you want to omit it. Got that? If not, don't worry about it too much. It's just another of those interim dialect kludges.
Listing 2 shows how the macro _BITMASK expands, depending on the definition of _HAS_ENUM_OVERLOADING, to deal with these variations on a theme.
Exceptions
Exceptions should be equally mysterious to all comers. The draft C++ Standard adds them to iostreams as a pure invention they are part of no prior practice that I know of. The sentiment was that the Standard C++ library could hardly ignore exceptions, since they were added to the language to deal with many library-related issues. (See "Standard C: The Header <exception)," CUJ, February 1994.)Traditionally, operations on iostreams that fail set one or more status bits within class ios and simply return. These status bits have some integer type iostate and are represented in Listing 1 as the private member _State. The member functions clear and setstate let you set and clear these bits. The member functions rdstate, good, bad, fail, and eof let you read and test the status bits in various ways.
All iostreams functions should be careful to test the status bits before trying anything rash. In particular:
But what if you don't want your program simply to keep stumbling on in the presence of iostream failures? Or what if you don't want to test the result of every operation? That's where the new exception mask comes in. Listing 1 indicates it as the private member _Except (also of type iostate). You can obtain its value by calling the member exception( ), or set it by calling exception(iostate). Any change of state that results in rdstate( ) & exceptions( ) becoming nonzero throws an exception.
- bad (bad( ) or rdbuf( ) & ios::badbit) indicates a read/write error or some other loss of integrity of the associated streambuf object controlling the actual I/O stream failure exception.
- eof (eof( ) or rdbuf( ) & ios::eofbit) indicates that an earlier read operation has encountered the end of the input stream.
- fail (fail( ) or rdbuf( ) & ios::failbit) indicates that an earlier read operation has failed to match the required pattern of input text.
Well, that's only approximately true. Listing 3 shows the file ios.c, which includes all the functions needed for any program that contains an object of class ios. For now, just look at the (misnamed) function clear. It sets all status flags to the value of its argument, then checks whether it should throw an exception. (Note, in passing, that the absence of an associated streambuf object automatically gives the ios object "bad" status.)
The exception ios::failure is reported by calling its member function raise( ). It may or may not actually throw an exception, as I've described in earlier installments of this column. You will find the destructor for ios::failure also in Listing 3, along with the destructor for ios itself. I explain the funny logic in ios::~ios( ) below.
Class ios has one other bit of cuteness with regard to testing its status flags. It overloads the unary NOT operator! () and the unary type cast operator void *( ) as a quick way of testing for I/O failures. Thus, you can write:
if (!(cin >> x)) cerr << "can't read x" << endl;because cin is derived from class ios. Note the use of _Bool for a function that returns zero/nonzero for testing. The committee has voted in a new type bool, which must now percolate through the pipeline of new compiler releases. This is its placemarker. For now, it is a synonym for type int.
Initialization
Iostreams has one very unusual requirement you're allowed to use it before it's constructed and after it's destructed. Put another way, you can extract characters from cin, or insert them in cout, cerr, or clog, even from constructors and destructors for static objects. The problem with this license is that you can't be sure all the static objects that make up iostreams are constructed (and not yet destructed) when you make such demands upon them.The trick to pulling this off is twofold:
Class Init plays a pivotal role in pulling off the first part of the trick. It is used in the header <iostream>, which declares all the standard stream objects, such as cin and friends. That header includes a declaration such as:
- Ensure that the objects get explicitly constructed exactly once before they're used in any translation unit.
- Ensure that the conventional constructors and destructors don't really do anything rash with the data stored in the objects.
static ios::Init _Ios_init;So any translation unit that includes <iostream>, to make use of the standard streams, must first construct a static object of type ios::Init. You will notice from Listing 1 that this class includes a static counter, called ios:: Init::_Init_cnt here. The constructor for ios::Init increments this counter, and the destructor decrements it. The constructor also uses the counter to determine when it is entered the very first time. And that's when all the static objects for iostreams really get constructed. (They are never really destructed, so you can use iostreams right up to program termination.) I'll show this code in more detail in a future installment.The second part of the trick is more pervasive. It requires that any static objects for iostreams have a destructor, and at least one constructor, that does nothing. (Storing a pointer to a "v-table" still occurs, even in an empty constructor, but that should be innocuous.)
Traditionally, it is the default constructor that serves this role. But the draft C++ Standard typically requires that such constructors do something nontrivial. And permitting a constructor to leave an object uninitialized is bad C++ manners, to say the least. So I have chosen to document this mildly bogus behavior in terms of a special secret enumeration:
enum _Uninitialized {_Noinit};Thus, you can tell at a glance that the protected constructor:
ios(_Uninitialized) {}is required to do nothing.Destructors are a bit messier, since you get only one per class. The one for class ios does have to do something, should the program make use of extensible arrays (described later in this column). The code that discards such arrays has to be kludged to not discard them for the standard streams. Listing 3 shows one way to perform a reasonably safe, if graceless, kludge.
You will see even more of this trickery in iostreams classes to come.
Construction and Copying
Class ios is properly initialized by calling the function init, shown in Listing 3. The default values for the various formatting properties are those spelled out in the draft C++ Standard. You will notice that the function ends by calling clear(goodbit), mostly for stylistic reasons. This particular call can't throw an exception when it changes the status bits or exception mask, but others can. So I make a point of calling clear, directly or indirectly, at the end of any function that makes such changes.Listing 4, for example, shows the member function exceptions(iostate). It follows this simple but safe rule. So, too, does the member function copyfmt(const ios&), shown in Listing 5. It was added as a way of saving and restoring all formatting information, without mucking with the pointer to streambuf or its associated status bits. The one funny bit of business involves the copying of any extensible arrays, which I still promise to describe later in this column.
The companion to copyfmt(const ios&) is the (misnamed) member function rdbuf(streambuf *), shown in Listing 6. It copies just the streambuf pointer and status bits, then checks to see whether the exception mask calls for an exception to be thrown. You use this function to attach multiple streams to the same streambuf, as I described briefly last month.
Now you can understand the special handling required for the ios assignment operator, shown in Listing 7, and the copy constructor, shown in Listing 8. The former does much the same thing as rdbuf(streambuf *), then calls copyfmt to finish the smart copy and check for exceptions. The latter initializes the new ios object to something innocuous, then relies on the assignment operator to do all the hard work.
The Real Stuff
At last we come to the raison d'tre for class ios keeping track of stream-related information. The functions that do so are mostly trivial. Quite a few, in fact, merely store a new value in one of the private member objects, then return the previously stored object. (It is up to the derived classes istream, ostream, and their kin to actually do something with all this information.)Listing 9 shows a representative sample, the member function tie(ostream *). (It is used to "tie" two streams together, so the output stream gets flushed before reads or writes occur to the other stream.) The member functions flags(fmtflags), fill(int), and width(int) are essentially the same, so I won't bother to show them.
The format flags are manipulated in a variety of ways, so class ios provides a variety of member functions to play with them. You can read the flags with flags( ), replace them with flags(fmtflags), set selected flags with setf(fmtflags) (Listing 10) , set selected flags under a mask with setf(fmtflags, fmtflags) (Listing 11) , and clear selected flags with unsetf(fmtflags) (Listing 12) .
Finally, you can store your own information in two extensible arrays. To get a unique index (across all ios objects), call the static member xalloc( ). You can then call the member iword(int) with that index to get a reference to an element of a long array, which is initialized to zero. Equally, you can call the member pword(int) with that index to get a reference to an element of a void * array, which is initialized to a null pointer.
Listing 13 shows the critical code, for the common access function _Findarr(int). It locates an existing element or extends the array as needed for a new index. As you can see from the code presented here, elements for these arrays are allocated in tandem and maintained as a linked list. The list is copied by copyfmt and discarded by the destructor (except for the standard streams, of course). Function _Tidy( ), back in Listing 3, performs this cleanup.
Manipulators
That's it for class ios. But you will note that the header <ios> also declares a whole mess of functions called manipulators. I described their uses briefly last month. There are so many, and they are all so similar, that I will show only a representative version here. Listing 14 shows the function dec(ios&), which sets the format flag ios::dec and clears the other flags that affect the conversion base.You can always call dec(ios&) directly, as in:
dec(cin);Or you can call it indirectly as an inserter or extractor, as in:
cin >> dec;But that last expression requires some magic that is not a part of class ios. There's still much more to come with iostreams.
Errata
Two errors have been reported to me (so far) on my column, "Standard C: C++ Language Support Library," CUJ, March 1994. The lesser one is at the end of page 17, where I omitted the empty brackets in the declarations of the global array operators. They should read:
void *operator new[](size_T); void operator delete[](void *);Worse, near the end of page 16, I suggested that "you can make operator new virtual in the base class and fail to override it in a derived class." It is, of course, insane to think of a virtual operator new for a class. If you declare operator new for a class it is implicitly a static function. (How else can it be called without referring to a particular instance of the class?) It is still true, however, that failing to override such a declaration can result in the static member function being called with an object size argument that is larger than for the base class. That was my point.Thanks to John Dlugosz for being the first to point out these errors.