RogueWave Banner

Click on the banner to return to the user guide home page.

©Copyright 1996 Rogue Wave Software

Polymorphic Persistence

Polymorphic persistence preserves pointer relationships (or morphology) among persisted objects, and also allows the restoring process to restore an object without prior knowledge of that object's type.

Tools.h++ uses classes derived from RWCollectable to do polymorphic persistence. The objects created from those classes may be any of the different types derived from RWCollectable. A group of such objects, where the objects may have different types, is called a heterogeneous collection.

Table 7 lists the classes that use polymorphic persistence.

Category
Description
RWCollectable (Smalltalk-like) classes
RWCollectableDate, RWCollectableString...
RWCollection classes (which derive from RWCollectable)
RWBinaryTree, RWBag...

Operators

The storage and retrieval of polymorphic objects that inherit from RWCollectable is a powerful and adaptable feature of the Tools.h++ class library. Like other persistence mechanisms, polymorphic persistence uses the overloaded extraction and insertion operators (operator<< and operator>>). When these operators are used in polymorphic persistence, not only are objects isomorphically saved and restored, but objects of unknown type can be restored.

Polymorphic persistence uses the operators listed below.

  • Operators that save references to RWCollectable objects:

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

    Each RWCollectable-derived object is saved isomorphically with a class ID that uniquely identifies the object's class.

  • Operators that save RWCollectable pointers:

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

    Each pointer to an object is saved isomorphically with a class ID that uniquely identifies the object's class. Even nil pointers can be saved.

  • Operators that restore already-existing RWCollectable objects:

    Rwvistream&  operator>>(RWvistream&, RWCollectable&);
    RWFile&      operator>>(RWFile&,     RWCollectable&);

    Each RWCollectable-derived object is restored isomorphically. The persistence mechanism determines the object type at run time by examining the class ID that was stored with the object.

  • Operators that restore pointers to RWCollectable objects:

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

    Each object derived from RWCollectable is restored isomorphically and the pointer reference is updated to point to the restored object. The persistence mechanism determines the object type at run time by examining the class ID that was stored with the object. Since the restored objects are allocated from the heap, you are responsible for deleting them when you are done with them.

  • Designing your Class to Use Polymorphic Persistence

    Note that the ability to restore the pointer relationships of a polymorphic object is a property of the base class, RWCollectable. Polymorphic persistence can be used by any object that inherits from RWCollectable - including your own classes. Chapter 15 describes how to implement polymorphic persistence in the classes that you create by inheriting from RWCollectable.

    Polymorphic Persistence Example

    This example of polymorphic persistence contains two distinct programs. The first example polymorphically saves the contents of a collection to standard output (stdout). The second example polymorphically restores the contents of the saved collection from standard input (stdin). We divided the example to demonstrate that you can use persistence to share objects between two different processes.

    If you compile and run the first example, the output is an object as it would be stored to a file. However, you can pipe the output of the first example into the second example:

    firstExample | secondExample

    Example One: Saving Polymorphically

    This example constructs an empty collection, inserts objects into that collection, then saves the collection polymorphically to standard output.

    Notice that example one creates and saves a collection that includes two copies of the same object and two other objects. The four objects have three different types. When example one saves the collection and when example two restores the collection, we see that:

    Here's the first example:

    #include <rw/ordcltn.h>
    #include <rw/collstr.h>
    #include <rw/collint.h>
    #include <rw/colldate.h>
    #include <rw/pstream.h>
    
    main(){
       // Construct an empty collection 
       RWOrdered collection;
    
       // Insert objects into the collection.
    
       RWCollectableString* george;
       george = new RWCollectableString("George");
    
       collection.insert(george);     // Add the string once
       collection.insert(george);     // Add the string twice
       collection.insert(new RWCollectableInt(100));    
       collection.insert(new RWCollectableDate(3, "May", 1959));
    
       // "Store" to cout using portable stream:
       RWpostream ostr(cout);
       ostr << collection;     
          // The above statement calls the insertion operator:
          //    Rwvistream&  
          //      operator<<(RWvistream&, const RWCollectable&);
    
       // Now delete all the members in collection.  
       // clearAndDestroy() has been written so that it deletes 
       // each object only once, so that you do not have to 
       // worry about deleting the same object too many times.
    
       collection.clearAndDestroy();
    
       return 0;
    }
    

    Note that there are three types of objects stored in collection, an RWCollectableDate, and RWCollectableInt, and two RWCollectableStrings. The same RWCollectableString, george, is inserted into collection twice.

    Example Two: Restoring Polymorphically

    The second example shows how the polymorphically saved collection of the first example can be read back in and faithfully restored using the overloaded extraction operator:

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

    In this example, persistence happens when the program executes the statement:

       istr >> collection2;
    

    This statement uses the overloaded extraction operator to isomorphically restore the collection saved by the first example into collection2.

    How does persistence happen? For each pointer to an RWCollectable-derived object restored into collection2 from the input stream istr, the extraction operator operator>> calls a variety of overloaded extraction operators and persistence functions. For each RWCollectable-derived object pointer, collection2's extraction operators:

    We'll look at the implementation details for the persistence mechanism again in the next section. You should note, however, that when a heterogeneous collection (which must be based on RWCollection) is restored, the restoring process does not know the types of objects it will be restoring. Hence, it must always allocate the objects off the heap. This means that you are responsible for deleting the restored contents. This happens at the end of the example, in the expression collection2.clearAndDestroy.

    Here is the listing of the example:

    #define RW_STD_TYPEDEFS
    #include <rw/ordcltn.h>
    #include <rw/collstr.h>
    #include <rw/collint.h>
    #include <rw/colldate.h>
    #include <rw/pstream.h>
    main(){
       RWpistream istr(cin);
       RWOrdered collection2;
       // Even though this program does not need to have prior
       // knowledge of exactly what it is restoring, the linker
       // needs to know what the possibilities are so that the
       // necessary code is linked in for use by RWFactory.
       // RWFactory creates RWCollectable objects based on
       // class ID's.
           RWCollectableInt   exemplarInt;
           RWCollectableDate  exemplarDate;
       // Read the collection back in:
           istr >> collection2;
       // Note: The above statement is the code that restores
       // the collection.  The rest of this example shows us
       // what is in the collection.
       // Create a temporary string with value "George"
       // in order to search for a string with the same value:
           RWCollectableString temp("George");
       // Find a "George":
       //   collection2 is searched for an occurrence of a
       //   string with value "George".
       //   The pointer "g" will point to such a string:
             RWCollectableString* g;
             g = (RWCollectableString*)collection2.find(&temp);
       // "g" now points to a string with the value "George"
       // How many occurrences of g are there in the collection?
       size_t georgeCount   = 0;
       size_t stringCount   = 0;
       size_t integerCount  = 0;
       size_t dateCount     = 0;
       size_t unknownCount  = 0;
       // Create an iterator:
           RWOrderedIterator sci(collection2);
           RWCollectable* item;
       // Iterate through the collection, item by item,
       // returning a pointer for each item:
           while ( item = sci() ) {
             // Test whether this pointer equals g.
             // That is, test for identity, not just equality:
                 if (item->isA() == __RWCOLLECTABLESTRING && item==g)
                   georgeCount++;
             // Count the strings, dates and integers:
                 switch (item->isA()) {
                   case __RWCOLLECTABLESTRING: stringCount++; break;
                   case __RWCOLLECTABLEINT:    integerCount++; break;
                   case __RWCOLLECTABLEDATE:   dateCount++; break;
                   default:                    unknownCount++; break;
                 }
       }
       // Output results:
           cout << "There are:\n\t"
             << stringCount   << " RWCollectableString(s)\n\t"
             << integerCount  << " RWCollectableInt(s)\n\t"
             << dateCount     << " RWCollectableDate(s)\n\t"
             << unknownCount  << " other RWCollectable(s)\n\n"
             << "There are "
             << georgeCount
             << " pointers to the same object \"George\"" << endl;
       // Delete all objects created and return:
           collection2.clearAndDestroy();
           return 0;
    }
    Program Output:
    There are:
            2 RWCollectableString(s)
            1 RWCollectableInt(s)
            1 RWCollectableDate(s)
            0 other RWCollectable(s)
    There are 2 pointers to the same object "George"
    

    Figure 8 illustrates the collection created in the first example and restored in the second. Notice that both the memory map and the datatypes are identical in the saved and restored collection.

    Figure 8. Polymorphic Persistence

    Diagram showing the collection created. Diagram showing the identical collection restored.

    Example Two Revisited

    It is worth looking at the second example again so that you can see the mechanisms used to implement polymorphic persistence. The expression:

    istr >> collection2;
    

    calls the overloaded extraction operator:

    RWvistream& operator>>(RWvistream& str, RWCollectable& obj);
    

    This extraction operator has been written to call the object's restoreGuts() virtual function. In this case the object, obj, is an ordered collection and its version of restoreGuts() has been written to repeatedly call:

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

    once for each member of the collection[21]. Notice that its second argument is a reference to a pointer, rather than just a reference. This version of the overloaded operator>> looks at the stream, figures out the kind of object on the stream, allocates an object of that type off the heap, restores it from the stream, and finally returns a pointer to it. If this operator>> encounters a reference to a previous object, it just returns the old address. These pointers are inserted into the collection by the ordered collection's restoreGuts().

    These details about the polymorphic persistence mechanism are particularly important when you design your own polymorphically persistable class, as described in Chapter 15, Designing an RWCollectable Class. And when working with such classes, note that when Smalltalk-like collection classes are restored, the type of the restored objects is never known. Hence, the restoring processes must always allocate those objects off the heap. This means that you are responsible for deleting the restored contents. An example of this occurs at the end of both polymorphic persistence examples.

    Choosing Which Persistence Operator to Use

    In the second example, the persistence operator restored our collection to a reference to an RWCollectable:

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

    instead of to a pointer to a reference to an RWCollectable:

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

    The collection was allocated on the stack:

    RWpistream istr(cin);
    RWOrdered collection2;
    istr >> collection2;
    ...
    collection2.clearAndDestroy();
    

    instead of having operator>>(RWvistream&,RWCollectable*&) allocate the memory for the collection:

    RWpistream istr(cin);
    RWOrdered* pCollection2;
    istr >> pCollection2;
    ...
    collection->clearAndDestroy();
    delete pCollection2;
    

    Why make this choice? If you know the type of the collection you are restoring, then you are usually better off allocating it yourself, then restoring via:

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

    By using the reference operator, you eliminate the time required for the persistence machinery to figure out the type of object and have the RWFactory allocate one (see A Note on the RWFactory). Furthermore, by allocating the collection yourself, you can tailor the allocation to suit your needs. For example, you can decide to set an initial capacity for a collection class.


    Previous File Table of Contents Next File