Essential Tools Module User's Guide : Chapter 8 Persistence : Polymorphic Persistence
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.
The Essential Tools Module 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 18 lists the classes that use polymorphic persistence.
Table 18 – Polymorphic persistence classes 
Category
Description
RWCollectable (Smalltalk-like) classes
RWCollection classes (which derive from RWCollectable)
Operators
The storage and retrieval of polymorphic objects that inherit from RWCollectable is a powerful and adaptable feature of the The Essential Tools Module. 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. “Designing an RWCollectable Class” 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:
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 is the first example:
 
#include <rw/ordcltn.h>
#include <rw/collstr.h>
#include <rw/collint.h>
#include <rw/tools/ctdatetime.h>
#include <rw/pstream.h>
 
int main()
{
// Construct an empty collection
RWOrdered collection;
 
// Insert objects into the collection.
 
RWCollectableString* george;
george = new RWCollectableString("George");
 
collection.insert(george); // Add a string once
collection.insert(george); // Add a string twice
collection.insert(new RWCollectableInt(100));
collection.insert(new RWCollectableDateTime("May 3, 1959",
RWDateTime::setDate));
 
// "Store" to std::cout using portable stream.
RWpostream ostr(std::cout);
 
// The following statement calls the insertion operator:
// Rwvistream&
// operator<<(RWvistream&, const RWCollectable&);
ostr << collection;
 
// 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, an 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:
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 will look at the implementation details for the persistence mechanism again in “Example Two Revisited”. 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:
 
#include <rw/ordcltn.h>
#include <rw/collstr.h>
#include <rw/collint.h>
#include <rw/tools/ctdatetime.h>
#include <rw/pstream.h>
 
#include <iostream>
 
int main ()
{
RWpistream istr(std::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;
RWCollectableDateTime exemplarDateTime;
 
// 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 (0 != (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 __RWCOLLECTABLEDATETIME:
dateCount++;
break;
default:
unknownCount++;
break;
}
}
 
// Output results:
std::cout << "There are:\n\t"
<< stringCount << " RWCollectableString(s)\n\t"
<< integerCount << " RWCollectableInt(s)\n\t"
<< dateCount << " RWCollectableDateTime(s)\n\t"
<< unknownCount << " other RWCollectable(s)\n\n"
<< "There are "
<< georgeCount
<< " pointers to the same object \"George\"" << std::endl;
 
// Delete all objects created and return:
collection2.clearAndDestroy();
return 0;
}
Program Output:
 
There are:
2 RWCollectableString(s)
1 RWCollectableInt(s)
1 RWCollectableDateTime(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 data types are identical in the saved and restored collection.
Figure 8 – Polymorphic persistence
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. Actually, the Smalltalk collection classes are so similar that they all share the same version of restoreGuts(). Note that its second argument is a reference to a pointer, not 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 “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 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.