Chapter 6. Using Virtual Streams

This chapter contains the following sections:

The iostream facility that comes with every C++ compiler is a resource that should be familiar to you as a C++ developer. Among its advantages are type-safe insertion and extraction into and out of streams, extensibility to new types, and transparency to the user of the source and sink of the stream bytes, which are set by the class streambuf.

But the iostream facility suffers from a number of limitations. Formatting abilities are particularly weak; for example, if you insert a double into an ostream, there is no type-safe way to insert it as binary. Furthermore, not all byte sources and sinks fit into the streambuf model. For many protocols, such as XDR, the format is intrinsically wedded to the byte stream and cannot be separated.

The Rogue Wave virtual streams facility overcomes these limitations by offering an idealized model of a stream. No assumptions are made about formatting, or stream models. At the root of the virtual streams class hierarchy is class RWvios. This is an abstract base class with an interface similar to the standard library class ios:

class RWvios{
public:
  virtual int   eof()             = 0;
  virtual int   fail()            = 0;
  virtual int   bad()             = 0;
  virtual int   good()            = 0;
  virtual int   rdstate()         = 0;
  virtual int   clear(int v = 0)  = 0;
};

Classes derived from RWvios will define these functions.

Inheriting from RWvios are the abstract base classes RWvistream and RWvostream. These classes declare a suite of pure virtual functions such as operator<<(), put(), get(), and the like, for all the basic built-in types and arrays of built-in types:

class RWvistream : public RWvios {
public:
  virtual Rwvistream&  operator>>(char&)       = 0;
  virtual Rwvistream&  operator>>(double&)     = 0;
  virtual int          get()                   = 0;
  virtual Rwvistream&  get(char&)              = 0;
  virtual Rwvistream&  get(double&)            = 0;
  virtual Rwvistream&  get(char*, size_t N)    = 0;
  virtual Rwvistream&  get(double*, size_t N)  = 0;
  .
  .
  .
};
 
class RWvostream : public RWvios {
public:
  virtual Rwvostream&  operator<<(char)             = 0;
  virtual Rwvostream&  operator<<(double)           = 0;
  virtual Rwvostream&  put(char)                    = 0;
  virtual Rwvostream&  put(double)                  = 0;
  virtual Rwvostream&  put(const char*, size_t N)   = 0;
  virtual Rwvostream&  put(const double*, size_t N) = 0;
  .
  .
  .
};

Streams that inherit from RWvistream and RWvostream are intended to store built-ins to specialized streams in a format that is transparent to the user of the classes.

The basic abstraction of the virtual streams facility is that built-ins are inserted into a virtual output stream, and extracted from a virtual input stream, without any regard for formatting. In other words, there is no need to pad output with whitespace, commas, or any other kind of formatting. You are effectively telling RWvostream, "Here is a double. Please store it for me in whatever format is convenient, and give it back to me in good shape when I ask for it."

The results are extremely powerful. You can write and use streaming operators without knowing anything about the final output medium or formatting to be used. For example, the output medium could be a disk, memory allocation, or even a network. The formatting could be in binary, ASCII, or network packet. In all of these cases, you use the same streaming operators.

Specializing Virtual Streams

The Rogue Wave classes include four types of classes that specialize RWvistream and RWvostream. The first uses a portable ASCII formatting, the second and third a binary formatting, and the fourth an XDR formatting (eXternal Data Representation, a SunMicrosytems standard):

 

Input class

Output class

Abstract base class

RWvistream

RWvostream

Portable ASCII

RWpistream

RWpostream

Binary

RWbistream

RWbostream

Endian

RWeistream

RWeostream

XDR

RWXDRistream

RWXDRostream

The portable ASCII versions store their inserted items in an ASCII format that escapes special characters (such as tabs, newlines, etc.) in such a manner that they will be restored properly, even under a different operating system. The binary versions do not reformat inserted items, but store them instead in their native format. The endian versions allow for the space and time efficiency of binary format, but can store or retrieve the information in big endian, little endian, or native format. XDR versions send their items to an XDR stream, to be transmitted remotely over a network.

None of these versions retain any state: they can be freely interchanged with regular streams, including XDR. Using them does not lock you into doing all your file I/O with them. For more information, see the respective entries in the Class Reference.

Simple Example

Here's a simple example that exercises RWbostream and RWbistream through their respective abstract base classes, RWvostream and RWvistream:

#include <rw/bstream.h>
#include <rw/cstring.h>
#include <fstream.h>
 
#ifdef __BORLANDC__
# define MODE ios::binary                                   // 1
#else
# define MODE 0
#endif
 
void save(const RWCString& a, RWvostream& v){
  v << a;                    // Save to the virtual output stream
}
 
RWCString recover(RWvistream& v) {
   RWCString dupe;
   v >> dupe;           // Restore from the virtual input stream
   return dupe;
}
 
main(){
   RWCString a("A string with\ttabs and a\nnewline.");
 
   {
     ofstream f("junk.dat", ios::out|MODE);                  // 2
     RWbostream bostr(f);                                    // 3
     save(a, bostr);
   }                                                         // 4
 
   ifstream f("junk.dat", ios::in|MODE);                     // 5
   RWbistream bistr(f);                                      // 6
   RWCString b = recover(bistr);                             // 7
 
   cout << a << endl;  // Compare the two strings            // 8
   cout << b << endl;
   return 0;
}

Program Output::

A string with   tabs and a
newline.
A string with   tabs and a
newline.

The job of function save(const RWCString& a, RWvostream& v) is to save the string a to the virtual output stream v. Function recover(RWvistream&) restores the results. These functions do not know the ultimate format with which the string will be stored. Here are some additional comments on particular lines:

//1, //2 

On these lines, a file output stream f is created for the file junk.dat. The default file open mode for many PC compilers is text, requiring that the explicit flag ios::binary be used to avoid automatic DOS new line conversion[6] .

//3 

On this line, an RWbostream is created from f.

//4 

Because this clause is enclosed in braces { ... }, the destructor for f will be called here. This will cause the file to be closed.

//5 

The file is reopened, this time for input.

//6 

Now an RWbistream is created from it.

//7 

The string is recovered from the file.

//8 

Finally, both the original and recovered strings are printed for comparison.

You could simplify this program by using class fstream, which multiply inherits ofstream and ifstream, for both output and input. A seek to beginning-of-file would occur before reading the results back in. Since some early implementations of seekg() have not proven reliable, the simpler approach was not chosen for this example.

Windows Clipboard and DDE Streambufs

In the previous section, you saw how the virtual streams facility abstracts the formatting of items inserted into the stream. The disposition of the items inserted into the streams has also been made abstract: it is set by the type of streambuf used.

Class streambuf is the underlying sequencing layer of the iostreams facility. It is responsible for producing and consuming sequences of characters. Your compiler comes with several versions. For example, class filebuf ultimately gets and puts its characters to a file. Class strstreambuf gets and puts to memory-based character streams; you can think of it as the iostream equivalent to ANSI-C's sprintf() function. Now Tools.h++ adds two Windows-based extensions:

  • Class RWCLIPstreambuf for getting and putting to the Windows Clipboard;

  • Class RWDDEstreambuf for getting and putting through the Windows Dynamic Data Exchange (DDE) facility.

These classes take care of the details of allocating and reallocating memory from Windows as buffers overflow and underflow. In the case of class RWDDEstreambuf, the associated DDEDATA header is also filled in for you. Any class that inherits from class ios can be used with these streambufs, including the familiar istream and ostream, as well as the Rogue Wave virtual stream classes.

The result is that the same code that is used to store a complex structure to a conventional disk-based file, for example, can also be used to transfer that structure through the DDE facility to another application!

DDE Example

Let's look at a more complicated example of how you might use class RWDDEstreambuf to exchange an RWBinaryTree through the Windows DDE facility. You would use a similar technique for the Windows Clipboard.

#include <rw/bintree.h>
#include <rw/collstr.h>
#include <rw/bstream.h>
#include <rw/winstrea.h>
#include <windows.h>
#include <dde.h>
 
BOOL
PostCollection(HWND hwndServer, WORD cFormat){
   RWBinaryTree sc;                                         // 1
   sc.insert(new RWCollectableString("Mary"));
   sc.insert(new RWCollectableString("Bill"));
   sc.insert(new RWCollectableString("Pierre"));
 
   // Allocate an RWDDEstreambuf and use it to initialize
   // an RWbostream:
   RWDDEstreambuf* sbuf = new RWDDEstreambuf(cFormat,       // 2
                                             FALSE,          // 3
                                             TRUE,           // 4
                                             TRUE);          // 5
   RWbostream bostr( sbuf );                                 // 6
 
   // Store the collection to the RWbostream:
   bostr << sc;                                              // 7
 
   // Lock the output stream, and get its handle:
   HANDLE hDDEData = sbuf->str();                            // 8
 
   // Get an atom to identify the DDE Message:
   ATOM atom = GlobalAddAtom("SortedNames");                 // 9
 
   // Post the DDE response:
   return PostMessage(0xFFFF, WM_DDE_DATA, hwndServer,       //10
                      MAKELONG(hDDEData, atom));
}

In the code above, the large memory model has been assumed. Here's the line-by-line description:

//1 

An RWBinaryTree is built and some items inserted into it.

//2-//5 

An RWDDEstreambuf is allocated. The constructor takes several arguments. The first argument is the Windows Clipboard format. In this example, the format type has been passed in as an argument, but in general, you will probably want to register a format with Windows (using RegisterClipboardFormat()) and use that.

The other arguments have to do with the intricacies of DDE data exchange acknowledgments and memory management. See the Class Reference for the list of arguments; for their meanings, see Petzold (1990), Chapter 17, or the Microsoft Windows Guide to Programming.

//6 

An RWbostream is constructed from the supplied RWDDEstreambuf. We could have used an RWpostream here, but DDE exchanges are done within the same machine architecture so, presumably, it is not worth the extra overhead of using the portable ASCII formats. Nevertheless, note how the disposition of the bytes, which is set by the type of streambuf, is cleanly separated from their formatting, which is set by the type of RWvostream.

//7 

The collection is saved to the RWbostream. Because the streambuf associated with RWbostream is actually an RWDDEstreambuf, the collection is actually being saved to a Windows global memory allocation with characteristic GMEM_DDESHARE. This allocation is resized automatically if it overflows. Like any other strstreambuf, you can change the size of the allocation chunks using member function setbuf().

//8 

The RWDDEstreambuf is locked. Once locked using str(), this streambuf, like any other strstreambuf, cannot be used again. Note, however, that RWDDEstreambuf::str() returns a handle, rather than a char*. The handle is unlocked before returning it.

//9 

An atom is constructed to identify this DDE data.

//10 

The handle returned by RWDDEstreambuf::str(), along with its identifying atom, is posted.

A similar and actually simpler technique can be used for Clipboard exchanges.

Note that there is nothing that constrains you to use the specialized streambufs RWCLIPstreambuf and RWDDEstreambuf with only the Rogue Wave virtual streams facility. You could quite easily use them with regular istreams and ostreams; you just wouldn't be able to set the formatting at run time.

RWAuditStreamBuffer

Classes RWDDEstreambuf and RWCLIPstreambuf specialize streambuf to hand off the characters according to the Windows API. But there are other useful specializations of a streambuf. Class RWAuditStreamBuffer allows you to count the bytes of any stream, while optionally calling a function of your choice for each character. See the code example in the Class Reference.

Recap

In this section, you have seen how an object can be stored to and recovered from a stream without regard for the final destination of the bytes of that stream, whether memory or disk. You have also seen that you need not be concerned with the final formatting of the stream, whether ASCII or binary.

You can also write your own specializing virtual stream class, much like RWpostream and RWpistream. The great advantage of the virtual streams facility is that, if you do write your own specialized virtual stream, you don't have to modify any of the code of the client classes—you just use your stream class as an argument to:

RWvostream& operator<<(RWvostream&, const ClassName&);
RWvistream& operator>>(RWvistream&,       ClassName&);

In addition to storing and retrieving an object to and from virtual streams, all of the classes can store and retrieve themselves in binary to and from an RWFile. This file encapsulates ANSI-C style file I/O. Although more limited in its abilities than stream I/O, this form of storage and retrieval is slightly faster to and from disk because the virtual dispatching machinery is not needed.



[6] With many PC compilers, even ostream::write() and istream::read() perform a text conversion unless the file is opened with the ios::binary flag.