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 ... |
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.
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. Section 15 describes how to implement polymorphic persistence in the classes that you create by inheriting from RWCollectable.
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
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:
The morphology of the collection is maintained;
The process that restores the collection does not know the object's type before it restores that object.
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.
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:
Read the stream istr to discover the type of the RWCollectable-derived object.
Read the stream istr to see if the RWCollectable-derived object that is pointed to has already been restored and referenced in the restore table.
If the RWCollectable-derived object has not yet been restored, the extraction operators create a pointer, create an object of the correct type from the heap, and initialize the created object with data read from the stream. Then the operators update the pointer with the address of the new object, and finally save a reference to the object in the restore table.
If the RWCollectable-derived object has already been restored, the extraction operators create a pointer and read the reference to the object from the stream. Then the operators use the reference to get the object's address from the restore table, and update the pointer with this address.
Finally, the restored pointer is inserted into the collection.
We'll look at the implementation details for the persistence mechanism again in Section 14.5.3.3. 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
Collection to be saved (collection1) |
Collection restored (collection2) |
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 Section 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.
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 Section 15.2.7, "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.