Personal tools
2.12 Creating New Stream Classes by Derivation
Click on the banner to return to the user guide home page.
2.12 Creating New Stream Classes by Derivation
Sometimes it is useful to derive a stream type from the standard iostreams. This is the case when you want to add data members or functions, or modify the behavior of a stream's I/O operations.
In Section 2.11, we learned that additional data can be added to a stream object by using xalloc(), iword(), and pword(). However, this solution has a certain weakness in that only a pointer to the additional data can be stored and someone else has to worry about the actual memory.
This weakness can be overcome by deriving a new stream type that stores the additional data as a data member. Let's consider again the example of the date inserter and the setfmt manipulator from Section 2.11. Here let's derive a new stream that has an additional data member for storing the format string together with a corresponding member function for setting the date format specification.[41] Again, we will confine the example to the inserter of the date object and omit the extractor. Instead of inserting into an output stream, as we did before, we will now use a new type of stream called odatstream:
date today; odatstream ostr(cout); // _ ostr << setfmt("%D") << today;
In the next sections, we will explore how we can implement such a derived stream type.
2.12.1 Choosing a Base Class
The first question is: Which of the standard stream classes shall be the base class? The answer fully depends on the kind of addition or modification you want to make. In our case, we want to add formatting information, which depends on the stream's character type since the format string is a sequence of tiny characters. As we will see later on, the format string must be expanded into a sequence of the stream's character type for use with the stream's locale. Consequently, a good choice for a base class is class basic_iostream <charT,Traits>, and since we want the format string to impact only output operations, the best choice is class basic_ostream <charT,Traits>.
In general, you choose a base class by looking at the kind of addition or modification you want to make and comparing it with the characteristics of the stream classes.
Choose ios_base if you add information and services that do not depend on the stream's character type.
Choose basic_ios<charT, Traits> if the added information does depend on the character type, or requires other information not available in ios_base, such as the stream buffer.
Derive from the stream classes basic_istream <charT,Traits>, basic_ostream <charT,Traits>, or basic_iostream <charT, Traits> if you want to add or change the input and output operations.
Derive from the stream classes basic_(i/o)fstream <charT,Traits>, or basic_(i/o)stringstream <charT, Traits, Allocator> if you want to add or modify behavior that is file- or string-related, such as the way a file is opened.
Derivations from basic_istream <charT,Traits>, basic_ostream <charT,Traits>, or basic_iostream <charT, Traits> are the most common cases, because you typically want to modify or add input and output operations.
If you derive from ios_base or basic_ios<charT, Traits>, you do not inherit any input and output operations; you do this if you really want to reimplement all of them or intend to implement a completely different kind of input or output operation, such as unformatted binary input and output.
Derivations from file or string streams such as basic_(i/o)fstream <charT,Traits> or basic_(i/o)stringstream <charT, Traits, Allocator> are equally rare, because they make sense only if file- or string-related data or services must be added or modified.
Choose basic_istream <charT,Traits>, basic_ostream <charT,Traits>, or basic_iostream <charT, Traits> as a base class when deriving new stream classes, unless you have good reason not to do so.
2.12.2 Construction and Initialization
All standard stream classes have class basic_ios<charT,Traits> as a virtual base class. In C++, a virtual base class is initialized by its most derived class; i.e., our new odatstream class is responsible for initialization of its base class basic_ios<charT,Traits>. Now class basic_ios<charT,Traits> has only one public constructor, which takes a pointer to a stream buffer. This is because class basic_ios<charT,Traits> contains a pointer to the stream buffer, which has to be initialized when a basic_ios object is constructed. Consequently, we have to figure out how to provide a stream buffer to our base class. Let's consider two options:
Derivation from file stream or string stream classes; i.e., class (i/o)fstream<> or class (i/o)stringstream<>, and
Derivation from the stream classes basic_(i/o)stream<>.
2.12.2.1 Derivation from File Stream or String Stream Classes Like (i/o)fstream<> or (i/o)stringstream<>
The file and string stream classes contain a stream buffer data member and already monitor their virtual base class's initialization by providing the pointer to their own stream buffer. If we derive from one of these classes, we will not provide another stream buffer pointer because it would be overwritten by the file or string stream's constructor anyway. (Remember that virtual base classes are constructed before non-virtual base classes regardless of where they appear in the hierarchy.) Consider:
template <class charT, class Traits=char_traits<charT> > class MyOfstream : public basic_ofstream<charT,Traits> { public: MyOfstream(const char* name) : basic_ios<charT,Traits>(_streambufptr_) , basic_ofstream<charT,Traits>(name) {} // . . . };
The order of construction would be:
basic_ios(basic_streambuf<charT,Traits>*) basic_ofstream(const char*) basic_ostream(basic_streambuf<charT,Traits>*) ios_base()
In other words, the constructor of basic_ofstream overwrites the stream buffer pointer set by the constructor of basic_ios.
To avoid this dilemma, class basic_ios<charT,Traits> has a protected default constructor in addition to its public constructor. This default constructor, which requires a stream buffer pointer, doesn't do anything. Instead, there is a protected initialization function basic_ios<charT,Traits>::init() that can be called by any class derived from basic_ios<charT,Traits>. With this function, initialization of the basic_ios<> base class is handled by the stream class that actually provides the stream buffer--in our example, basic_ofstream<charT,Traits>. It will call the protected init() function:
template <class charT, class Traits=char_traits<charT> > class MyOfstream : public basic_ofstream<charT,Traits> { public: MyOfstream(const char* name) : basic_ofstream<charT,Traits>(name) {} // . . . };
The order of construction and initialization is:
basic_ios() basic_ofstream(const char*) basic_ostream() which calls: basic_ios<charT,Traits>::init(basic_streambuf<charT,Traits>*) ios_base()
2.12.2.2 Derivation from the Stream Classes basic_(i/o)stream<>
The scheme for deriving from the stream classes is slightly different in that you must always provide a pointer to a stream buffer. This is because the stream classes do not contain a stream buffer, as the file or string stream classes do. For example, a class derived from an output stream could look like this:
template <class charT, class Traits=char_traits<charT> > class MyOstream : public basic_ostream<charT,Traits> { public: MyOstream(basic_streambuf<charT,Traits>* sb) : basic_ostream<charT,Traits>(sb) {} // . . . };
There are several ways to provide the stream buffer required for constructing such a stream:
Create the stream buffer independently, before the stream is created. Here is a simple example in which a file buffer is created as a separate object and used by the derived stream:
basic_filebuf<char> strbuf; strbuf.open("/tmp/xxx"); MyOstream<char> mostr(&strbuf); mostr << "Hello world\n";
Take the stream buffer from another stream. In the example below, the stream buffer is "borrowed" from the standard error stream cerr:
MyOstream<char,char_traits<char> > mostr(cerr.rdbuf()); mostr << "Hello world\n";
Remember that the stream buffer is now shared between mostr and cerr (see Section 2.9.2 for details).
Contain the stream buffer in the derived stream, either as a data member or inherited. This option is typically preferred when a new stream buffer type is used along with the new stream type.
2.12.3 The Example
Let's return now to our example, in which we are creating a new stream class by derivation.
2.12.3.1 The Derived Stream Class
Let us derive a new stream type odatstream that has an additional data member fmt_ for storing a date format string, together with a corresponding member function fmt() for setting the date format specification.
template <class charT, class Traits=char_traits<charT> > class odatstream : public basic_ostream <charT,Traits> {l public: odatstream(basic_ostream<charT,Traits>& ostr,const char* fmt = "%x") \\1 : basic_ostream<charT,Traits>(ostr.rdbuf()) { fmt_=new charT[strlen(fmt)]; use_facet<ctype<charT> >(ostr.getloc()). widen(fmt, fmt+strlen(fmt), fmt_); \\2 } basic_ostream<charT,Traits>& fmt(const char* f) \\3 { delete[] fmt_; fmt_=new charT[strlen(f)]; use_facet<ctype<charT> >(os.getloc()). widen(f, f+strlen(f), fmt_); return *this; } charT const* fmt() const \\4 { charT * p = new charT[Traits::length(fmt_)]; Traits::copy(p,fmt_,Traits::length(fmt_)); return p; } ~odatstream() \\5 { delete[] fmt_; } private: charT* fmt_; \\6 template <class charT, class Traits> \\7 friend basic_ostream<charT, Traits> & operator << (basic_ostream<charT, Traits >& os, const date& dat); };
//1 | A date output stream borrows the stream buffer of an already existing output stream, so that the two streams will share the stream buffer.
The constructor also takes an optional argument, the date format string. This is always a sequence of tiny characters. |
//2 | The format string is widened or translated into the stream's character type charT. This is because the format string will be provided to the time facet of the stream's locale, which expects an array of characters of type charT. |
//3 | This version of function fmt() allows you to set the format string. |
//4 | This version of function fmt() returns the current format string setting. |
//5 | The date stream class needs a destructor that deletes the format string. |
//6 | A pointer to the date format specification is stored as a private data member fmt_. |
//7 | The inserter for dates will have to access the date format specification. For this reason, we make it a friend of class odatstream. |
2.12.3.2 The Date Inserter
We would like to be able to insert date objects into all kinds of output streams. And, whenever the output stream is a date output stream of type odatstream, we would like to take advantage of its ability to carry additional information for formatting date output. How can this be achieved?
It would be ideal if the inserter for date objects were a virtual member function of all output stream classes that we could implement differently for different types of output streams. For example, when a date object is inserted into an odatstream, the formatting would use the available date formatting string; when inserted into an arbitrary output stream, default formatting would be performed. Unfortunately, we cannot modify the existing output stream classes, since they are part of a library you will not want to modify.
This kind of problem is typically solved using dynamic casts. Since the stream classes have a virtual destructor, inherited from class basic_ios, we can use dynamic casts to achieve the desired virtual behavior.[42]
Here is the implementation of the date inserter, which is similar to the one in Section 2.7.2. The differences are shaded:
template<class charT, class Traits> basic_ostream<charT, Traits> & operator << (basic_ostream<charT, Traits >& os, const date& dat) { ios_base::iostate err = 0; try { typename basic_ostream<charT, Traits>::sentry opfx(os); if(opfx) { charT* fmt; charT buf[3]; try { \\1 odatstream<charT,Traits>* p = dynamic_cast<odatstream<charT,Traits>*>(&os); \\2 } catch (bad_cast) \\3 { char patt[3] = "%x"; use_facet(os.getloc(), (ctype<charT>*)0).widen(patt,patt+3,buf); } fmt = (p) ? p->fmt_ : buf; \\4 if (use_facet<time_put<charT,ostreambuf_iterator<charT,Traits> > >(os.getloc()) .put(os,os,os.fill(),&dat.tm_date,fmt,fmt+Traits::length(fmt)).failed()) err = ios_base::badbit; os.width(0); } } //try catch(...) { bool flag = FALSE; try { os.setstate(ios_base::failbit); } catch( ios_base::failure ) { flag= TRUE; } if ( flag ) throw; } if ( err ) os.setstate(err); return os; }
//1 | We will perform a dynamic cast in statement //2. A dynamic cast throws an exception in case of mismatch. Naturally, we do not want to confront our user with bad_cast exceptions because the mismatch does not signify an error condition, but only that the default formatting will be performed. For this reason, we will try to catch the potential bad_cast exception. |
//2 | This is the dynamic cast to find out whether the stream is a date stream or any other kind of output stream. |
//3 | In case of mismatch, we prepare the default date format specification "%x". |
//4 | If the stream is not of type odatstream, the default format specification prepared in the catch clause is used. Otherwise, the format specification is taken from the private data member fmt_. |
2.12.3.3 The Manipulator
The date output stream has a member function for setting the format specification. Analogous to the standard stream format functions, we would like to provide a manipulator for setting the format specification. This manipulator will affect only output streams. Therefore, we have to define a manipulator base class for output stream manipulators, osmanip, along with the necessary inserter for this manipulator. We do this in the code below. See Section 2.8 for a detailed discussion of the technique we are using here:
template <class Ostream, class Arg> class osmanip { public: typedef Ostream ostream_type; typedef Arg argument_type; osmanip(Ostream& (*pf)(Ostream&, Arg), Arg arg) : pf_(pf) , arg_(arg) { ; } protected: Ostream& (*pf_)(Ostream&, Arg); Arg arg_; friend Ostream& operator<< (Ostream& ostr, const osmanip<Ostream,Arg>& manip); }; template <class Ostream, class Arg> Ostream& operator<< (Ostream& ostr,const osmanip<Ostream,Arg>& manip) { (*manip.pf_)(ostr,manip.arg_); return ostr; }
After these preliminaries, we can now implement the setfmt manipulator itself:
template <class charT, class Traits> inline basic_ostream<charT,Traits>& sfmt(basic_ostream<charT,Traits>& ostr, const char* f) \\1 { try { \\2 odatstream<charT,Traits>* p = dynamic_cast<odatstream<charT,Traits>*>(&ostr); } catch (bad_cast) \\3 { return ostr; } p->fmt(f); \\4 return ostr; } template <class charT,class Traits> inline osmanip<basic_ostream<charT,Traits>,const char*> setfmt(const char* fmt) { return osmanip<basic_ostream<charT,Traits>,const char*>(sfmt,fmt); } \\5
//1 | The function sfmt() is the function associated with the setfmt manipulator. Its task is to take a format specification and hand it over to the stream. This happens only if the stream is a date output stream; otherwise, nothing is done. |
//2 | We determine the stream's type through a dynamic cast. As it would be rather drastic to let a manipulator call result in an exception thrown, we catch the potential bad_cast exception. |
//3 | In case of mismatch, we don't do anything and simply return. |
//4 | In case the stream actually is a date output stream, we store the format specification by calling the stream's fmt() function. |
//5 | The manipulator itself is a function that creates an output manipulator object. |
2.12.3.4 A Remark on Performance
The solution suggested in the previous Sections 2.12.3.2 and 2.12.3.3 uses dynamic casts and exception handling to implement the date inserter and the date format manipulator. Although this technique is elegant and makes proper use of the C++ language, it might introduce some loss in runtime performance due to the use of exception handling. This is particularly true as the dynamic cast expression, and the exception it raises, is used as a sort of branching statement. In other words, the "exceptional" case occurs relatively often and is not really an exception.
If optimal performance is important, you can choose an alternative approach: in the proposed solution that uses dynamic casts, extend the date inserter for arbitrary output streams basic_ostream<charT,Traits>& operator<< (basic_ostream <charT,Traits>&, const date&) so that it formats dates differently, depending on the type of output stream. Alternatively, you can leave the existing date inserter for output streams unchanged and implement an additional date inserter that works for output date streams only; its signature would be odatstream<charT,Traits>& operator<< (odatstream<charT,Traits>&, const date&). Also, you would have two manipulator functions, one for arbitrary output streams and one for output date streams only, that is, basic_ostream<charT,Traits>& sfmt (basic_ostream<charT,Traits>&, const char*) and odatstream<charT,Traits>& sfmt (odatstream<charT,Traits>&, const char*). In each of the functions for date streams, you would replace those operations that are specific for output date streams.
This technique has the drawback of duplicating most of the inserter's code, which in turn might introduce maintenance problems. The advantage is that the runtime performance is likely to be improved.
2.12.4 Using iword/pword for RTTI in Derived Streams
In the previous section, we discussed an example that used runtime-type identification (RTTI) to enable a given input or output operation to adapt its behavior based on the respective stream type's properties.
Before RTTI was introduced into the C++ language in the form of the new style casts dynamic_cast<>, the problem was solved using iword(), pword(), and xalloc() as substitutes for runtime-type identification (RTTI).[43] We describe this old-fashioned technique only briefly because, as the previous example suggests, the use of dynamic casts is clearly preferable over the RTTI substitute. Still, the traditional technique might be useful if your current compiler does not yet support the new-style casts.
The basic idea of the traditional technique is that the stream class and all functions and classes that need the runtime type information, like the inserter and the manipulator function, agree on two things:
An index into the arrays for additional storage; in other words, Where do I find the RTTI?, and
The content or type identification that all concerned parties expect to find there; in other words, What will I find?
In the sketch below, the derived stream class reserves an index into the additional storage. The index is a static data member of the derived stream class, and identifies all objects of that stream class. The content of that particular slot in the stream's additional storage, which is accessible through pword(), is expected to be the respective stream object's this pointer.
Here are the modifications to the derived class odatstream:
template <class charT, class Traits=char_traits<charT> > class odatstream : public basic_ostream <charT,Traits> { public: static int xindex() \\1 { static int inited = 0; static int value = 0; if (!inited) { value = xalloc(); inited++; } return value; } odatstream(basic_ostream<charT,Traits>& ostr,const char* fmt = "%x") : basic_ostream<charT,Traits>(ostr.rdbuf()) { pword(xindex()) = this; \\2 fmt_=new charT[strlen(fmt)]; use_facet<ctype<charT> >(ostr.getloc()).widen(fmt, fmt+strlen(fmt), fmt_); } // _ other member, as in the previous section _ };
//1 | The static function xindex() is concerned with reserving the index into the arrays for additional storage. It also serves as the access function to the index. |
//2 | The reserved slot in the arrays for additional storage is filled with the object's own address. |
Here are the corresponding modifications to the manipulator:
template <class charT, class Traits> inline basic_ostream<charT,Traits>& sfmt(basic_ostream<charT,Traits>& ostr, const char* f) { if (ostr.pword(odatstream<charT,Traits>::xindex()) == &ostr) \\1 ((odatstream<charT,Traits>&)ostr).fmt(f); return ostr; }
//1 | The manipulator function checks whether the content of the reserved slot in the stream's storage is the stream's address. If it is, the stream is considered to be a date output stream. |
Note that the technique described in this section is not safe. There is no way to ensure that date output streams and their related functions and classes are the only ones that access the reserved slot in a date output stream's additional storage. In principle, every stream object of any type can access all entries through iword() or pword(). It's up to your programming discipline to restrict access to the desired functions. It is unlikely, however, that all streams will make the same assumptions about the storage's content. Instead of agreeing on each stream object's address as the run-time-type identification, we also could have stored certain integers, pointers to certain strings, etc. Remember, it's the combination of reserved index and assumed content that represents the RTTI substitute.
©Copyright 1996, Rogue Wave Software, Inc.