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. For a description of the persistence mechanism, see
Chapter 8. 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
passengers_ 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 might 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.
We do not 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 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 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 is 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 the Essential Tools Module 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().