Always Save an Object by Value Before Saving the Identical Object by Pointer
In the case of both isomorphic and polymorphic persistence of objects, you should never stream out an object by pointer before streaming out the identical object by value. Whenever you design a class that contains a value and a pointer to that value, the saveGuts() and restoreGuts() member functions for that class should always save or restore the value, and then the pointer.
Here is an example that creates a class that uses isomorphic persistence, then instantiates objects of the class and attempts to persist those objects isomorphically. This example will fail. See the explanation that follows the code.
class Elmer {
public:
/* ... */
Elroy elroy_;
Elroy* elroyPtr_; // elroyPtr_ will point to elroy_.
};
RW_DEFINE_PERSISTABLE(Elmer)
void rwSaveGuts(RWFile& file, const Elmer& elmer) {
// Create a value for elroyPtr_ and stream it:
file << *elroyPtr_;
// If elroyPtr_ == &elroy_, store a reference
// to elroy_ that points to the created value for
// elroyPtr_:
file << elroy_;
}
void rwRestoreGuts(RWFile& file, Elmer& elmer){
// Create a value for elroyPtr_ in memory
// and change elroyPtr_ to point to that
// value in memory:
file >> elroyPtr_;
// Assign reference to value.
// If elroyPtr_ == &elroy then the value of
// elroy_ will already be created but now
// elroyPtr_ != &elroy so an
// RWTOOL_REF exception will be thrown:
file >> elroy_;
}
/* ... */
RWFile file("elmer.dat");
Elmer elmer;
Elmer elmer2;
elmer.elroyPtr_ = &(elmer.elroy_);
/* ... */
file << elmer; // Trouble is coming ...
/* ... */
file >> elmer2; // Trouble has arrived. RWTOOL_REF exception!
/* ... */
In the above code, the following statement isomorphically saves elmer to file:
file << elmer;
First, the statement calls the insertion operator:
operator<<(RWFile&, const Elmer&)
Since elmer hasn't been saved yet, the value of elmer will be saved to file and that value will be added to the isomorphic save table. However, elmer has the members elroy_ and elroyPtr_, which must be saved as part of saving elmer. The members of elmer are saved in rwSaveGuts(RWFile&, const Elmer&).
The function rwSaveGuts(RWFile&, const Elmer&) saves elroyPtr _ first, then elroy_. When rwSaveGuts() saves the value *elroyPtr_, it calls the insertion operator operator<<(RWFile&, const Elroy&).
The insertion operator operator<<(RWFile&, const Elroy&) sees that elmer.elroyPtr_ hasn't been stored yet, so it saves the value *(elmer.elroyPtr_) to file, and makes a note in the isomorphic save table that this value has been stored. Then operator<<(RWFile&, const Elroy&) returns to rwSaveGuts(RWFile&, const Elmer&).
Back in rwSaveGuts(RWFile&, const Elmer&), it is time for elmer.elroy_ to be saved. In this example, elmer.elroyPtr_ has the same address as elmer.elroy_. Once again the insertion operator operator<<(RWFile&, const Elroy&) is called, but this time the insertion operator notices from the isomorphic save table that *(elmer.elroyPtr_) has already been stored, so a reference to *(elmer.elroyPtr_) is stored to file instead of the value.
Everything seems okay, but trouble is looming. Trouble arrives with the statement:
file >> elmer2;
This statement calls the extraction operator operator>>(RWFile&, const Elmer&). Since elmer2 hasn't been restored yet, the value of elmer2 will be extracted from file and added to the isomorphic restore table. In order to extract the value of elmer2, the members elroy_ and elroyPtr_ must be extracted. The members of elmer2 are extracted in rwRestoreGuts(RWFile&, Elmer&).
The function rwRestoreGuts(RWFile&, Elmer&) restores elroyPtr _ first, then elroy_. When rwRestoreGuts restores the value *elroyPtr_, it calls the extraction operator operator>>(RWFile&, Elroy*&).
The extraction operator operator>>(RWFile&, Elroy*&) sees that elmer2.elroyPtr_ hasn't yet been extracted from file. So the extraction operator extracts the value *(elmer2.elroyPtr_) from file, allocates memory to create this value, and updates elmer2.elroyPtr_ to point to this value. Then the extraction operator makes a note in the isomorphic restore table that this value has been created. After making the note, operator>>(RWFile&, Elroy&) returns to rwRestoreGuts(RWFile&, Elmer&).
Back in rwRestoreGuts(RWFile&, Elmer&), it's time to restore elmer2.elroy_. Remember that elmer.elroyPtr_ has the same address as elmer.elroy_. The rwRestoreGuts function calls the extraction operator operator>>(RWFile&, Elroy&), which notices from the isomorphic save table that *(elmer2.elroyPtr_) has already been extracted from file. Because a value has already been stored in the restore table, the extraction operator extracts a reference to *(elmer.elroyPtr_) from file instead of extracting the value. But the extraction operator notices that the address of the value that elmer2.elroyPtr_ put into the restore table is different from the address of elmer2.elroy_. So operator>>(RWFile&, Elroy&) throws an RWTOOL_REF exception, and the restoration is aborted.
The solution to the problem in this particular case is easy: reverse the order of saving and restoring Elmer 's members! Here is the problem:
// WRONG!
void rwSaveGuts(RWFile& file, const Elmer& elmer) {
file << *elroyPtr_;
file << elroy_;
}
void rwRestoreGuts(RWFile& file, Elmer& elmer){
file >> elroyPtr_;
file >> elroy_;
}
Instead, you should write your functions the following way:
// RIGHT!
void rwSaveGuts(RWFile& file, const Elmer& elmer) {
file << elroy_;
file << *elroyPtr_;
}
void rwRestoreGuts(RWFile& file, Elmer& elmer){
file >> elroy_;
file >> elroyPtr_;
}
If you correct rwRestoreGuts() and rwSaveGuts() as suggested above, then the isomorphic save and restore tables can use the address of Elmer::elroy_ to update Elmer::elroyPtr_ if necessary.
Summary: Because of the possibility of having both a value and a pointer that points to that value in the same class, it's a good idea to define your rwSaveGuts() and rwRestoreGuts() member functions so that they always operate on values before pointers.