Chapter 14. Designing an RWCollectable Class

Classes that derive from RWCollectable carry two major advantages: they can be used by the Smalltalk-like collections, which also derive from RWCollectable, and they are the only set of collection classes able to use the powerful polymorphic persistence machinery. In this section, we will provide some examples of RWCollectable classes, then describe how to create your own RWCollectable classes.

What we don't do in this section is describe the mechanism of polymorphic persistence itself. Polymorphic, isomorphic, and simple persistence are all covered in detail in the Chapter 14. To summarize, polymorphic persistence is the storage and retrieval of objects to and from a stream or file in such a way that pointer relationships are preserved among persisted objects, which can be restored without the restoring process knowing the object's type.

Why Design an RWCollectable Class?

Before we get to the nuts and bolts of how to design an RWCollectable

class, let's discuss a concrete example of why you might choose to design RWCollectable classes.

Suppose you run a bus company. To automate part of your ridership tracking system, you want to write classes that represent a bus, its set of customers, and its set of actual passengers. In order to be a passenger, a person must be a customer. Hence, the set of customers is a superset of the set of passengers. Also, a person can physically be on the bus only once, and there is no point in putting the same person on the customer list more than once. As the developer of this system, you must make sure there are no duplicates on either list.

These duplicates can be a problem. Suppose that the program needs to be able to save and restore information about the bus and its customers. When it comes time to polymorphically save the bus, if your program naïvely iterates over the set of customers, then over the set of passengers, saving each one, any person who is both a customer and a passenger is saved twice. When the program polymorphically restores the bus, the list of passengers will not simply refer to people already on the customer list. Instead, each passenger will have a separate instantiation on both lists.

You need some way of recognizing when a person has already been polymorphically saved to the stream and, instead of saving him or her again, merely saving a reference to the previous instance.

This is the job of class RWCollectable. Objects that inherit from RWCollectable have the ability to save not only their contents, but also their relationships with other objects that inherit from RWCollectable. We call this feature isomorphic persistence. Class RWCollectable has isomorphic persistence, but more than that, it can determine at run time the type of the object to be saved or restored. We call the type of persistence provided by RWCollectablepolymorphic persistence, and recognize it as a superset of isomorphic persistence.

An Example of RWCollectable Classes

The code below shows how we might declare the classes described in the previous section. Later we'll use the macro RWDECLARE_COLLECTABLE and discuss our function choices. You'll find the complete code from which this example is taken at the end of this chapter; it is also given as the bus example in the toolexam directory.

class Bus : public RWCollectable  {
  RWDECLARE_COLLECTABLE(Bus)
 
public:
 
   Bus();
   Bus(int busno, const RWCString& driver);
   ~Bus();
 
   // Inherited from class "RWCollectable":
   Rwspace     binaryStoreSize() const;
   int         compareTo(const RWCollectable*) const;
   RWBoolean   isEqual(const RWCollectable*) const;
   unsigned    hash() const;
   void        restoreGuts(RWFile&);
   void        restoreGuts(RWvistream&);
   void        saveGuts(RWFile&) const;
   void        saveGuts(RWvostream&) const;
 
   void        addPassenger(const char* name);
   void        addCustomer(const char* name);
   size_t      customers() const;
   size_t      passengers() const;
   RWCString   driver() const    {return driver_;}
   int         number() const    {return busNumber_;}
 
private:
 
   RWSet       customers_;
   RWSet*      passengers_;
   int         busNumber_;
   RWCString   driver_;
};
 
class Client : public RWCollectable  {
  RWDECLARE_COLLECTABLE(Client)
  Client(const char* name) : name_(name) {}
private:
  RWCString     name_;
//ignore other client information for this example
};
 

Note how both classes inherit from RWCollectable. We have chosen to implement the set of customers by using class RWSet, which does not allow duplicate entries. This will guarantee that the same person is not entered into the customer list more than once. For the same reason, we have also chosen to implement the set of passengers using class RWSet. However, we have chosen to have this set live on the heap. This will help illustrate some points in the coming discussion.

How to Create an RWCollectable Object

Here's an outline of how to make your object inherit from RWCollectable. Additional information about how to do each step appears in the indicated section.

  1. Define a default constructor. See "Define a Default Constructor."

  2. Add the macro RWDECLARE_COLLECTABLE to your class declaration. See "Add RWDECLARE_COLLECTABLE() to your Class Declaration."

  3. Provide a class identifier for your class by adding one of two definition macros, RWDEFINE_COLLECTABLE or RWDEFINE_NAMED_COLLECTABLE, to one and only one source file (.cpp), to be compiled. See "Provide a Class Identifier for Your Class."

  4. Add definitions for inherited virtual functions as necessary. You may be able to use inherited definitions. "Add Definitions for Virtual Functions" discusses the following virtual functions:

    Int compareTo(const RWCollectable*) const;

    RWBoolean isEqual(const RWCollectable*) const;

    unsigned hash() const;

  5. Consider whether you need to define a destructor. See "Add Definitions for Virtual Functions."

  6. Add persistence to the class. You may be able to use inherited definitions, or you may have to add definitions for the following functions. See "How to Add Polymorphic Persistence."

    RWspace binaryStoreSize() const;

    void restoreGuts(RWFile&);

    void restoreGuts(RWvistream&);

    void saveGuts(RWFile&) const;

    void saveGuts(RWvostream&) const;

A note on RWFactory follows these steps. See "A Note on the RWFactory."

Define a Default Constructor

All RWCollectable classes must have a default constructor. The default constructor takes no arguments. The persistence mechanism uses this constructor to create an empty object, then restore that object with appropriate contents.

Default constructors are necessary in order to create vectors of objects in C++, so providing a default constructor is a good habit to get into anyway. Here's a possible definition of a default constructor for our Bus class.

Bus::Bus() :
  busNumber_  (0),
  driver_     ("Unknown"),
  passengers_ (rwnil)
{
}

Add RWDECLARE_COLLECTABLE() to your Class Declaration

The example in "An Example of RWCollectable Classes" includes the macro invocation RWDECLARE_COLLECTABLE(Bus) in the declaration for Bus. You must put this macro in your class declaration, using the class name as the argument. Using the macro guarantees that all necessary member functions are declared correctly.

Provide a Class Identifier for Your Class

Polymorphic persistence lets you save a class in one executable, and restore it in a different executable or in a different run of the original executable. The restoring executable can use the class, without prior knowledge of its type. In order to provide polymorphic persistence, a class must have a unique,[21] unchanging identifier. Because classes derived from RWCollectable are polymorphically persistent, they must have such an identifier.

Identifiers can be either numbers or strings. A numeric identifier is an unsigned short with a typedef of RWClassID. A string identifier has a typedef of RWStringID. If you choose to specify a numeric identifier, your class will have an automatically generated string identifier, which will be the same sequence of characters as the name of the class. Similarly, if you choose to specify a string identifier, your class will have an automatically generated numeric ID when used in an executable.

Tools.h++ includes two definition macros to provide an identifier for the class you design. If you want to specify a numeric ID, use:

RWDEFINE_COLLECTABLE (className, numericID) 

If you want to specify a string ID, use:

RWDEFINE_NAMED_COLLECTABLE (className, stringID) 

Note that you do not include the definition macros in the header file for the class. Rather, the macros are part of a .cpp file that uses the class. You must include exactly one define macro for each RWCollectable class that you're creating, in one and only source file (.cpp). Use the class name as the first argument, and a numeric class ID or string class ID as the second argument. For the bus example, you can include the following definition macros:

RWDEFINE_COLLECTABLE(Bus, 200)

or:

RWDEFINE_NAMED_COLLECTABLE(Client, "a client")

The first use provides a numeric ID 200 for class Bus, and the second provides a string ID, "a client", for class Client.

In the remainder of this manual, we use RWDEFINITION_MACRO to indicate that you can choose either of these macros. In example code, we will pick one or the other macro.

Either macro will automatically supply the definitions for the virtual functions isA() and newSpecies().[22] In the following sections, we describe these virtual functions, discuss the stringID() method which is new in Version 7 of Tools.h++, and provide a brief introduction to the RWFactory class, which helps implement polymorphic persistence.

Virtual Function isA( )

The virtual function isA() returns a class identifier: a unique number that identifies an object's class. It can be used to determine the class to which an object belongs. Here's the function declaration provided by macro RWDECLARE_COLLECTABLE:

virtual RWClassID isA() const;

RWClassID is actually a typedef to an unsigned short. Numbers from 0x8000 (hex) and up are reserved for use by Rogue Wave. You may choose a numeric class ID from 9x0001 to 0x7fff. There is a set of class symbols defined in <rw/tooldefs.h> for the Tools.h++ Class Library. Generally, these follow the pattern of a double underscore followed by the class name with all letters in upper case. For example:

RWCollectableString yogi;
yogi.isA() == __RWCOLLECTABLESTRING;         // Evaluates TRUE

The macro RWDECLARE_COLLECTABLE(className) will automatically provide a declaration for isA(). Either RWDEFINITION_MACRO will supply the definition.

Virtual Function newSpecies()

The job of this function is to return a pointer to a brand new object of the same type as self. Here is the function declaration provided by macro RWDECLARE_COLLECTABLE:

virtual RWCollectable*  newSpecies() const;

The definition is automatically provided by either version of RWDEFINITION_MACRO.

Function stringID()

The stringID() function acts like a virtual function, but it is not.[23] It returns an instance of RWStringID, a unique string that identifies an object's class. RWStringID is derived from class RWCString. By default, the string identifier for a class is the same as the name of the class. RWStringID can be used instead of, or as a suppplement to, RWClassIDs.

Add Definitions for Virtual Functions

Class RWCollectable declares the following virtual functions:

virtual                 ~RWCollectable();
virtual Rwspace         binaryStoreSize() const;
virtual int             compareTo(const RWCollectable*) const;
virtual unsigned        hash() const;
virtual RWClassID       isA() const;
virtual RWBoolean       isEqual(const RWCollectable*) const;
virtual RWCollectable*  newSpecies() const;
virtual void            restoreGuts(RWvistream&);
virtual void            restoreGuts(RWFile&);
virtual void            saveGuts(RWvostream&) const;
virtual void            saveGuts(RWFile&) const;

In these functions RWBoolean is a typedef for an int, RWspace is a typedef for unsigned long, and RWClassID is a typedef for an unsigned short. Any class that derives from class RWCollectable should be able to understand any of these methods. Although default definitions are given for all of them in the base class RWCollectable, it is best for you as the class designer to provide definitions tailored to the class at hand.

We've split our discussion of these virtual functions. We discuss the destructor in "Object Destruction" and the binaryStoreSize(), saveGuts(), and restoreGuts() functions in "How to Add Polymorphic Persistence," where we describe how to add persistence to a class. Virtual functions isA() and newSpecies() are declared and defined by macros, so they were discussed above, in "Virtual Function isA( )" and "Virtual Function newSpecies()." This section presents discussion on the remaining functions: compareTo(), isEqual(), and hash(). A very brief example, showing how all three functions deal with the same data, appears in "An Example of compareTo(), isEqual(), and hash()."

Virtual Function compareTo()

The virtual function compareTo() is used to order objects relative to each other. This function is required in collection classes that depend on such ordering, such as RWBinaryTree or RWBTree. Here is its declaration:

virtual int  compareTo(const RWCollectable*) const;

The function int compareTo(const RWCollectable*) const should return a number greater than zero if self is greater than the argument, a number less than zero if self is less than the argument, and zero if self is equal to the argument.

The definition and meaning of whether one object is greater than, less than, or equal to another object is left to the class designer. The default definition, found in class RWCollectable, is to compare the two addresses of the objects. This default definition should be considered a placeholder; in practice, it is not very useful and could vary from run to run of a program.

Here is a possible definition of compareTo():

int Bus::compareTo(const RWCollectable* c) const
{  const Bus* b = (const Bus*)c;
  if (busNumber_ == b->busNumber_) return 0;
   return busNumber_ > b->busNumber_ ? 1 : -1;
}

Here we are using the bus number as a measure of the ordering of buses. If we need to insert a group of buses into an RWBinaryTree, they would be sorted by their bus number. Note that there are many other possible choices(we could have used the driver name, in which case they would have been sorted by the driver name. Which choice you use will depend on your particular problem.

There is a hazard here. We have been glib in assuming that the actual type of the RWCollectable which c points to is always a Bus. If a careless user inserted, say, an RWCollectableString into the collection, then the results of the cast (const Bus*)c would be invalid, and dereferencing it could bring disaster[24] . The necessity for all overloaded virtual functions to share the same signatures requires that they return the lowest common denominator, in this case, class RWCollectable. The result is that much compile-time type checking breaks down.


Note: You must be careful that the members of a collection are either homogeneous (i.e., all of the same type), or that there is some way of telling them apart. The member functions isA() or stringID() can be used for this.


Virtual Function isEqual()

The virtual function isEqual() plays a similar role to the tester function of the generic collection classes described in "Tester Functions" in Chapter 12.

RWBoolean  isEqual(const RWCollectable* c) const;

The function RWBoolean isEqual(const RWCollectable*) should return TRUE if the object and its argument are considered equal, and FALSE otherwise. The definition of equality is left to the class designer. The default definition, as defined in class RWCollectable, is to test the two addresses for equality, that is, to test for identity.

Note that isEqual does not have to be defined as being identical. Rather isEqual can mean that two objects are equivalent in some sense. In fact, the two objects need not even be of the same type. The only requirement is that the object passed as an argument must inherit type RWCollectable. You are responsible for making sure that any typecasts you do are appropriate.

Also note that there is no formal requirement that two objects that compare equal (i.e., compareTo() returns zero) must also return TRUE from isEqual(), although it is hard to imagine a situation where this wouldn't be the case. It is also possible to design a class for which the isEqual test returns true for objects that have different hash values. This would make it impossible to search for such objects in a hash-based collection.

For the Bus class, an appropriate definition of isEqual might be:

RWBoolean Bus::isEqual(const RWCollectable* c) const
{  const Bus* b = (const Bus*)c;
   return busNumber_ == b->busNumber_;
}

Here we are considering buses to be equal if their bus numbers are the same. Again, other choices are possible.

Virtual Function hash()

The function hash() should return an appropriate hashing value for the object. Here is the function's declaration:

unsigned  hash() const;

A possible definition of hash() for our class Bus might be:

unsigned Bus::hash() const{
  return (unsigned)busNumber_;
}

The example above simply returns the bus number as a hash value. Alternatively, we could choose the driver's name as a hash value:

unsigned Bus::hash() const{
  return driver_.hash();
}

In the above example, driver_ is an RWCString that already has a hash function defined.


Note: we expect that two objects that test TRUE for isEqual will hash to the same value.


An Example of compareTo(), isEqual(), and hash()

We've described three inherited virtual functions: compareTo(), isEqual(), and hash(). Here is an example that defines a set of objects, and applies the functions. The results of the functions appear as comments in the code.

RWCollectableString a("a");
RWCollectableString b("b");
RWCollectableString a2("a");
 
a.compareTo(&b);      // Returns -1
a.compareTo(&a2);     // Returns 0 ("compares equal")
b.compareTo(&a);      // Returns 1
 
a.isEqual(&a2);       // Returns TRUE
a.isEqual(&b);        // Returns FALSE
 
a.hash()              // Returns 96 (operating system dependent)

Note that the compareTo() function for RWCollectableStrings has been defined to compare strings lexicographically in a case sensitive manner. See class RWCString in the Class Reference for details.

Object Destruction

All objects inheriting from class RWCollectable inherit a virtual destructor. Hence, the actual type of the object need not be known until run time in order to delete the object. This allows all items in a collection to be deleted without knowing their actual type.

As with any C++ class, objects inheriting from RWCollectable may need a destructor to release the resources they hold. In the case of Bus, the names of passengers and customers are RWCollectableStrings that were allocated off the heap. Hence, they must be reclaimed. Because these strings never appear outside the scope of the class, we do not have to worry about the user having access to them. Hence, we can confidentially delete them in the destructor, knowing that no dangling pointers will be left.

Furthermore, because the set pointed to by customers_ is a superset of the set pointed to by passengers_, it is essential that we delete only the contents of customers_.

Here's a possible definition:

Bus::~Bus()
{  customers_.clearAndDestroy();
   delete passengers_;
}

Note that the language guarantees that it is okay to call delete on the pointer passengers_ even if it is nil.

How to Add Polymorphic Persistence

The saveGuts() and restoreGuts() virtual functions are responsible for saving and restoring the internal state of RWCollectable objects. To add persistence to your RWCollectable class, you must override the saveGuts() and restoreGuts() virtual member functions so that they write out all of your object's member data. "Virtual Functions saveGuts(RWFile&) and saveGuts(RWvostream&)" and "Virtual Functions restoreGuts(RWFile&) and restoreGuts(RWvistream&)" describe approaches you can use to correctly define these functions. "Multiply-referenced Objects" describes how these functions handle multiply-referenced objects.

Polymorphically saving an object to a file may require some knowledge of the number of bytes that need to be allocated for storage of an object. The binaryStoreSize() function calculates this value. "Virtual Function binaryStoreSize()" describes how to use binaryStoreSize().

RWCollection has its own versions of the saveGuts() and restoreGuts() functions that are used to polymorphically save collections that inherit from that class. "Polymorphically Persisting Custom Collections" briefly describes how these functions work.

Virtual Functions saveGuts(RWFile&) and saveGuts(RWvostream&)

The saveGuts(RWFile&) and saveGuts(RWvostream&) virtual functions are responsible for polymorphically saving the internal state of an RWCollectable object on either a binary file, using class RWFile, or on a virtual output stream, using class RWvostream.[25] This allows the object to be restored at some later time, or in a different location. Here are some rules for defining a saveGuts() function:

  1. Save the state of your base class by calling its version of saveGuts().

  2. For each type of member data, save its state. How to do this depends upon the type of the member data:

    Primitives. For primitives, save the data directly. When saving to RWFiles, use RWFile::Write(); when saving to virtual streams, use the insertion operator RWvostream::operator<<().

    Rogue Wave classes. Most Rogue Wave classes offer an overloaded version of the insertion operator. For example, RWCString offers:

    RWvostream& operator<<(RWvostream&, const RWCString& str);

    Hence, many Rogue Wave classes can simply be shifted onto the stream.

    Objects inheriting from RWCollectable. For most of these objects, use the global function:

    RWvostream& operator<<(RWvostream&, 
                           const RWCollectable& obj);

    This function will call saveGuts() recursively for the object.

With these rules in mind, let's look at a possible definition of the saveGuts() functions for the Bus example:

void Bus::saveGuts(RWFile& f) const
{  RWCollectable::saveGuts(f);    // Save base class
   f.Write(busNumber_);           // Write primitive directly
   f << driver_ << customers_;    // Use Rogue Wave 
                                  //provided versions
   f << passengers_;              // Will detect nil pointer
                                  // automatically
}
 
void Bus::saveGuts(RWvostream& strm) const
{  RWCollectable::saveGuts(strm); // Save base class
   strm << busNumber_;            // Write primitives directly
   strm << driver_ << customers_; // Use Rogue Wave 
                                  // provided versions
   strm << passengers_;           // Will detect nil pointer
                                  // automatically
}

Member data busNumber_ is an int, a C++ primitive. It is stored directly using either RWFile::Write(int), or RWvostream::operator<<(int).

Member data driver_ is an RWCString. It does not inherit from RWCollectable. It is stored using:

RWvostream& operator<<(RWvostream&, const RWCString&);

Member data customers_ is an RWSet. It does inherit from RWCollectable. It is stored using:

RWvostream& operator<<(RWvostream&, const RWCollectable&);

Finally, member data passengers_ is a little tricky. This data is a pointer to an RWSet, which inherits from RWCollectable. However, there is the possibility that the pointer is nil. If it is nil, then passing it to:

RWvostream& operator<<(RWvostream&, const RWCollectable&);

would be disastrous, as we would have to dereference passengers_:

strm << *passengers_;

Instead, since our class has declared passenger_ as an RWSet*, we pass it to:

RWvostream& operator<<(RWvostream&, const RWCollectable*);

which automatically detects the nil pointer and stores a record of it.

Virtual Functions restoreGuts(RWFile&) and restoreGuts(RWvistream&)

In a manner similar to saveGuts(), these virtual functions are used to restore the internal state of an RWCollectable from a file or stream. Here is a definition of these functions for the Bus class:

void Bus::restoreGuts(RWFile& f)
{  RWCollectable::restoreGuts(f);   // Restore base class
   f.Read(busNumber_);              // Restore primitive
   f >> driver_ >> customers_;      // Uses Rogue Wave provided
                                    // versions
 
   delete passengers_;              // Delete old RWSet
   f >> passengers_;                // Replace with a new one
}
 
void Bus::restoreGuts(RWvistream& strm)
{  RWCollectable::restoreGuts(strm);   // Restore base class
   strm >> busNumber_ >> driver_ >> customers_;
 
   delete passengers_;                 // Delete old RWSet
   strm >> passengers_;                // Replace with a new one
}

Note that the pointer passengers_ is restored using:

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

If the original passengers_ is non-nil, then this function restores a new RWSet off the heap and returns a pointer to it. Otherwise, it returns a nil pointer. Either way, the old contents of passengers_ are replaced. Hence, we must call delete passengers_ first.

Multiply-referenced Objects

A passenger name can exist in the set pointed to by customers_and in the set pointed to by passengers_; that is, both collections contain the same string. When the Bus is restored, we want to make sure that the pointer relationship is maintained, and that our restoration does not create another copy of the string.

Fortunately, we don't have to do anything special to insure that the pointer relationship stays as it should be. Consider the call:

Bus aBus;
RWFile aFile("busdata.dat");
 
aBus.addPassenger("John");
aFile << aBus;

Because passenger_is a subset of customer_, the function addPassenger puts the name on both the customer list and the passenger list. When we save aBus to aFile, both lists are saved in a single call: first the customer list, then the passenger list. The polymorphic persistence machinery saves the first reference to John, but for the second reference it merely stores a reference to the first copy. During the restore, both references will resolve to the same object, replicating the original morphology of the collection.

Virtual Function binaryStoreSize()

The binaryStoreSize() virtual function calculates the number of bytes necessary to store an object using RWFile. The function is:

virtual Rwspace  binaryStoreSize() const;

This function is useful for classes RWFileManager and RWBTreeOnDisk, which require allocation of space for an object before it can be stored. The non-virtual function recursiveStoreSize() returns the number of bytes that is actually stored. Recursive store size uses binaryStoreSize() to do its work.

Writing a version of binaryStoreSize() is usually straightforward. You just follow the pattern set by saveGuts(RWFile&), except that instead of saving member data, you add up their sizes. The only real difference is a syntactic one: instead of insertion operators, you use sizeof() and the member functions indicated below:

  • For primitives, use sizeof().

  • For objects that inherit from RWCollectable, if the pointer is non-nil, use member function:

    RWspace RWCollectable::recursiveStoreSize();

  • For objects that inherit from RWCollectable, if the pointer is nil, use the static member function:

    RWspace RWCollectable::nilStoreSize();

  • For other objects, use member function binaryStoreSize().

Here's a sample definition of a binaryStoreSize() function for class Bus:

RWspace Bus::binaryStoreSize() const{
   RWspace count = RWCollectable::binaryStoreSize() +
     customers_.recursiveStoreSize() +
     sizeof(busNumber_) +
     driver_.binaryStoreSize();
 
   if (passengers_)
     count += passengers_->recursiveStoreSize();
   else
     count += RWCollectable::nilStoreSize();
 
   return count;
}

Polymorphically Persisting Custom Collections

The versions of saveGuts() and restoreGuts() that Tools.h++ built into class RWCollection are sufficient for most collection classes. The function RWCollection::saveGuts() works by repeatedly calling:

RWvostream& operator<<(RWvostream&, const RWCollectable&);

for each item in the collection. Similarly, RWCollection::restoreGuts() works by repeatedly calling:

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

This operator allocates a new object of the proper type off the heap, then calls insert(). Because all of the Rogue Wave Smalltalk-like collection classes inherit from RWCollection, they all use this mechanism.

If you decide to write your own collection classes that inherit from class RWCollection, you will rarely have to define your own saveGuts() or restoreGuts().

There are exceptions. For example, class RWBinaryTree has its own version of saveGuts(). This is necessary because the default version of saveGuts() stores items in order. For a binary tree, this would result in a severely unbalanced tree when the tree was read back in—essentially, the degenerate case of a linked list. Hence, RWBinaryTree's version of saveGuts() stores the tree level-by-level.

When you design your class, you must determine whether it has similar special requirements which may need a custom version of saveGuts() and restoreGuts().

A Note on the RWFactory

Let's review what the RWDEFINITION_MACROs look like:

RWDEFINE_COLLECTABLE(className, numericID)

or, using a string ID:

RWDEFINE_NAMED_COLLECTABLE(className, stringID)

In the .cpp file for the bus example, the macros appear like this:

RWDEFINE_COLLECTABLE(Bus, 200)

and:

RWDEFINE_NAMED_COLLECTABLE(Client, "a client")

Because you use these macros, a program can allow a new instance of your class to be created given only its RWClassID:

Bus* newBus = (Bus*)theFactory->create(200);

or its RWStringID:

Client* aClient = (Client*)theFactory->create("a client");

The pointer theFactory is a global pointer that points to a one-of-a-kind global instance of class RWFactory, used to hold information about all RWCollectable classes that have instances in the executable. The create() method of RWFactory is used internally by the polymorphic persistence machinery to create a new instance of a persisted object whose type is not known at run time. You will not normally use this capability in your own source code, because the use of RWFactory is generally transparent to the user. See the Class Reference for more details on RWFactory.

Summary

In general, you may not have to supply definitions for all of these virtual functions when designing your own class. For example, if you know that your class will never be used in sorted collections, then you do not need a definition for compareTo(). Nevertheless, it is a good idea to supply definitions for all virtual functions anyway: that's the best way to encourage code reuse!

Here then, is the complete listing for our class Bus:

BUS.H:BEGIN FILE: bus.h
#ifndef __BUS_H__
#define __BUS_H__
 
#include <rw/rwset.h>
#include <rw/collstr.h>
 
class Bus : public RWCollectable  {
  RWDECLARE_COLLECTABLE(Bus)
 
public:
 
  Bus();
  Bus(int busno, const RWCString& driver);
  ~Bus();
 
  // Inherited from class "RWCollectable":
  Rwspace      binaryStoreSize() const;
  int          compareTo(const RWCollectable*) const;
  RWBoolean    isEqual(const RWCollectable*) const;
  unsigned     hash() const;
  void         restoreGuts(RWFile&);
  void         restoreGuts(RWvistream&);
  void         saveGuts(RWFile&) const;
  void         saveGuts(RWvostream&) const;
 
  void         addPassenger(const char* name);
  void         addCustomer(const char* name);
  size_t       customers() const;
  size_t       passengers() const;
  RWCString    driver() const     {return driver_;}
  int          number() const     {return busNumber_;}
 
private:
 
  RWSet        customers_;
  RWSet*       passengers_;
  int          busNumber_;
  RWCString    driver_;
};
 
class Client : public RWCollectable  {
  RWDECLARE_COLLECTABLE(Client)
  Client();
  Client(const char* name);
  Rwspace      binaryStoreSize() const;
  int          compareTo(const RWCollectable*) const;
  RWBoolean    isEqual(const RWCollectable*) const;
  unsigned     hash() const;
  void         restoreGuts(RWFile&);
  void         restoreGuts(RWvistream&);
  void         saveGuts(RWFile&) const;
  void         saveGuts(RWvostream&) const;
private:
  RWCString    name_;
//ignore other client information for this example
};
#endif
 
 
 
 
BUS.CPP:
 
#include "bus.h"
#include <rw/pstream.h>
#include <rw/rwfile.h>
#ifdef __GLOCK__
#  include <fstream.hxx>
#else
#  include <fstream.h>
#endif
RWDEFINE_COLLECTABLE(Bus, 200)
 
Bus::Bus() :
  busNumber_  (0),
  driver_     ("Unknown"),
  passengers_ (rwnil)
{}
Bus::Bus(int busno, const RWCString& driver) :
  busNumber_  (busno),
  driver_     (driver),
  passengers_ (rwnil)
{}
 
Bus::~Bus()  {
  customers_.clearAndDestroy();
  delete passengers_;
}
 
RWspace
Bus::binaryStoreSize() const  {
  RWspace count = RWCollectable::binaryStoreSize() +
    customers_.recursiveStoreSize() +
    sizeof(busNumber_) +
    driver_.binaryStoreSize();
 
  if (passengers_)
    count += passengers_->recursiveStoreSize();
 
  return count;
}
 
int
Bus::compareTo(const RWCollectable* c) const  {
  const Bus* b = (const Bus*)c;
  if (busNumber_ == b->busNumber_) return 0;
  return busNumber_ > b->busNumber_ ? 1 : -1;
}
 
RWBoolean
Bus::isEqual(const RWCollectable* c) const  {
  const Bus* b = (const Bus*)c;
  return busNumber_ == b->busNumber_;
}
 
unsigned
Bus::hash() const  {
  return (unsigned)busNumber_;
}
 
size_t
Bus::customers() const  {
  return customers_.entries();
}
 
size_t
Bus::passengers() const   return passengers_ ? passengers_->entries() : 0;
}
 
void 
Bus::saveGuts(RWFile& f) const  {
  RWCollectable::saveGuts(f);    // Save base class
  f.Write(busNumber_);           // Write primitive directly
  f << driver_ << customers_;    // Use Rogue Wave provided versions
  f << passengers_;       // Will detect nil pointer automatically
}
 
void 
Bus::saveGuts(RWvostream& strm) const  {
  RWCollectable::saveGuts(strm);   // Save base class
  strm << busNumber_;              // Write primitives directly
  strm << driver_ << customers_;   // Use Rogue Wave 
                                   // provided versions
  strm << passengers_;     // Will detect nil pointer automatically
}
 
void Bus::restoreGuts(RWFile& f)  {
  RWCollectable::restoreGuts(f);  // Restore base class
  f.Read(busNumber_);             // Restore primitive
  f >> driver_ >> customers_;     // Uses Rogue Wave
                                  // provided versions
 
  delete passengers_;             // Delete old RWSet
  f >> passengers_;               // Replace with a new one
}
 
void Bus::restoreGuts(RWvistream& strm)  {
  RWCollectable::restoreGuts(strm); // Restore base class
  strm >> busNumber_ >> driver_ >> customers_;
 
  delete passengers_;             // Delete old RWSet
  strm >> passengers_;            // Replace with a new one
}
 
void
Bus::addPassenger(const char* name)  {
  Client* s = new Client(name);
  customers_.insert( s );
 
  if (!passengers_)
    passengers_ = new RWSet;
 
  passengers_->insert(s);
}
 
void
Bus::addCustomer(const char* name)  {
  customers_.insert( new Client(name) );
}
 
///////////////    Here are Client methods //////////////
RWDEFINE_NAMED_COLLECTABLE(Client,"client")
 
Client::Client() {} // Uses RWCString default constructor
 
Client::Client(const char* name) : name_(name) {}
 
RWspace
Client::binaryStoreSize() const {
  return name_->binaryStoreSize();
}
 
int
Client::compareTo(const RWCollectable* c) const  {
return name_.compareTo(((Client*)c)->name_);
}
 
RWBoolean
Client::isEqual(const RWCollectable* c) const  {
  return name_ == *(Client*)c;
}
 
unsigned
Client::hash() const  {
  return name_.hash();
}
 
void
Client::restoreGuts(RWFile& f)  {
  f >> name_;
}
 
void
Client::restoreGuts(RWvistream& vis)  {
  vis >> name_;
}
 
void
Client::saveGuts(RWFile& f) const  {
  f << name_;
}
 
void
Client::saveGuts(RWvostream& vos) const  {
  vos << name_;
}
 
 
main()  {
  Bus theBus(1, "Kesey");
  theBus.addPassenger("Frank");
  theBus.addPassenger("Paula");
  theBus.addCustomer("Dan");
  theBus.addCustomer("Chris");
 
  { // block controls lifetime of stream
    ofstream f("bus.str");
    RWpostream stream(f);
    stream << theBus;       // Persist theBus to an ASCII stream
  }
 
  {
    ifstream f("bus.str");
    RWpistream stream(f);
    Bus* newBus;
    stream >> newBus;     // Restore it from an ASCII stream
 
    cout << "Bus number " << newBus->number() 
         << " has been restored; its driver is " 
         << newBus->driver() << ".\n";
    cout << "It has " << newBus->customers() 
         << " customers and "
        << newBus->passengers() << " passengers.\n\n";
 
    delete newBus;
  }
 
  return 0;
}

Program Output:

Bus number 1 has been restored; its driver is Kesey.
It has 4 customers and 2 passengers.



[21] Strictly, it only needs to be different from every other identifier in any given executable.

[22] The RWDEFINITION_MACROs do more than merely implement the two mentioned methods. Before you choose not to use one of the provided macros, review them in detail to be sure you understand all that they do.

[23] See "mplementing Virtuals Via Statics" in Chapter 18 for a discussion of RWStringID and how to mimic a virtual function. We wrote the code this way to maintain link compatibility with object code compiled from the previous version of Tools.h++.

[24] This is a glaring deficiency in C++ that the user must constantly be aware of, especially if the user plans to have heterogeneous collections. See "Don't Use Sorted RWCollections to Store Heterogeneous RWCollectables" in Chapter 14 for a description of the problem.

[25] For a description of the persistence mechanism, see Chapter 14.