Chapter 17. Advanced Topics

You'll probably come to this section on Advanced Topics after you've had some experience with Tools.h++. You don't need all the information contained here to start using the library effectively, but you should read it to expand your knowledge of specific topics, to see how we've implemented various product features, and to check for new information.

Dynamic Link Library

The Tools.h++ Class Library can be linked as a Microsoft Windows 3.X Dynamic Link Library (DLL). In a DLL, linking occurs at run time when the routine is actually used. This results in much smaller executables because routines are pulled in only as needed. Another advantage is that many applications can share the same code, rather than duplicating it in each application.

Because Windows and C++ technology is evolving so rapidly, be sure to check the file TOOLDLL.DOC on your distribution disk for more updates.

The DLL Example

This section discusses a sample Windows application, DEMO.CPP, that uses the Tools.h++ DLL. The code for this program can be found in the subdirectory DLLDEMO. This program falls into the familiar category of "Hello World" examples.

Figure 17-1. The Demo Program Window

Figure 18-1 The Demo Program Window

The program is somewhat unusual, however, in that it maintains a linked list of Drawable objects you can insert into the window at run time. You can find the list, which is implemented using class RWSlistCollectables, in the subdirectory DLLDEMO. The discussion in this section assumes that you are somewhat familiar with Windows 3.X programming, but not with its relationship to C++.

The DEMO.CPP Code

Here's the main program of DEMO.CPP, the sample Windows application that uses the Tools.h++ DLL.

/*
 * Sample Windows 3.X program, using the Tools.h++ DLL.
 */
 
#include "demowind.h"
 
int PASCAL
WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
        LPSTR , int nCmdShow)                                // 1
{  // Create an instance of the "DemoWindow" class:
   DemoWindow ww(hInstance, hPrevInstance, nCmdShow);        // 2
 
   // Add some items to it:
   ww.insert( new RWRectangle(200, 50, 250, 100));           // 3
   ww.insert( new RWEllipse(50, 50, 100, 100));
   ww.insert( new RWText(20, 20, "Hello world, from Rogue 
              Wave!"));
 
   ww.show();      // Show the window                        // 4
   ww.update();    // Update the window
 
   // Enter the message loop:
   MSG msg;
   while( GetMessage( &msg, NULL, 0, 0))                     // 5
   {
     TranslateMessage( &msg );
     DispatchMessage( &msg );
   }
 
   return msg.wParam;                                        // 6
}

Here's the line-by-line description of the program.

//1 

This is the Windows program entry point that every Windows program must have. It is equivalent to C's main function.

//2 

This creates an instance of the class DemoWindow, which we will see later. It represents an abstract window into which objects can be inserted.

//3 

Here we insert three different objects into the DemoWindow: a rectangle, an ellipse, and a text string.

//4 

This tells the DemoWindow to show itself and to update itself.

//5 

Finally, the windows main event loop is entered.

//6 

The DemoWindow destructor frees all memory allocated for its window objects.

DEMOWIND.H

This header file declares the class DemoWindow. A key feature is the singly linked list called myList, which holds the list of items that have been inserted into the window. The member function DemoWindow::insert(RWDrawable*) allows new items to be inserted. Only objects that inherit from class RWDrawable, to be defined in "An Excerpt from SHAPES.H," may be inserted.

The member function paint() may be called when it is time to repaint the window. The list myList will be traversed and each item in it will be repainted onto the screen.

Here's a listing of the DEMOWIND.H file

#ifndef __DEMOWIND_H__
#define __DEMOWIND_H__
 
/*
 * A Demonstration Window class --- allows items that
 * inherit from the base class RWDrawable to be
 * "inserted" into it.
 */
 
#include <windows.h>
#include <rw/slistcol.h>
#include "shapes.h"
 
class DemoWindow {
  HWND                  hWnd;              // My window handle
  HINSTANCE             myInstance;       // My instance's handle
  int                   nCmdShow;
  RWSlistCollectables   myList;  // A list of items in the window
 
public:
 
  DemoWindow(HINSTANCE mom, HINSTANCE prev, int);
  ~DemoWindow();
 
    void    insert(RWDrawable*);    // Add a new item to the window
 
  HWND     handle() {return hWnd;}
  int      registerClass();            // First time registration
  void     paint();                 // Called by Windows procedure
  int      show() {return ShowWindow(hWnd,nCmdShow);}
  void     update() {UpdateWindow(hWnd);}
 
  friend long FAR PASCAL _export
         DemoWindow_Callback(HWND, UINT, WPARAM, LPARAM);
};
 
DemoWindow*  RWGetWindowPtr(HWND h);
void         RWSetWindowPtr(HWND h, DemoWindow* p);
 
#endif
 

The demowind.cpp File

Now let's look at the definitions of the public functions of class DemoWindow: the DemoWindow constructor, the DemoWindow destructor, and the member functions insert(), registerClass(), and paint(). Detailed comments follow this listing.

#include <windows.h>
#include <rw/vstream.h>
#include "demowind.h"
#include "shapes.h"
#include <stdlib.h>
#include <string.h>
 
/*
 * Construct a new window.
 */
DemoWindow::DemoWindow( HINSTANCE mom, HINSTANCE prev,
                        int cmdShow)                          //1
{
  myInstance = mom;
  nCmdShow = cmdShow;
 
  // Register the class if there was no previous instance:
  if(!prev) registerClass();                                  //2
 
  hWnd = CreateWindow("DemoWindow",                           //3
                      "DemoWindow",
                      WS_OVERLAPPEDWINDOW,
                      CW_USEDEFAULT, 0,
                      CW_USEDEFAULT, 0,
                      NULL,
                      NULL,
                      myInstance,
                     (LPSTR)this );     // Stash away 'this'  //4
 
  if(!hWnd) exit( FALSE );
}
 
/*
 * Register self. Called once only.
 */
int
DemoWindow::registerClass(){                                   //5
   WNDCLASS wndclass;
   wndclass.style = CS_HREDRAW | CS_VREDRAW;
   wndclass.lpfnWndProc = ::DemoWindow_Callback;              //6
   wndclass.cbClsExtra = 0;
   // Request extra space to store the 'this' pointer
   wndclass.cbWndExtra = sizeof(this);                        //7
   wndclass.hInstance = myInstance;
   wndclass.hIcon = 0;
   wndclass.hCursor = LoadCursor( NULL, IDC_ARROW );
   wndclass.hbrBackground = GetStockObject( WHITE_BRUSH );
   wndclass.lpszMenuName = NULL;
   wndclass.lpszClassName = "DemoWindow";
 
   if( !RegisterClass(&wndclass) ) exit(FALSE);
   return TRUE;
}
 
DemoWindow::~DemoWindow(){
   // Delete all items in my list:
   myList.clearAndDestroy();                                  //8
}
 
void
DemoWindow::insert(RWDrawable* d){
   // Add a new item to the window:
   myList.insert(d);                                          //9
}
 
void
DemoWindow::paint(){                                          //10
   RWDrawable* shape;
   PAINTSTRUCT ps;
   BeginPaint( handle(), &ps);                                //11
 
   // Draw all items in the list.  Start by making an iterator:
   RWSlistCollectablesIterator next(myList);                  //12
 
   // Now iterate through the collection, drawing each item:
   while( shape = (RWDrawable*)next() )                       //13
     shape->drawWith(ps.hdc);                                 //14
 
   EndPaint( handle(), &ps );                                 //15
}
 
/*
 * The callback routine for this window class.
 */
long FAR PASCAL _export
DemoWindow_Callback(HWND hWnd, unsigned iMessage,             //16
                    WPARAM wParam, LPARAM lParam)
{
   DemoWindow* pDemoWindow;
 
   if( iMessage==WM_CREATE ){                                 //17
     // Get the "this" pointer out of the create structure 
     // and put it in the windows instance:
     LPCREATESTRUCT lpcs = (LPCREATESTRUCT)lParam;
     pDemoWindow = (DemoWindow*) lpcs->lpCreateParams;
     RWSetWindowPtr(hWnd, pDemoWindow);
     return NULL;
  }
 
   // Get the appropriate "this" pointer
   pDemoWindow = RWGetWindowPtr(hWnd);                        //18
 
   switch( iMessage ){
     case WM_PAINT:
       pDemoWindow->paint();                                  //19
       break;
     case WM_DESTROY:
       PostQuitMessage( 0 );
       break;
     default:
       return DefWindowProc(hWnd, iMessage, wParam, lParam);
  };
   return NULL;
}
 
void RWSetWindowPtr(HWND h, DemoWindow* p){                   //20
   SetWindowLong(h, 0, (LONG)p);
}
 
DemoWindow* RWGetWindowPtr(HWND h){                           //21
   return (DemoWindow*)GetWindowLong(h, 0);
}

//1 

This is the constructor for DemoWindow. It requires the handle of the application instance creating it, mom, the handle of any previously existing instance, prev, and whether to show the window in iconic form. These variables are as received from WinMain, the main windows procedure that we have already seen.

//2 

The constructor checks to see if any previous application instance has been run and, if not, registers the class.

//3 

The new window is created.

//4 

A key feature is the use of the Windows extra data feature to store the this pointer for this DemoWindow. The procedure to do this is somewhat cumbersome, but very useful. The value placed here will appear in the CREATESTRUCT structure, which we can retrieve when processing the WM_CREATE message generated by the CreateWindow function.

//5 

This member function is only called for the first instance of an application.

//6 

The global function DemoWindow_Callback is registered as the callback procedure for this window.

//7 

We ask Windows to set aside space for the this pointer in this Windows class. This will be used to associate a Windows handle with a particular DemoWindow.

//8 

The destructor calls clearAndDestroy() to delete all items that have been inserted into myList.

//9 

The member function insert(RWDrawable*) inserts a new item into the window. Because RWDrawable inherits from RWCollectable, as we shall see in "An Excerpt from SHAPES.H," there is no need to do a cast when calling RWSlistCollectables::insert(RWCollectable*). By making myList private and offering a restricted insert that takes arguments of type RWDrawable* only, we ensure that only drawables will be inserted into the window.

//10 

Here's the paint procedure, called when it is time to repaint the window.

//11 

We start by getting the handle for this window, then calling BeginPaint to fill a PAINTSTRUCT with information about the painting.

//12 

An iterator is constructed in preparation for traversing the list of items to be painted.

//13 

Get the next item to be painted. If nil is returned, then there are no more items and the while loop will finish.

//14 

For each item, call the virtual function drawWith, causing the item to paint itself onto the given device context.

//15 

The PAINTSTRUCT is returned.

//16 

Here's the callback routine to be called when it is necessary to process a message for this window. It uses the very convenient _export keyword of Borland C++ to indicate that this function should be exported. If you use this procedure, you don't have to list the function in an Exports section of the module definition file.

//17 

If the message is a WM_CREATE message, it was generated by the CreateWindow function and this is the first time through for this procedure. Use the rather baroque procedure to fetch the this pointer from the CREATESTRUCT (recall it had been inserted at line 4), and put it into the Windows extra data. The function RWSetWindowPtr will be defined later.

//18 

The function RWGetWindowPtr(HWND) is used to retrieve the pointer to the appropriate DemoWindow, given a Windows HANDLE.

//19 

If a WM_PAINT has been retrieved, then call the paint() member function, which we have already seen.

//20 

This function is used to put a this pointer into the Windows extra data. The idea is to have a one-to-one mapping of Windows handles to instances of DemoWindow.

//21 

This function is used to fetch the this pointer back out.

An Excerpt from SHAPES.H

This section deals with the subclasses of RWDrawable. Class RWDrawable is an abstract base class that inherits from the Tools.h++ class RWCollectable, and adds a new member function drawWith(HDC). Subclasses specializing RWDrawable should implement this function to draw itself onto the supplied device context handle.

We have reprinted only one subclass here, RWRectangle. Class RWRectangle inherits from RWDrawable. Note that it uses the struct RECT provided by Windows as member data to hold the corners of the rectangle.

class RWDrawable : public RWCollectable{
public:
  virtual void  drawWith(HDC) const = 0;
};
 
class RWRectangle : public RWDrawable{
  RWDECLARE_COLLECTABLE(RWRectangle)
 
public:
  RWRectangle() { }
  RWRectangle(int, int, int, int);
 
  // Inherited from RWDrawable:
  virtual void  drawWith(HDC) const;
 
  // Inherited from RWCollectable:
  virtual Rwspace    binaryStoreSize() const 
                        {return 4*sizeof(int);}
  virtual unsigned   hash() const;
  virtual RWBoolean  isEqual(const RWCollectable*) const;
  virtual void       restoreGuts(RWvistream& s);
  virtual void       restoreGuts(RWFile&);
  virtual void       saveGuts(RWvostream& s) const;
  virtual void       saveGuts(RWFile&) const;
 
private:
 
  RECT  bounds;                     // The bounds of the rectangle
};
 

An Excerpt from SHAPES.CPP

For the purposes of this DLL demo, it really isn't necessary to provide definitions for any of the member functions inherited from RWCollectable, but let's do it anyway, for the sake of completeness.

#include "shapes.h"
#include <rw/vstream.h>
#include <rw/rwfile.h>
 
RWDEFINE_COLLECTABLE(RWRectangle, 0x1000)                     //1
 
// Constructor
RWRectangle::RWRectangle(int l, int t, int r, int b)          //2
{
   bounds.left   = l;
   bounds.top    = t;
   bounds.right  = r;
   bounds.bottom = b;
}
 
// Inherited from Drawable:
void
RWRectangle::drawWith(HDC hdc) const                           //3
{
   // Make the Windows call:
   Rectangle(hdc, bounds.left, bounds.top,
             bounds.right, bounds.bottom);
}
 
// Inherited from RWCollectable:
unsigned
RWRectangle::hash() const                                      //4
{
  return bounds.left ^ bounds.top ^ bounds.bottom ^ bounds.right;
}
 
 
RWBoolean
RWRectangle::isEqual(const RWCollectable* c) const             //5
{
   if(c->isA() != isA() ) return FALSE;
 
   const RWRectangle* r = (const RWRectangle*)c;
 
   return   bounds.left   == r->bounds.left  &&
            bounds.top    == r->bounds.top   &&
            bounds.right  == r->bounds.right &&
            bounds.bottom == r->bounds.bottom;
}
 
// Restore the RWRectangle from a virtual stream:
void
RWRectangle::restoreGuts(RWvistream& s)                        //6
{
   s >> bounds.left  >> bounds.top;
   s >> bounds.right >> bounds.bottom;
}

// Restore from an RWFile:
void
RWRectangle::restoreGuts(RWFile& f)                            //7
{
   f.Read(bounds.left);
   f.Read(bounds.top);
   f.Read(bounds.right);
   f.Read(bounds.bottom);
}
 
void
RWRectangle::saveGuts(RWvostream& s) const                     //8
{
   s << bounds.left  << bounds.top;
   s << bounds.right << bounds.bottom;
}
 
void
RWRectangle::saveGuts(RWFile& f) const                         //9
{
   f.Write(bounds.left);
   f.Write(bounds.top);
   f.Write(bounds.right);
   f.Write(bounds.bottom);
}
 

//1 

This is a macro that all subclasses of RWCollectable are required to compile once, and only once. See "How to Create an RWCollectable Object" in Chapter 15 for more details.

//2 

This is the constructor for RWRectangle that fills in the RECT structure.

//3 

This is the definition of the virtual function drawWith(HDC). It simply makes a call to the Windows function Rectangle() with appropriate arguments.

//4 

Supplies an appropriate hashing value in case we ever want to retrieve this RWRectangle from a hash table.

//5 

Supplies an appropriate isEqual() implementation.

//6 

This function retrieves the RWRectangle from a virtual stream.

//7 

This function retrieves the RWRectangle from an RWFile.

//8 

This function stores the RWRectangle on a virtual stream. Note how there is no need to separate elements by white space—this will be done by the virtual stream, if necessary.

//9 

This function stores the RWRectangle on an RWFile.

The other shapes, RWEllipse and RWText, are implemented in a similar manner.

Copy on Write

Classes RWCString, RWWString, and RWTValVirtualArray<T> use a technique called copy on write to minimize copying. This technique offers the advantage of easy-to-understand value semantics with the speed of reference counted pointer implementation.

Here is how the technique works. When an RWCString is initialized with another RWCString via the copy constructor:

RWCString(const RWCString&);

the two strings share the same data until one of them tries to write to it. At that point, a copy of the data is made and the two strings go their separate ways. Copying only at "write" time makes copies of strings, particularly read-only copies, very inexpensive. In the following example, you can see how four objects share one copy of a string until one of the objects attempts to change the string:

#include <rw/cstring.h>
 
RWCString g;                                      // Global object
void setGlobal(RWCString x) { g = x; }
main(){
  RWCString a("kernel");                                     // 1
  RWCString b(a);                                            // 2
  RWCString c(a);                                            // 3
 
  setGlobal(a);       // Still only one copy of "kernel"!    // 4
 
  b += "s";           // Now b has its own data: "kernels"   // 5
}
 

//1 

The actual allocation and initialization of the memory to hold the string kernel occurs at the RWCString object a.

//2-//3 

When objects b and c are created from a, they merely increment a reference count in the original data and return. At this point, there are three objects looking at the same piece of data.

//4 

The function setGlobal() sets the value of g, the global RWCString, to the same value. Now the reference count is up to four, and there is still only one copy of the string kernel.

//5 

Finally, object b tries to change the value of the string. It looks at the reference count and sees that it is greater than one, implying that the string is being shared by more than one object. At this point, a clone of the string is made and modified. The reference count of the original string drops back down to three, while the reference count of the newly cloned string is one.

A More Comprehensive Example

Because copies of RWCStrings are so inexpensive, you are encouraged to store them by value inside your objects, rather than storing a pointer. This will greatly simplify their management, as the following comparison demonstrates. Suppose you have a window whose background and foreground colors can be set. A simple-minded approach to setting the colors would be to use pointers as follows:

class SimpleMinded {
  const RWCString*  foreground;
  const RWCString*  background;
public:
  setForeground(const RWCString* c) {foreground=c;}
  setBackground(const RWCString* c) {background=c;}
};

On the surface, this approach is appealing because only one copy of the string need be made. In this sense, calling setForeground() seems efficient. However, a closer look indicates that the resulting semantics can be muddled: what if the string pointed to by foreground changes? Should the foreground color change? If so, how will class Simple know of the change? There is also a maintenance problem: before you can delete a color string, you must know if anything is still pointing to it.

Here's a much easier approach:

class Smart {
  RWCString  foreground;
  RWCString  background;
public:
  setForeground(const RWCString& c) {foreground=c;}
  setBackground(const RWCString& c) {background=c;}

Now the assignment foreground=c will use value semantics. The color that class Smart should use is completely unambiguous. Copy on write makes the process efficient, too, since a copy of the data will not be made unless the string should change. The next example maintains a single copy of white until white is changed:

Smart window;
RWCString color("white");
 
window.setForeground(color);            // Two references to white
 
color = "Blue";               // One reference to white, one to blue

RWStringID

Many Rogue Wave clients have asked for a larger range of possible class identifiers for RWCollectable classes than is available using RWClassID. We did not change the meaning of RWClassID, in order to preserve backward compatibility for existing polymorphically persisted files, but we did add a new kind of class identifier, RWStringID.

An RWStringID is an identifier for RWCollectables in Tools.h++ Version 7 and later. It is derived from RWCString, and may be manipulated by any of the const RWCString methods. The non-const methods have been hidden to prevent the disaster that could occur if the RWStringID of a class changed at run time.

You can associate an RWStringID with an RWCollectable class in one of two ways: pick the RWStringID for the class, or allow the library to automatically generate an RWStringID that is the same sequence of characters as the name of the class; for example, class MyColl : public RWCollectable would get the automatic RWStringID"MyColl".

You specify a class with a fixed RWClassID and generated RWStringID by using the macro RWDEFINE_COLLECTABLE as follows:

RWDEFINE_COLLECTABLE(ClassName, ClassID)
RWDEFINE_COLLECTABLE(MyCollectable1,0x1000)          //for example

You specify a class with a fixed RWStringID and a generated RWClassID by using the new macro RWDEFINE_NAMED_COLLECTABLE as follows:

RWDEFINE_NAMED_COLLECTABLE(ClassName, StringID) 
RWDEFINE_NAMED_COLLECTABLE(MyCollectable2, "Second Collectable") // for example

Using the examples above, you could write:

// First set up the experiment
MyCollectable1 one; MyCollectable2 two;
// All running RWClassIDs are guaranteed distinct
one.isA() != two.isA();
// Every RWCollectable has an RWStringID
one.stringID() == "MyCollectable1"; 
// There are several ways to find ids
RWCollectable::stringID(0x1000) == "MyCollectable1";
two.isA() == RWCollectable::classID("Second Collectable");

Duration of Identifiers

Providing polymorphic persistence between different executions of the same or different programs requires a permanent identifier for each class being persisted. Until now, the permanent identifier for any RWCollectable has been its RWClassID. For each class that derives from RWCollectable, the macro RWDEFINE_COLLECTABLE caused code to be generated that forever associated the class and its RWClassID. This identification has been retained, but in the current version of Tools.h++ you may choose the RWDEFINE_NAMED_COLLECTABLE macro, which will permanently link the chosen RWStringID with the class.

The addition of RWStringID identifiers will result in more identifiers, and more self-documenting RWCollectable identifiers, than were possible under the old restriction. To accommodate the new identifiers, a temporary RWClassID is now generated for each RWCollectable class that has an RWStringID specified by the developer. These RWClassIDs are built as needed during the run of an executable, and remain constant throughout that run. However, they may be generated in a different order on a different executable or during a different run, so they are not suitable for permanent storage.

Programming with RWStringIDs

RWCollectable now has a new regular member function, and two new static member functions. Since one of the major goals of Version 7 of Tools.h++ is to maintain link compatibility with objects compiled against Version 6, none of these functions is virtual. The functions are therefore slightly less efficient than they would be if we broke link-compatibility.

The new regular member function is:

RWStringID  stringID() const;

The new static member functions are:

RWStringID  stringID(RWClassID);             // looks up RWStringID
RWClassID  classID(RWStringID);              // looks up classID 

RWFactory also includes the following new functions:

void           addFunction(RWuserCreator, RWClassID, RWStringID);
RWCollectable* create(RWStringID) const; 
RWuserCreator  getFunction(RWStringID) const; 
void           removeFunction(RWStringID); 
RWStringID     stringID(RWClassID) const; 
RWClassID      classID(RWStringID) const; 

You can use RWCollectables that ship with Tools.h++ and RWCollectables that have been defined with fixed RWClassIDs exactly as in previous versions of Tools.h++. For instance, you could still use this common programming idiom:

RWCollectable *ctp;                         // assign the pointer
if (ctp->isA() == SOME_CONST_CLASSID)      // do a specific thing

However, when you use RWCollectables that have user-provided RWStringIDs, which implies any non-permanent ClassIDs, you must anticipate that the RWClassID may have different values during different runs of the executable. For these classes, there are two possible idioms to replace the one above:

RWCollectable *ctp; 
// assign the pointer somehow 
// use with existing RWCollectable for comparison: 
// comparison will be faster than comparing RWStringIDs
if(ctp->isA() == someRWCollectablePtr->isA()) 
  // you may code to that class interface 
// ... 
// idiom to hard code the identification. Slightly 
// slower because string comparisons are slower than int 
// comparisons; also stringID() uses a dictionary lookup. 
if (ctp->stringID() == "Some ID String")  {
  // you may code to that class interface 
} 

Implementation Details of RWStringID

The next few sections cover implementation details of RWStringID. If you are curious about how we manage to provide virtual functionality without adding virtual methods, or if you are interested in issues of design, efficiency, and other specifics, these sections are for you.

Automatic RWClassIDs

Automatic RWClassIDs are created in a systematic way from unused RWClassIDs in the range 0x9200 to 0xDAFF. There are 18,687 possible such RWClassIDs, so only extraordinary programs can possibly run out. However, we are used to dealing with extraordinary customers, so we feel we must warn you: you will not be able to build and use more than 18,687 different classes with automatically generated RWClassIDs in any one program.

Note that this implies nothing about the total number of objects of each class that you may have. That number is limited only by the requirements of your operating system and compiler. Of course, you also have access to the full set of RWClassIDs below 0x8000—that is, 32767 more possible RWCollectables—but they will not be automatically generated. You must specify them manually.

mplementing Virtuals Via Statics

Since the virtual method isA() returns a "run-time unique" RWClassID, we can use this one virtual method to provide an index into a lookup table where various data or function pointers are stored. (This may remind you of C++ built-in vtables!) Since RWCollectables already depend on the existence of a single RWFactory, we chose to use that RWFactory instance to hold the lookup information.

The static method:

RWStringID  RWCollectable::stringID(RWClassID id);

will attempt to look up id in the RWFactory instance. If it succeeds in finding an associated RWStringID, it will return it. Otherwise, it will return RWStringID("NoID").

The static method:

RWClassID  RWCollectable::classID(RWStringID sid)

works in an analogous manner, looking in the RWFactory instance to see if there is an RWClassID associated with sid. If the method finds one, it returns it; otherwise, it returns RWClassID__RWUNKNOWN.

Polymorphic Persistence

Polymorphic persistence of RWCollectables is not affected by the addition of the new class RWStringID. Existing files can still be read using newly compiled and linked executables, as long as the old RWClassIDs are unchanged. New classes that have RWStringIDs may be freely intermixed with old classes. The storage size of collectables that do not have permanent RWClassIDs will reflect their larger space requirements, but the store size of other RWCollectables will be unaffected.

Note that collections containing RWCollectables with the same RWStringID have that RWStringID stored into a stream or file only once, just as multiple references to the same RWCollectable are only stored the first time they are seen.

Efficiency

Since RWClassID is more efficient in both time and space than RWStringID, you may wish to continue using it wherever possible. RWStringIDs are useful:

  • For organizations that need to generate unique identifiers for many programming groups;

  • For third party libraries that need to avoid clashes with other libraries or users;

  • Anywhere the self-documenting feature of RWStringID adds enough value to compensate for its slight inefficiencies.

RWStringIDs are generated for all RWCollectable classes that are compiled under the current version of Tools.h++. This additional code generation has only minor impact on programs that do not use the RWStringIDs. The RWFactory will be larger, to hold lookups from RWClassID and RWStringID; and startup time will be very slightly longer, to accommodate the addition of the extra data to the RWFactory.

Identification Collisions

While RWStringID can help alleviate identification collisions, the possibility of collisions between RWStringIDs of different classes still exists. Collisions can occur:

  • When an automatically generated RWStringID conflicts with a user-chosen one;

  • When one or more classes are accidentally assigned the same RWStringID;

  • When two classes in different namespaces have the same name and thus the same automatically generated RWStringID. This assumes your compiler supports namespaces.

In some cases, collisions like these will be unimportant. Automatically generated RWClassIDs are guaranteed to be distinct from one another and from any legal user-provided RWClassID. The virtual isA() method, the stringID() method, and constructor lookup based on the RWClassID will all continue to work correctly.

There will be some situations, however, where collisions will cause difficulty. Polymorphic persistence of classes with user-chosen RWStringIDs that collide will not work correctly. In these cases, the data will not be recoverable, even though it is stored correctly. Similarly, user code that depends on distinguishing between classes based only on their RWStringIDs will fail.

As a developer, you can work to avoid such collisions. First of all, you should use an RWStringID which is unlikely to collide with any other. For instance, you might choose RWStringIDs that mimic the inheritance hierarchy of your class, or that imbed your name, your company's name, a creation time, or a file path such as found in revision control systems. And of course, you should always test your program to insure that the class actually associated with your RWStringID is the one you expected.

More on Storing and Retrieving RWCollectables

In "Operators" in Chapter 14 we saw how to save and restore the morphology or pointer relationships of a class using the following global functions:

Rwvostream&  operator<<(RWvostream&, const RWCollectable&);
RWFile&      operator<<(RWFile&,     const RWCollectable&);
Rwvostream&  operator<<(RWvostream&, const RWCollectable*);
RWFile&      operator<<(RWFile&,     const RWCollectable*);
Rwvistream&  operator>>(RWvistream&, RWCollectable&);
RWFile&      operator>>(RWFile&,     RWCollectable&);
Rwvistream&  operator>>(RWvistream&, RWCollectable*&);
RWFile&      operator>>(RWFile&,     RWCollectable*&);

When working with RWCollectables, it is useful to understand how these functions work. Here is a brief description.

When you call one of the left-shift << operators for any collectable object for the first time, an identity dictionary is created internally. The object's address is put into the dictionary, along with its ordinal position in the output file—for example, first, second, sixth, etc.

Once this is done, a call is made to the object's virtual function saveGuts(). Because this is a virtual function, the call will go to the definition of saveGuts() used by the derived class. As we have seen, the job of saveGuts() is to store the internal components of the object. If the object contains other objects inheriting from RWCollectable, the object's saveGuts() calls operator<<() recursively for each of these objects.

Subsequent invocations of operator<<() do not create a new identity dictionary, but store the object's address in the already existing dictionary. If an address is encountered which is identical to a previously written object's address, then saveGuts() is not called. Instead, a reference is written that this object is identical to some previous object.

When the entire collection is traversed and the initial call to saveGuts() returns, the identity dictionary is deleted and the initial call to operator<<() returns.

The function operator>>() essentially reverses this whole process by calling restoreGuts() to restore objects into memory from a stream or file. When encountering a reference to an object that has already been created, it merely returns the address of the old object rather than asking the RWFactory to create a new one.

Here is a more sophisticated example of a class that uses these features:

#include <rw/collect.h>
#include <rw/rwfile.h>
#include <assert.h>
 
class Tangle : public RWCollectable
{
 
public:
 
  RWDECLARE_COLLECTABLE(Tangle)
 
  Tangle* nextTangle;
  int     someData;
 
  Tangle(Tangle* t = 0, int dat = 0){nextTangle=t; someData=dat;}
 
  virtual void saveGuts(RWFile&) const;
  virtual void restoreGuts(RWFile&);
 
};
 
void Tangle::saveGuts(RWFile& file) const{
   RWCollectable::saveGuts(file);          // Save the base class
 
   file.Write(someData);                        // Save internals
 
   file << nextTangle;                      // Save the next link
}
 
void Tangle::restoreGuts(RWFile& file){
   RWCollectable::restoreGuts(file);    // Restore the base class
 
  file.Read(someData);                       // Restore internals
 
  file >> nextTangle;                    // Restore the next link
}
 
// Checks the integrity of a null terminated list with head "p":
void checkList(Tangle* p){
   int i=0;
   while (p)
   {
     assert(p->someData==i);
     p = p->nextTangle;
     i++;
   }
}
 
RWDEFINE_COLLECTABLE(Tangle, 100)
 
main(){
   Tangle *head = 0, *head2 = 0;
 
   for (int i=9; i >= 0; i--)
     head = new Tangle(head,i);
 
   checkList(head);                    // Check the original list
 
   {
     RWFile file("junk.dat");
     file << head;
   }
 
   RWFile file2("junk.dat");
   file2 >> head2;
 
   checkList(head2);                   // Check the restored list
   return 0;
}

In the above example, the class Tangle implements a circularly linked list. What happens? When function operator<<() is called for the first time for an instance of Tangle, it sets up the identity dictionary as described above, then calls Tangle's saveGuts(), whose definition is shown above. This definition stores any member data of Tangle, then calls operator<<() for the next link. This recursion continues on around the chain.

If the chain ends with a nil object (that is, if nextTangle is zero), then operator<<() notes this internally and stops the recursion.

On the other hand, if the list is circular, then a call to operator<<() is eventually made again for the first instance of Tangle, the one that started this whole chain. When this happens, operator<<() will recognize that it has already seen this instance before and, rather than call saveGuts() again, will just make a reference to the previously written link. This stops the series of recursive calls and the stack unwinds.

Restoration of the chain is done in a similar manner. A call to:

RWFile& operator>>(RWFile&, RWCollectable*&);

can create a new object off the heap and return a pointer to it, return the address of a previously read object, or return the null pointer. In the last two cases, the recursion stops and the stack unwinds.

Multiple Inheritance

In Chapter 15 we built a Bus class by inheriting from RWCollectable. If we had an existing Bus class at hand, we might have saved ourselves some work by using multiple inheritance to create a new class with the functionality of both Bus and RWCollectable as follows:

class CollectableBus : public RWCollectable, public Bus {
  .
  .
  .
};

This is the approach taken by many of the Rogue Wave collectable classes; for example, class RWCollectableString inherits from both class RWCollectable and class RWCString. The general idea is to create your object first, then tack on the RWCollectable class to make the whole thing collectable. This way, you will be able to use your objects for other things or in other situations, where you might not want to inherit from class RWCollectable.

There is another good reason for using this approach: to avoid ambiguous base classes. Here's an example:

class A { };
 
class B : public A { };
 
class C : public A { };
 
class D : public B, public C { };
 
void fun(A&);
 
main () {
  D  d;
  fun(d);                                           // Which A ?
}

There are two approaches to disambiguating the call to fun(). We can either change it to:

fun((B)d);                         // We mean B's occurrence of A

or make A a virtual base class.

The first approach is error-prone because the user must know the details of the inheritance tree in order to make the proper cast.

The second approach, making A a virtual base class, solves this problem, but introduces another: it becomes nearly impossible to make a cast back to the derived class! This is because there are now two or more paths back through the inheritance hierarchy or, if you prefer a more physical reason, the compiler implements virtual base classes as pointers to the base class and you can't follow a pointer backwards.

We could exhaustively search all possible paths in the object's inheritance hierarchy, looking for a match. (This is the approach of the NIH Classes.) However, this search is slow, even if speeded up by "memorizing" the resulting addresses, since it must be done for every cast. Since it is also bulky and always complicated, we decided that it was unacceptable.

Hence, we went back to the first route. This can be made acceptable if we keep the inheritance trees simple by not making everything derive from the same base class. Hence, rather than using a large secular base class with lots of functionality, we have chosen to tease out the separate bits of functionality into separate, smaller base classes.

The idea is to first build your object, then tack on the base class that will supply the functionality you need, such as collectability. You thus avoid multiple base classes of the same type and the resulting ambiguous calls.