Columns


Standard C/C++

Implementing <ostream>

P.J. Plauger


P.J. Plauger is senior editor of C/C++ Users Journal. He is convener of the ISO C standards committee, WG14, and active on the C++ committee, WG21. His latest books are The Draft Standard C++ Library, and Programming on Purpose (three volumes), all published by Prentice-Hall. You can reach him at pjp@plauger.com.

Introduction <ostream>

This is the last of three installments on the class ostream, defined in the header <ostream> (See "Standard C/C++: The Header <ostream>," CUJ, September 1994, and "Standard C/C++: Inserters," CUJ, October 1994.) So far, I've shown how the draft C++ Standard defines the class and described the various member functions. I conclude this month by showing how I've chosen to implement this class.

The Header File

Listing 1 shows the file ostream, which implements the standard header <ostream>. Its principal business is defining the class ostream, but it also declares the manipulators endl, ends, and flush. The type _Uninitialized and the value _Noinit are a bit of magic needed to properly initialize the standard streams. The macro _HAS_SIGNED_CHAR is not defined for an implementation that (erroneously) fails to treat signed char as a type distinct from char.

Five protected functions with secret names perform formatted output:

I warned of the limitations of using the fprintf functions last month. I use vfprintf anyway because it does so much of the job, and portably in the bargain. To avoid the obvious problems, _Print constructs a format string with no field width and with a bounded precision. Thus, an arbitrary conversion can be stored in a fixed-length buffer. The function _Pad does all the things that vfprintf does poorly, or not at all.

_Print is a specialized version of printf. Its first argument is a pointer to the first element of an array code of four formatting codes:

Now you can understand how most of the formatted output functions work. Here, for example, is the definition of the inserter for type short:

ostream& operator<<(short _X)
   {return (_Print(&"B hoB hxB hd"[_If()], _X)); }
The protected member function _If determines the address within the string literal that is passed to _Printf. If, say, the group basefield has only the format flag hex set, then the call _If() returns 4. The first argument to _Printf begins with the sequence B hx. If the format flag showbase is also set, the resultant format string on the call to vfprintf is then %#hx.

With a bit of study, you should now be able to understand all the inline calls to _Printf within the header <ostream>.

Other Files

Listing 2 shows the file ostream. c. It defines the three functions you are likely to need any time you declare an object of class ostream, its destructor, opfx(), and osfx(). Note the use of the call tie() to access the stored pointer to an object of class ostream, just as in istream::ipfx(fnt). It can be convenient to flush one output stream when inserting in another, just as you often want to flush an output stream before extracting from an associated input stream.

Listing 3 shows the file osipoint.c, which defines the member function operator<<(void *). It is the only scalar formatted output function that I found too elaborate to define inline within the header.

For a description of the macros _TRY_IO_BEGIN and _CATCH_IO_END, see "Standard C/C++: The Header <istream)," CUJ, July 1994. They expand to different styles of exception handling, including none at all. I showed how to use them to write a generic extractor in that installment. The corresponding generic code for an inserter (in this implementation, at least) looks like:

_TRY_I0_BEGIN
   if (opfx())
      <perform any output>
   osfx();
_CATCH_IO_END
Inserting a pointer to void must work in concert with extracting such a pointer. (See "Standard C/C++: Extractors," CUJ, August 1994.) In this implementation, the pointer overlaps an array of unsigned long. The function inserts elements of the array as hexadecimal integers, separated by colons. In the common case where one element of the array suffices, however, the converted pointer contains no colons.

Listing 4 shows the file osprint.c, which defines the member function _Print. It composes the format string for a call to vsprintf which does all the hard work in converting a string or scalar value. The varying length argument list that vsprintf expects is the list assembled for the call to _Printf. The macro _MAX_EXP_DIG specifies the maximum number of digits required to specify a floating-point exponent. The macro _MAX_SIG_DIG specifies the maximum number of significant digits required for any of the floating-point types. These are used, much as in several istream member functions, to define a safe buffer size for the conversion.

Listing 5 shows the file ospad.c, which defines the member functions _Pad and _Pr. The latter computes the bounded precision used by a floating-point conversion, as I indicated earlier. The macro _MAX_SIG_DIG establishes the upper bound, which should be large enough to capture all meaningful precision on a conversion by vfprintf.

Function _Pad is called by _Print to finish a formatted output operation, as I also outlined above. The logic is, to put it mildly, intricate. It must:

To keep the logic more or less readable, the code treats separately each of the five(!) places where fill characters can be injected. It also inserts all characters by calling one of two inline functions local to this translation unit:

The logic is nevertheless intimately dependent on the kinds of character sequences presumably generated by vfprintf.

The remaining ostream member functions are relatively simple, by comparison. Listing 6 shows the file osput.c, which defines the member function put(char). It simply inserts the character argument into the stream buffer, with requisite checking. Note that the character inserters all end up calling put(char) as well.

Listing 7 shows the file osistrin.c, which defines the member function operator<<(const char *). It could have been defined inline within the header, invoking the conversion specification %s, but that just leads to excess wheel spinning. The member function _Pad can perform the only difficult operation — determining how to add any padding at either end of the inserted string.

Listing 8 shows the file osistrea.c, which defines the member function operator<<(streambuf&). It closely resembles two istream member functions, isxtrea.c (August 1994) and isgstrea.c (July 1994). The macro _RERAISE rethrows an exception from within a handler. Once again, the inner macros handle exceptions thrown during insertion, while the outer macros handle exceptions thrown during extraction.

Listing 9 shows the file osflush.c, which defines the member function flush(). And Listing 10 shows the file oswrite.c, which defines the member function write(const char *, int). Both simply call the appropriate streambuf public member function to perform the critical operation.

Finally, three manipulators work only with ostream objects. Listing 11 shows the file endl.c, which defines the function endl. Listing 12 shows the file ends.c, which defines the function ends. And Listing 13 shows the file flush.c, which defines the function flush. All three are straightforward.

[This article is excerpted in part from P.J. Plauger, The Draft Standard C++ Library, (Englewood Cliffs, NJ: Prentice Hall, 1995.]