Chapter 13. Persistence

This chapter contains the following sections:

Persistence is the ability to save an object to a file or a stream and then restore that object from the file or stream. Persistence is a very important feature of objects because it facilitates the exchange of objects between processes. Using persistence and working through streams, you can send objects from one program to another, or from one user to another. You can also save a persistent object to a file on a disk, and restore it from disk at another time, or in another place.

Levels of Persistence

An object has one of four levels of persistence:

  • No persistence. There is no mechanism for storage and retrieval of the object.

  • Simple persistence. A level of persistence that provides storage and retrieval of individual objects to and from a stream or file. Simple persistence does not preserve pointer relationships among the persisted objects.

  • Isomorphic persistence. A level of persistence that preserves the pointer relationships among the persisted objects.

  • Polymorphic persistence. The highest level of persistence. Polymorphic persistence preserves pointer relationships among the persisted objects and allows the restoring process to restore an object without prior knowledge of that object's type.

The Class Reference indicates the level of persistence for each class. This section provides information about each level of persistence through descriptions, examples, and procedures for designing your own persistent classes.

A Note About Terminology

Tools.h++ provides input and output classes that let you save and restore objects. These classes are:

  • RWFile: RWFile lets you save and restore objects to a file;

  • RWvostream: Classes derived from RWvostream, such as RWpostream, RWbostream, and RWeostream, are used to save objects;

  • RWvistream: Classes derived from RWvistream, such as RWpostream, RWbistream, and RWeistream, are used to restore objects.

To keep our explanations simple, we'll refer to all of these input and output classes as streams. For a discussion of the trade-offs in using RWvostream and RWvistream versus RWFile, see Chapter 6 and Chapter 7.

About the Examples in this Section

For your convenience, all examples listed in this section are provided on disk in the directory rw/toolexam/manual. Each of the examples in this chapter has the name persist*.cpp.

No Persistence

Some Tools.h++ classes have no persistence. The Class Reference indicates "None" in the Persistence section for all such classes. Review the class reference entry for a particular class if you have a question about its level of persistence.

Simple Persistence

Simplepersistence is the storage and retrieval of an object to and from a stream. lists the classes in Tools.h++ that use simple persistence.

Table 13-1. Classes with Simple Persistence

Category

Description

C++ fundamental types

int, char, float, …

Rogue Wave date and time classes

RWDate, RWTime

Rogue Wave string classes

RWCString, RWWString

Miscellaneous Rogue Wave classes

RWBitVec

Because it is straightforward, simple persistence is a quick and easy way to save and restore objects that have neither pointers to other objects nor virtual member functions.

However, when objects that refer to each other are saved and then restored with simple persistence, the pointer relationships, or morphology, among the objects can change. This is because simple persistence assumes that every pointer reference to an object in memory refers to a unique object. Thus, when an object is saved with simple persistence, two references to the same memory location will cause two copies of the contents of that memory location to be saved. Not only does this use extra space in the stream, but it also causes the restored object to point to two distinct copies of the referenced object.

Two Examples of Simple Persistence

Let's look at a two examples of simple persistence. The first example illustrates successful persistence of fundamental datatypes, and demonstrates the Tools.h++ overloaded operators operator<< and operator>>, which save and restore persistent objects. The second example illustrates one of the problems with simple persistence—its inability to maintain pointer relationships among objects.

Example One: Simple Persisting Objects of Fundamental Type

This example uses simple persistence to save two integers to an output stream po, which saves the integers to the file int.dat. Then the example restores the two integers from the stream pi, which reads the integers from the file int.dat.

The example uses the overloaded insertion operator operator<< to save the objects, and the overloaded extraction operator operator>> to restore the objects, much the same way as you use these operators to output and input objects in C++ streams.

Note that the saving stream and the restoring stream are put into separate blocks. This is so that opening pi will cause it to be positioned at the beginning of the file.

Here's the code:

#include <assert.h>
#include <fstream.h>
#include <rw/pstream.h>
main (){
  int j1 = 1;
  int k1 = 2;
  // Save integers to the file "int.dat"
    {
    // Open the stream to save to:
        ofstream          f("int.dat");
        RWpostream        po(f);
    // Use overloaded insertion operator 
    // "RWpostream::operator<<(int)" to save integers:
        po << j1;
        po << k1;
    }
  // Restore integers from the file "int.dat"
    int j2 = 0;
    int k2 = 0;
    {
    // Open a separate stream to restore from:       
        ifstream          f("int.dat");
        RWpistream        pi(f);
    // Use overloaded extraction operator
    // "RWpistream::operator>>(int)" to restore integers:
        pi >> j2;         // j1 == j2
        pi >> k2;         // k1 == k2
    }
  assert(j1 == j2);
  assert(k1 == k2);
  return 0;
}

The preceding example shows how easy it is to use overloaded operators to implement this level of persistence. So, what are some of the problems with using simple persistence? As mentioned above, one problem is that simple persistence will not maintain the pointer relationships among objects. We'll take a look at this problem in the next example.

Example Two: Simple Persistence and Pointers

This example shows one of the shortcomings of simple persistence: its inability to maintain the pointer relationships among persisted objects. Let's say that you have a class Developer that contains a pointer to other Developer objects:

Developer {
  public:
    Developer(const char* name, const Developer* anAlias = 0L)
      : name_(name), alias_(anAlias) {}
  RWCString        name_;  // Name of developer.
  const Developer* alias_;  // Alias points to another Developer.
};

Now let's say that you have another class, Team, that is an array of pointers to Developers:

class Team {
  public:
    Developer*  member_[3];
};

Note that Team::member_ doesn't actually contain Developers, but only pointers to Developers.

We'll assume that you've written overloaded extraction and insertion operators that use simple persistence to save and restore Developers and Teams. The example code for this is omitted to keep the explanation from getting cluttered.

When you save and restore a Team with simple persistence, what you restore may be different from what you saved. Let's look at the following code, which creates a team, then saves and restores it with simple persistence.

main (){
  Developer*  kevin   = new Developer("Kevin");
  Developer*  rudi    = new Developer("Rudi", kevin);
  Team        team1;
  team1.member_[0] = rudi;
  team1.member_[1] = rudi;
  team1.member_[2] = kevin;
  // Save with simple persistence:
  {
    RWFile    f("team.dat");
    f << team1;  // Simple persistence of team1.
  }
  // Restore with simple persistence:
  Team       team2;
  {
    RWFile           f("team.dat");
    f >> team2;
  }
  return 0;
}

Because this example uses simple persistence, which does not maintain pointer relationships, the restored team has different pointer relationships than the original team. shows what the created and restored teams look like in memory if you run the program.

Figure 13-1. Simple Persistence

Figure 14-1 Simple Persistence

As you can see in , when objects that refer to each other are saved and then are restored with simple persistence, the morphology among the objects can change. This is because simple persistence assumes that every pointer reference to an object in memory refers to a unique object. Thus, when such objects are saved, two references to the same memory location will cause two copies of the contents of that memory location to be saved, and later restored.

Isomorphic Persistence

Isomorphic persistence is the storage and retrieval of objects to and from a stream such that the pointer relationships between the objects are preserved. If there are no pointer relationships, isomorphic persistence effectively saves and restores objects the same way as simple persistence. When a collection is isomorphically persisted, all objects within that collection are assumed to have the same type. (A collection of objects of the same type is called a homogeneous collection.)

You can use isomorphic persistence to save and restore any Tools.h++ class listed in above. In addition, you can add isomorphic persistence to a class by following the technique described in "Designing Your Class to Use Isomorphic Persistence."

Note that in this implementation of Tools.h++, isomorphic persistence of types templatized on pointers is not supported. For example, saving and restoring RWTValDlist<int*> is not supported.[18] In this case, we suggest that you use RWTPtrDlist<int> instead.

Table 13-2. Isomorphic Persistence Classes

Category

Description

Rogue Wave Standard C++ Library-based collection classes

RWTValDeque,RWTPtrMap,…

RWCollectable (Smalltalk-like) classes

RWCollectableDate,RWCollectableString... Note that RWCollectable classes also provide polymorphic persistence (see "Polymorphic Persistence" in this chapter).

RWCollection classes that derive from RWCollectable

RWBinaryTree,RWBag,…

Rogue Wave Tools.h++ 6.x templatized collections

RWTPtrDlist, RWTValDlist, RWTPtrSlist, RWTValSlist, RWTPtrOrderedVector, RWTValOrderedVector, RWTPtrSortedVector, RWTValSortedVector, RWTPtrVector, RWTValVector


Isomorphic versus Simple Persistence

Let's look at a couple of illustrations that show the difference between isomorphic and simple persistence. In , a collection of multiple pointers to the same object is saved to and restored from a stream, using simple persistence. Notice that when the collection is stored and restored in , each pointer points to a distinct object. Contrast this with the isomorphic persistence of the same collection, shown in , in which all of the restored pointers point to the same object, just as they did in the original collection.

Figure 13-2. Saving And Restoring With Simple Persistence

Figure 14-2 Saving And Restoring With Simple Persistence

Figure 13-3. Saving and Restoring a Collection with Isomorphic Persistence

Figure 14-3 Saving and Restoring a Collection with Isomorphic Persistence

In , we attempt to save and restore a circularly-linked list, using simple persistence. As shown in the figure, any attempt to use simple persistence to save a circularly-linked list results in an infinite loop.

The simple persistence mechanism creates a copy of each object that is pointed to and saves that object to a stream. But the simple persistence mechanism doesn't remember which objects it has already saved. When the simple persistence mechanism encounters a pointer, it has no way of knowing whether it has already saved that pointer's object. So in a circularly-linked list, the simple persistence mechanism saves the same objects over and over and over as the mechanism cycles through the list forever.

On the other hand, as shown in , isomorphic persistence allows us to save the circularly-linked list. The isomorphic persistence mechanism uses a table to keep track of pointers it has saved. When the isomorphic persistence mechanism encounters a pointer to an unsaved object, it copies the object data, saves that object data—not the pointer—to the stream, then keeps track of the pointer in the save table. If the isomorphic persistence mechanism later encounters a pointer to the same object, instead of copying and saving the object data, the mechanism saves the save table's reference to the pointer.

When the isomorphic persistence mechanism restores pointers to objects from the stream, the mechanism uses a restore table to reverse the process. When the isomorphic persistence mechanism encounters a pointer to an unrestored object, it recreates the object with data from the stream, then changes the restored pointer to point to the recreated object. The mechanism keeps track of the pointer in the restore table. If the isomorphic persistence mechanism later encounters a reference to an already-restored pointer, then the mechanism looks up the reference in the restore table, and updates the restored pointer to point to the object referred to in the table.

Figure 13-4. Attempt to Save and Restore a Circularly-linked List with Simple Persistence

Figure 14-4 Attempt to Save and Restore a Circularly-linked List with Simple Persistence

Figure 13-5. Saving and Restoring a Circularly-linked List with Isomorphic Persistence

Figure 14-5 Saving and Restoring a Circularly-linked List with Isomorphic Persistence

Isomorphic Persistence of a Tools.h++ Class

The following example shows the isomorphic persistence of a templatized collection of RWCollectable integers, RWTPtrDlist<RWCollectableInt>.RWTPtrDlist is a templatized, reference-based, doubly-linked list that uses isomorphic persistence to store pointer references to values in memory.

This example uses RWCollectableInt instead of int because ints use simple persistence. By using RWCollectableInts, we can implement isomorphic persistence.

When RWTPtrDlist is saved and then restored, the pointer relationships of the restored list will have the same morphology as the original list.

#include <assert.h>
#include <rw/tpdlist.h>  // RWTPtrDlist
#include <rw/collint.h>  // RWCollectableInt
#include <rw/rwfile.h>   // RWFile
main (){
  RWTPtrDlist<RWCollectableInt> dlist1;
  RWCollectableInt    *one = new RWCollectableInt(1);
  dlist1.insert(one);
  dlist1.insert(one);
  {
    RWFile           f("dlist.dat");
    f << dlist1;  // Isomorphic persistence of dlist1.
  }
  assert(dlist1[0] == one && dlist1[0] == dlist1[1]);
    // dlist1[0], dlist[1] and "one" all point to the
    // same place.
  RWTPtrDlist<RWCollectableInt> dlist2;
  {
    RWFile            f("dlist.dat");
    f >> dlist2; 
      // restore dlist2 from f
      // dlist2 now contains 2 pointers
      // to the same RWCollectableInt of value 1.
      // However, this RWCollectableInt isn't at 
      // the same address as the value 
      // that "one" points to.
  }
  // See  below to see what dlist1 and dlist2
  // now look like in memory.
  assert(dlist2[0] == dlist2[1] && (*dlist2[0]) == *one);
    // dlist2[0] and dlist2[1] point to the same place
    // and that place has the same value as "one".
  delete dlist2[0];
  delete one;
    // The developer must allocate and delete objects.
    // The templatized collection member function
    // clearAndDestroy() doesn't check that a given
    // pointer is deleted only once.
    // So in this case, delete the shared
    // pointer manually.
 
  return 0;   
}

Figure 13-6. After Isomorphic Save and Restore of RWTPtrDlist<RWCollectableInt>

Figure 14-6 After Isomorphic Save and Restore of RWTPtrDlist<RWCollectableInt>

Designing Your Class to Use Isomorphic Persistence

Table 14-2 lists the Tools.h++ classes that implement isomorphic persistence. You can also add isomorphic persistence to an existing class, even if you only have the header files for that class. Before you can add isomorphic persistence to a class, it must meet the following requirements:

  • Class T must have appropriate default and copy constructors defined or generated by the compiler:

    T();                 // default constructorT(T& t);             // copy constructor

  • Class T must have an assignment operator defined as a member or as a global function:

    T& operator=(const T& t);           // member functionT& operator=(T& lhs, const T& rhs); // global function

  • Class Tcannot have any non-type template parameters. For example, in RWTBitVec<size>, "size" is placeholder for a value rather than a type. No present compiler accepts function templates with non-type template parameters, and the global functions used to implement isomorphic persistence (rwRestoreGuts and RWSaveGuts) are function templates when they are used to persist templatized classes.

  • Class T must use the macros RW_DECLARE_PERSISTABLE and RW_DEFINE_PERSISTABLE or their equivalents. More about this below in "Add RWDECLARE_PERSISTABLE to Your Header File" and "Add RWDEFINE_PERSISTABLE to One Source File."

  • All the data necessary to recreate an instance of Class T must be globally available (have accessor functions). If you can't make this data available, you can't implement isomorphic persistence. More about this in "Make All Necessary Class Data Available."

If your class T will be stored in a Standard C++ Library container or a Standard C++ Library-based collection, you may need to implement operator<(const T&, const T&) and operator==(const T&, const T&). See "Keeping the Standard C++ Library in Mind for Portability" in Chapter 11 for more information.

To create an isomorphically persistent class or to add isomorphic persistence to an existing class, follow these steps:

  1. Make all necessary class data available.

  2. Add RWDECLARE_PERSISTABLE to your header file.

  3. Add RWDEFINE_PERSISTABLE to one source file.

  4. Check for possible problems.

  5. Define rwSaveGuts and rwRestoreGuts.

Make All Necessary Class Data Available

All class data that will be isomorphically persisted must be accessible to the global functions, rwSaveGuts and rwRestoreGuts, used to implement persistence for the class.

Note that only the information necessary to recreate an object of that class must be accessible to rwSaveGuts and rwRestoreGuts. Other data can be kept protected or private.

There are several ways to make protected and private data members of classes accessible.

First, your class could make friends with rwSaveGuts and rwRestoreGuts:

class Friendly {// These global functions access private members.
  friend void rwSaveGuts(RWvostream&, const Friendly&);
  friend void rwRestoreGuts(RWFile&, Friendly&);
  friend void rwSaveGuts(RWFile&, const Friendly&);
  friend void rwRestoreGuts(RWvistream&, Friendly&);
//...
};

Or your class could have accessor functions to the restricted but necessary members:

class Accessible {
public:
   int  secret(){return secret_}
   void secret(const int s){secret  = s}
//...
private:
   int   secret_;
};

If you can't change the source code for the class to which you want to add isomorphic persistence, then you could consider deriving a new class that provides access via public methods or friendship:

class Unfriendly{
protected:
   int secret_;
// ...
};
 
class Friendlier : public Unfriendly {
public:
   int  secret(){return secret_}
   void secret(const int s){secret_ = s}
//...
};

If you can't change the source code for a class, you will be unable to isomorphically persist private members of that class. But remember: you only need access to the data necessary to recreate the class object, not to all the members of the class. For example, if your class has a private cache that is created at run time, you probably don't need to save and restore the cache. Thus, even though that cache is private, you don't need access to it in order to persist the class object.

Add RWDECLARE_PERSISTABLE to Your Header File

Once you have determined that all necessary class data is accessible, you must add declaration statements to your header files. These statements declare the global functions operator<< and operator>> for your class. The global functions permit storage to and retrieval from RWvistream, RWvostream and RWFile.

Tools.h++ provides several macros that make adding these declarations easy. The macro you choose depends upon whether your class is templatized or not, and if it is templatized, how many templatized parameters it has.

  • For non-templatized classes, use RWDECLARE_PERSISTABLE.

    RWDECLARE_PERSISTABLE is a macro found in rw/edefs.h. To use it, add the following lines to your header file (*.h):

    #include <rw/edefs.h>
    RWDECLARE_PERSISTABLE(YourClass)

    RWDECLARE_PERSISTABLE(YourClass) will expand to declare the following global functions:

    RWvostream& operator<<(RWvostream& strm, const YourClass& item);
    RWvistream& operator>>(RWvistream& strm, YourClass& obj);
    RWvistream& operator>>(RWvistream& strm, YourClass*& pObj);
    RWFile& operator<<(RWFile& strm, const YourClass& item);
    RWFile& operator>>(RWFile& strm, YourClass& obj);
    RWFile& operator>>(RWFile& strm, YourClass*& pObj);

  • For templatized classes with a single template parameterT, use the macro RWDECLARE_PERSISTABLE_TEMPLATE.

    RWDECLARE_PERSISTABLE_TEMPLATE is also found in rw/edefs.h. To use it, add the following lines to your header file (*.h):

    #include <rw/edefs.h>
    RWDECLARE_PERSISTABLE_TEMPLATE(YourClass)
    

    RWDECLARE_PERSISTABLE_TEMPLATE(YourClass) will expand to declare the following global functions:

    template<class T> 
    RWvostream& operator<<
    (RWvostream& strm, const YourClass<T>& item);
    template<class T> 
    RWvistream& operator>>
    (RWvistream& strm, YourClass<T>& obj);
    template<class T> 
    RWvistream& operator>>
    (RWvistream& strm, YourClass<T>*& pObj);
    template<class T> 
    RWFile& operator<<(RWFile& strm, const YourClass<T>& item);
    template<class T> 
    RWFile& operator>>(RWFile& strm, YourClass<T>& obj);
    template<class T> 
    RWFile& operator>>(RWFile& strm, YourClass<T>*& pObj);

  • For templatized classes with more than one and less than five template parameters, use one of the following macros from rw/edefs.h.:

    // For YourClass<T1,T2>:
    RWDECLARE_PERSISTABLE_TEMPLATE_2(YourClass)
    // For YourClass<T1,T2,T3>:
    RWDECLARE_PERSISTABLE_TEMPLATE_3(YourClass)
    // For YourClass<T1,T2,T3,T4>:
    RWDECLARE_PERSISTABLE_TEMPLATE_4(YourClass)


Note: Remember, if your templatized class has any non-type template parameters, it cannot be isomorphically persisted.


  • If you need to persist templatized classes with five or more

    template parameters, you can write additional macros for RWDECLARE_PERSISTABLE_TEMPLATE_n. The macros are found in the header file rw/edefs.h.

Add RWDEFINE_PERSISTABLE to One Source File

After you have declared the global storage and retrieval operators, you must define them. Tools.h++ provides macros that add code to your source file[19] to define the global functions operator<< and operator>> for storage to and retrieval from RWvistream, RWvostream, and RWFile. RWDEFINE_PERSISTABLE macros will automatically create global operator<< and operator>> functions that perform isomorphic persistence duties and call the global persistence functions rwSaveGuts and rwRestoreGuts for your class. More about rwSaveGuts and rwRestoreGuts later.

Again, your choice of which macro to use is determined by whether your class is templatized, and if so, how many parameters it requires.

  • For non-templatized classes, use RWDEFINE_PERSISTABLE.

    RWDEFINE_PERSISTABLE is a macro found in rw/epersist.h. To use it, add the following lines to one and only one source file (*.cpp or *.C):

    #include <rw/epersist.h>
    RWDEFINE_PERSISTABLE(YourClass)

    RWDEFINE_PERSISTABLE(YourClass) will expand to generate the source code for (that is, to define) the following global functions:

    RWvostream& operator<<(RWvostream& strm, const YourClass& item)
    RWvistream& operator>>(RWvistream& strm, YourClass& obj)
    RWvistream& operator>>(RWvistream& strm, YourClass*& pObj)
     
    RWFile& operator<<(RWFile& strm, const YourClass& item)
    RWFile& operator>>(RWFile& strm, YourClass& obj)
    RWFile& operator>>(RWFile& strm, YourClass*& pObj)

  • For templatized classes with a single template parameterT, use RWDEFINE_PERSISTABLE_TEMPLATE.

    RWDEFINE_PERSISTABLE_TEMPLATE is also found in rw/epersist.h. To use it, add the following lines to one and only one source file (*.cpp or *.C):

    #include <rw/epersist.h>
    RWDEFINE_PERSISTABLE_TEMPLATE(YourClass)

    RWDEFINE_PERSISTABLE_TEMPLATE(YourClass) will expand to generate the source code for the following global functions:

    template<class T> 
    RWvostream& operator<<
    (RWvostream& strm, const YourClass<T>& item)
     
    template<class T> 
    RWvistream& operator>>
    (RWvistream& strm, YourClass<T>& obj)
     
    template<class T> 
    RWvistream& operator>>
    (RWvistream& strm, YourClass<T>*& pObj)
     
    template<class T> 
    RWFile& operator<<(RWFile& strm, const YourClass<T>& item)
     
    template<class T> 
    RWFile& operator>>(RWFile& strm, YourClass<T>& obj)
     
    template<class T> 
    RWFile& operator>>(RWFile& strm, YourClass<T>*& pObj)

  • For templatized classes with more than one and less than five template parameters, use one of the following macros from rw/epersist.h:

    // For YourClass<T1,T2>:
    RWDEFINE_PERSISTABLE_TEMPLATE_2(YourClass)
     
    // For YourClass<T1,T2,T3>:
    RWDEFINE_PERSISTABLE_TEMPLATE_3(YourClass)
     
    // For YourClass<T1,T2,T3,T4>:
    RWDEFINE_PERSISTABLE_TEMPLATE_4(YourClass)


Note: Remember, if your templatized class has any non-type template parameters, it cannot be isomorphically persisted.


  • If you need to persist templatized classes with five or more template parameters, you can write additional macros for RWDEFINE_PERSISTABLE_TEMPLATE_n. The macros are found in

    the header file rw/epersist.h.

Check for Possible Problems

You've made the necessary data accessible, and declared and defined the global functions required for isomorphic persistence. Before you go any further, you need to review your work for two possible problems.

  1. You can't use the RWDECLARE_PERSISTABLE_TEMPLATE and RWDEFINE_PERSISTABLE_TEMPLATE macros to persist any templatized class that has non-type template parameters. Templates with non-type template parameters, such as RWTBitVec<size>, cannot be isomorphically persisted.

  2. If you have defined any of the following global operators and you use the RWDEFINE_PERSISTABLE macro, you will get compiler ambiguity errors.

    RWvostream& operator<<(RWvostream& s, const YourClass& t);
    RWvistream& operator>>(RWvistream& s, YourClass& t);
    RWvistream& operator>>(RWvistream& s, YourClass*& pT);
     
    RWFile& operator<<(RWFile& s, const YourClass& t);
    RWFile& operator>>(RWFile& s, YourClass& t);
    RWFile& operator>>(RWFile& s, YourClass*& pT);
     

    The compiler errors occur because using RWDEFINE_PERSISTABLE along with a different definition of the operators defines the operators twice. This means that the compiler does not know which operator definition to use. In this case, you have two choices:

    • Remove the operator<< and operator>> global functions that you previously defined for YourClass and replace them with the operators generated by the RWDEFINE_PERSISTABLE(YourClass).

    • Modify your operator<< and operator>> global functions for YourClass using the contents of the RWDEFINE_PERSISTABLE macro in rw/epersist.h as a guide.

Define rwSaveGuts and rwRestoreGuts

Now you must add to one and only onesource file the global functions rwSaveGuts and rwRestoreGuts, which will be used to save and restore the internal state of your class. These functions are called by the operator<< and operator>> that were declared and defined as discussed in "Add RWDECLARE_PERSISTABLE to Your Header File" and "Add RWDEFINE_PERSISTABLE to One Source File" above.

Note: "Writing rwSaveGuts and rwRestoreGuts Functions" provides guidelines about how to write rwSaveGuts and rwRestoreGuts global functions.

  • For non-templatized classes , define the following functions:

    void rwSaveGuts(RWFile& f, const YourClass& t){/*…*/}
    void rwSaveGuts(RWvostream& s, const YourClass& t) {/*…*/}
    void rwRestoreGuts(RWFile& f, YourClass& t) {/*…*/}
    void rwRestoreGuts(RWvistream& s, YourClass& t) {/*…*/}

  • For templatized classes with a single template parameterT, define the following functions:

    template<class T> void 
    rwSaveGuts(RWFile& f, const YourClass<T>& t){/*…*/}
    template<class T> void 
    rwSaveGuts(RWvostream& s, const YourClass<T>& t) {/*…*/}
    template<class T> void 
    rwRestoreGuts(RWFile& f, YourClass<T>& t) {/*…*/}
    template<class T>void 
    rwRestoreGuts(RWvistream& s, YourClass<T>& t) {/*…*/}

  • For templatized classes with more than one template parameter, define rwRestoreGuts and rwSaveGuts with the appropriate number of template parameters.

Function rwSaveGuts saves the state of each class member necessary for persistence to an RWvostream or an RWFile. If the members of your class can be persisted (see above), and if the necessary class members are accessible to rwSaveGuts, you can use operator<< to save the class members.

Function rwRestoreGuts restores the state of each class member necessary for persistence from an RWvistream or an RWFile. Provided that the members of your class are types that can be persisted, and provided that the members of your class are accessible to rwRestoreGuts, you can use operator>> to restore the class members.

Writing rwSaveGuts and rwRestoreGuts Functions

The next two sections discuss guidelines for writing rwSaveGuts and rwRestoreGuts global functions. To illustrate these guidelines, the following class will be used:

class Gut {
  public:
    int                   fundamentalType_;
    RWCString             aRogueWaveObject_;
    RWTValDlist           anotherRogueWaveObject_;
    RWCollectableString   anRWCollectable_
    RWCollectableString*  pointerToAnRWCollectable_;
    Gut*                  pointerToAnObject_;
};

The discussion in the next two sections describes how to write rwSaveGuts and rwRestoreGuts functions for non-templatized classes. However, the descriptions also apply to the templatized rwSaveGuts and rwRestoreGuts that are written for templatized classes.

Guidelines for Writing rwSaveGuts

The global overloaded functions:

rwSaveGuts(RWFile& f, const YourClass& t) 
rwSaveGuts (RWvostream& s, const YourClass& t) 

are responsible for saving the internal state of a YourClass object to either a binary file (using class RWFile) or to a virtual output stream (an RWvostream). This allows the object to be restored at some later time.

The rwSaveGuts functions that you write must save the state of each member in YourClass, including the members of the class that you inherited from.

How you write the functions depends upon the type of the member data:

  • To save member data that are either C++ fundamental types (int, char, float,…), or most Rogue Wave classes, including RWCollectable, use the overloaded insertion operator operator<<.

  • Saving members that are pointers to non-RWCollectable objects can be a bit tricky. This is because it is possible that a pointer does not point to any object at all. One way of dealing with the possibility of nil pointers is to check whether a pointer points to a valid object. If the pointer is valid, save a Boolean true, then save the dereferenced pointer. If the pointer is invalid, save a Boolean false but don't save the pointer.

    When you restore the pointer, rwRestoreGuts first restores the Boolean. If the Boolean is true, then rwRestoreGuts restores the valid pointer. If the Boolean is false, then rwRestoreGuts sets the pointer to nil.

  • Saving pointers to objects derived from RWCollectable is easier. It is still possible that a pointer is nil. But if you use:

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

    to save the pointer, the nil pointer will be detected automatically.

Using these guidelines, you can write rwSaveGuts functions for the example class Gut as follows:

void rwSaveGuts(RWvostream& stream, const Gut& gut) {
 
  // Use insertion operators to save fundamental objects,
  // Rogue Wave objects and pointers to 
  // RWCollectable-derived objects.
 
  stream 
    << gut.fundamentalType_
    << gut.aRogueWaveObject_
    << gut.anotherRogueWaveObject_
    << gut.pointerToAnRWCollectable_;
 
  // The tricky saving of a pointer 
  // to a non-RWCollectable object.
 
  if (gut.pointerToAnObject_ == 0)  // Is it a nil pointer? 
    stream << false;                      // Yes, don't save.
  else {
    stream << true;                       // No, it's valid 
    stream << *(gut.pointerToAnObject_);  // so save it.
  }
}
 
void rwSaveGuts(RWFile& stream, const Gut& gut) {
  // The body of this function is identical to
  // rwSaveGuts(RWvostream& stream, const Gut& gut).
}

Guidelines for Writing rwRestoreGuts

The global overloaded functions:

rwRestoreGuts(RWFile& f, YourClass& t) 
rwRestoreGuts(RWvostream& s, YourClass& t) 

are responsible for restoring the internal state of a YourClass object from either a binary file (using class RWFile) or from a virtual output stream (an RWvostream).

The rwRestoreGuts functions that you write must restore the state of each member in YourClass, including the members of the class that you inherited from. The functions must restore member data in the order that it was saved.

How you write the functions depends upon the type of the member data:

  • To restore member data that are either C++ fundamental types (int, char, float,…) or most Rogue Wave classes, including RWCollectable, use the overloaded extraction operators (operator>>).

  • Restoring members that are pointers to non-RWCollectable objects can be a bit tricky. This is because it is possible that a saved pointer did not point to any object at all. But if rwSaveGuts saved a Boolean flag before saving the pointer, as we described in the previous section, then it is a relatively simple matter for the rwRestoreGuts to restore valid and nil pointers.

    Assuming that the members were saved with a compatible rwSaveGuts, when you restore the pointer, rwRestoreGuts first restores the Boolean. If the Boolean is true, then rwRestoreGuts restores the valid pointer. If the Boolean is false, then rwRestoreGuts sets the pointer to nil.

  • Restoring pointers to objects derived from RWCollectable is easier. It is still possible that the pointer is nil. But if you use:

        RWvostream& operator>>(RWvostream&, const RWCollectable*&);

  • to restore the pointer, the nil pointer will be detected automatically.

Using these guidelines, you can write the rwRestoreGuts functions for the example class Gut as follows:

void rwRestoreGuts(RWvistream& stream, const Gut& gut) {
 
  // Use extraction operators to restore fundamental objects,
  // Rogue Wave objects and pointers to 
  // RWCollectable-derived objects.
 
  stream 
    >> gut.fundamentalType_
    >> gut.aRogueWaveObject_
    >> gut.anotherRogueWaveObject_
    >> gut.pointerToAnRWCollectable_;
 
  // The tricky restoring of a pointer 
  // to a non-RWCollectable object.
 
  bool isValid;
  stream >> isValid;                   // Is it a nil pointer?
 
  if (isValid)                         // No,
    stream >> gut.pointerToAnObject_;  // restore the pointer.
  else                                 // Yes,
    gut.pointerToAnObject_ = rwnil;    // set pointer to nil.
}
 
void rwRestoreGuts(RWFile& stream, Gut& gut) {
  // The body of this function is identical to
  // rwRestoreGuts(RWvostream& stream, Gut& gut).
}

Isomorphic Persistence of a User-designed Class

"Example Two: Simple Persistence and Pointers" described some example code that implements simple persistence on a collection that includes pointers. That example illustrated how simple persistence does not maintain the original collection's morphology.

This example implements isomorphic persistence on the collection we set up in "Example Two: Simple Persistence and Pointers" : Team, which contains three Developers. Figure 14-7 shows the morphology of the original Team collection and of the Team collection after we saved and restored it with isomorphic persistence.

Figure 13-7. Isomorphic Persistence

Figure 14-7 Isomorphic Persistence

As you read the code, notice how the Developer::alias_ member, which points to other Developers, is saved and restored. You'll find that after saving Developer::name_ the rwSaveGuts function for Developer checks to see if alias_ is pointing to a Developer in memory. If not, rwSaveGuts stores a Boolean false to signify that alias_ is a nil pointer. If alias_is pointing to a Developer, rwSaveGuts stores a Boolean true. It is only afterwards that rwSaveGuts finally stores the value of the Developer that alias_ is pointing to.

This code can distinguish between new Developers and existing

Developers because the insertion operators generated by RWDEFINE_PERSISTABLE(Developer) keep track of Developers that have been stored previously. The insertion operator, operator<<, calls the rwSaveGuts if and only if a Developer has not yet been stored in the stream by operator<<.

When a Developer object is restored, the extraction operator, operator>>, for Developer is called. Like the insertion operators, the extraction operators are generated by RWDEFINE_PERSISTABLE(Developer). If a Developer object has already been restored, then the extraction operator will adjust the Developer::alias_ pointer so that it points to the already existing Developer. If the Developer has not yet been restored, then rwRestoreGuts for Developer will be called.

After restoring Developer::name_ , rwRestoreGuts for Developer restores a Boolean value to determine whether Developer::alias_ should point to a Developer in memory or not. If the Boolean is true, then alias_ should point to a Developer, so rwRestoreGuts restores the Developer object. Then rwRestoreGuts updates alias_ to point to the restored Developer.

The isomorphic persistence storage and retrieval process described above for Developer.alias_ can also be applied to the Developer pointers in Team.

Here is the code:

#include <iostream.h>     // For user output.
#include <assert.h>
#include <rw/cstring.h>
#include <rw/rwfile.h>
#include <rw/epersist.h>
//------------------ Declarations ---------------------
//------------------- Developer -----------------------
class Developer {
  public:
    Developer
      (const char* name = "", Developer* anAlias = rwnil)
      : name_(name), alias_(anAlias) {}
  RWCString         name_;
  Developer*        alias_;
};
#include <rw/edefs.h>
RWDECLARE_PERSISTABLE(Developer)
//--------------------- Team --------------------------
class Team {
  public:
    Developer*  member_[3];
};
RWDECLARE_PERSISTABLE(Team);
//---------- rwSaveGuts and rwRestoreGuts -------------
//------------------- Developer -----------------------
RWDEFINE_PERSISTABLE(Developer)
// This macro generates the following insertion and extraction
// operators:
//   RWvostream& operator<<
//     (RWvostream& strm, const Developer& item)
//   RWvistream& operator>>(RWvistream& strm, Developer& obj)
//   RWvistream& operator>>(RWvistream& strm, Developer*& pObj)
//   RWFile& operator<<(RWFile& strm, const Developer& item)
//   RWFile& operator>>(RWFile& strm, Developer& obj)
//   RWFile& operator>>(RWFile& strm, Developer*& pObj)
void rwSaveGuts(RWFile& file, const Developer& developer){
// Called by:
//   RWFile& operator<<(RWFile& strm, const Developer& item)
  file << developer.name_;       // Save name.
  // See if alias_ is pointing to a Developer in memory.  
  // If not, then rwSaveGuts stores a boolean false to signify
  // that alias_ is a nil pointer. 
  // If alias_ is pointing to a Developer, 
  // then rwSaveGuts stores a boolean true 
  // and stores the value of the Developer 
  // that alias_ is pointing to.  
  if (developer.alias_ == rwnil) 
    file << false;                // No alias.
  else {
    file << true;
    file << *(developer.alias_);  // Save alias.
  }
}
 
 
void rwSaveGuts(RWvostream& stream, const Developer& developer) {
// Called by:
//    RWvostream& operator<<
//      (RWvostream& strm, const Developer& item)
  stream << developer.name_;       // Save name.
  // See if alias_ is pointing to a Developer in memory.  
  if (developer.alias_ == rwnil)
    stream << false;                // No alias.
  else {
    stream << true;
    stream << *(developer.alias_);  // Save alias.
  }
}
void rwRestoreGuts(RWFile& file, Developer& developer) {
//  Called by:
//     RWFile& operator>>(RWFile& strm, Developer& obj)
 
  file >> developer.name_;        // Restore name.
 
  // Should developer.alias_ point to a Developer?
  RWBoolean alias;
  file >> alias;
  // If alias_ should point to a Developer, 
  // then rwRestoreGuts restores the Developer object 
  // and then updates alias_ to point to the new Developer.
  if (alias)                       // Yes.
     file >> developer.alias_;
        // Call:
        //    RWFile& operator>>(RWFile& strm, Developer*& pObj)
}
void rwRestoreGuts(RWvistream& stream, Developer& developer) {
// Called by:
//   RWvistream& operator>>(RWvistream& strm, Developer& obj)
 
  stream >> developer.name_;      // Restore name.
 
  // Should developer.alias_ point to a Developer?
  RWBoolean alias;
  stream >> alias;
  if (alias)                     // Yes.
    stream >> developer.alias_;  
       // Restore alias and update pointer. 
       // Calls:
       //    RWvistream& operator>>
       //      (RWvistream& strm, Developer*& pObj)
}
// For user output only:
ostream& operator<<(ostream& stream, const Developer& d) {
  stream << d.name_
    << " at memory address: " << (void*)&d;
  if (d.alias_)
    stream << " has an alias at memory address: " 
      << (void*)d.alias_ << " ";
  else
    stream << " has no alias.";
  return stream;
}
//--------------------- Team -------------------------------
RWDEFINE_PERSISTABLE(Team);
// This macro generates the following insertion and extraction
// operators:
//    RWvostream& operator<<
//      (RWvostream& strm, const Team& item)
//    RWvistream& operator>>(RWvistream& strm, Team& obj)
//    RWvistream& operator>>(RWvistream& strm, Team*& pObj)
//    RWFile& operator<<(RWFile& strm, const Team& item)
//    RWFile& operator>>(RWFile& strm, Team& obj)
//    RWFile& operator>>(RWFile& strm, Team*& pObj)
void rwSaveGuts(RWFile& file, const Team& team){
// Called by RWFile& operator<<(RWFile& strm, const Team& item)
 
  for (int i = 0; i < 3; i++)
    file << *(team.member_[i]);    
      // Save Developer value.
      // Call:
      //   RWFile& operator<<
      //     (RWFile& strm, const Developer& item)
}
void rwSaveGuts(RWvostream& stream, const Team& team) {
// Called by:
//   RWvostream& operator<<(RWvostream& strm, const Team& item)
  for (int i = 0; i < 3; i++)
    stream << *(team.member_[i]);
      // Save Developer value.
      // Call:
      //   RWvostream& operator<<
      //     (RWvostream& strm, const Developer& item)
}
void rwRestoreGuts(RWFile& file, Team& team) {
// Called by RWFile& operator>>(RWFile& strm, Team& obj)
  for (int i = 0; i < 3; i++)
    file >> team.member_[i];   
      // Restore Developer and update pointer.
      // Call:
      //    RWFile& operator>>(RWFile& strm, Developer*& pObj)
}
void rwRestoreGuts(RWvistream& stream, Team& team) {
// Called by:
//   RWvistream& operator>>(RWvistream& strm, Team& obj)
  for (int i = 0; i < 3; i++)
     stream >> team.member_[i]; 
       // Restore Developer and update pointer.
       // Call:
       //    RWvistream& operator>>
       //      (RWvistream& strm, Developer*& pObj)
}
// For user output only:
 
ostream& operator<<(ostream& stream, const Team& t) {
  for (int i = 0; i < 3; i++)
    stream << "[" << i << "]:" << *(t.member_[i]) << endl;
  return stream;
}
 
//-------------------- main --------------------------
main (){
  Developer*  kevin   = new Developer("Kevin");
  Developer*  rudi    = new Developer("Rudi", kevin);
  Team        team1;
  team1.member_[0] = rudi;
  team1.member_[1] = rudi;
  team1.member_[2] = kevin;
  cout << "team1 (before save):" << endl
    << team1 << endl << endl;      // Output to user.
  {
    RWFile     f("team.dat");
    f << team1;       // Isomorphic persistence of team.
  }
 
  Team        team2;
  {
    RWFile                f("team.dat");
    f >> team2;
  }
  cout << "team2 (after restore):" << endl
    << team2 << endl << endl;   // Output to user.
  delete kevin;
  delete rudi;
  return 0;
}

Output:

team1 (before save):
[0]:Rudi at memory address: 0x10002be0 
    has an alias at memory address: 0x10002bd0
[1]:Rudi at memory address: 0x10002be0 
    has an alias at memory address: 0x10002bd0
[2]:Kevin at memory address: 0x10002bd0 has no alias.
team2 (after restore):
[0]:Rudi at memory address: 0x10002c00 
    has an alias at memory address: 0x10002c10
[1]:Rudi at memory address: 0x10002c00 
    has an alias at memory address: 0x10002c10
[2]:Kevin at memory address: 0x10002c10 has no alias.

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 14-3 lists the classes that use polymorphic persistence.

Table 13-3. Polymorphic Persistence Classes

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 RWCollectableobjects:

    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 RWCollectablepointers:

    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 RWCollectableobjects:

    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:

  • 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.

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'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"

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 13-8. Polymorphic Persistence

Figure 14-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[20] . 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" in Chapter 15). 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.

A Few Friendly Warnings

Persistence is a useful quality, but requires care in some areas. Here are a few things to look out for when you use persistence on objects.

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 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_.  
  };
 
  RWDEFINE_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's 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.

Don't Save Distinct Objects with the Same Address

You must be careful not to isomorphically save distinct objects that may have the same address. The internal tables that are used in isomorphic and polymorphic persistence use the address of an object to determine whether or not an object has already been saved.

The following example assumes that all the work has been done to make Godzilla and Mothra isomorphically persistable:

class Mothra {/* … */};
RWDEFINE_PERSISTABLE(Mothra)
 
struct Godzilla {
  Mothra  mothra_;
  int     wins_;
};
 
RWDEFINE_PERSISTABLE(Godzilla)
/*… */
Godzilla  godzilla;
/* … */
stream << godzilla;
/* … */
stream >> godzilla;       // The restore may be garbled!

When godzilla is saved, the address of godzilla will be saved in an isomorphic save table. The next item to be saved is godzilla.mothra_. Its address is saved in the same internal save table.

The problem is that on some compilers godzilla and godzilla.mothra_ have the same address! Upon restoration of godzilla, godzilla.mothra_ is streamed out as a value, and godzilla is streamed out as a reference to godzilla.mothra_. If godzilla and godzilla.mothra have the same address, the restore of godzilla fails because the extraction operator attempts to initialize godzilla with the contents of godzilla.mothra_.

There are two ways to overcome this difficulty. The first is to structure your class so that simple data members, such as int, precede data members that are isomorphically persistent. Using this method, class Godzilla looks like this:

struct Godzilla {
  int      wins_;
  Mothra mothra_;    // mothra_ now has a different address.
};

If Godzilla is structured as shown here, mothra_ is displaced from the front of godzilla and can't be confused with godzilla. The variable wins_, of type int, is saved with simple persistence and is not stored in the isomorphic save table.

The second approach to solving the problem of identical addresses between a class and its members is to insert an isomorphically persistable member as a pointer rather than a value. For Godzilla this would look like:

struct Godzilla {
  Mothra* mothraPtr_;// mothraPtr_ points to a different address.
  int     wins_;
};

In this second approach, mothraPtr_ points to a different address than godzilla, so confusion is once again avoided.

Don't Use Sorted RWCollections to Store Heterogeneous RWCollectables

When you have more than one different type of RWCollectable stored in an RWCollection, you can't use a sorted RWCollection. For example, this means that if you plan to store RWCollectableStrings and RWCollectableDates in the same RWCollection, you can't store them in a sorted RWCollection such as RWBtree. The sorted RWCollections are RWBinaryTree, RWBtree, RWBTreeDictionary, and RWSortedVector.

The reason for this restriction is that the comparison functions for sorted RWCollections expect that the objects to be compared will have the same type.

Define All RWCollectables That Will Be Restored

Make certain that your program declares variables of all possible RWCollectable objects that you might restore. For an example of this practice, see the preceding "Two Examples of Simple Persistence."

These declarations are of particular concern when you save an RWCollectable in a collection, then attempt to take advantage of polymorphic persistence by restoring the collection in a different program, without using the RWCollectable that you saved. If you don't declare the appropriate variables, during the restore attempt the RWFactory will throw an RW_NOCREATE exception for some RWCollectable class ID that you know exists. The RWFactory won't throw an RW_NOCREATE exception when you declare variables of all the RWCollectables that could be polymorphically restored.

The problem occurs because your compiler's linker only links the code that RWFactory needs to create the missing RWCollectable when that RWCollectable is specifically mentioned in your code. Declaring the missing RWCollectables gives the linker the information it needs to link the appropriate code needed by RWFactory.



[18] C++ template mechanisms prevent us from being able to do this. However, this restriction is probably a good thing—pointers saved at one location are likely to be troublesome indeed when injected into another.

[19] You may find for template classes that with some compilers the source file must have the same base name as the header file where RWDECLARE_PERSISTABLE was used.

[20] Actually, the Smalltalk collection classes are so similar that they all share the same version of restoreGuts(), inherited from RWCollection.