Boost logo

Boost :

From: Dave Harris (brangdon_at_[hidden])
Date: 2003-10-05 11:19:58


In-Reply-To: <01C389BC.BF188CE0_at_[hidden]>
ramey_at_[hidden] (Robert Ramey) wrote (abridged):
> Here is a slightly longer explantation:
>
> case I Serialization objects already created - the most common case
>
> Objects already exist no creation necessary. serialization saves
> and reloads their current state.
>
> case II Serialization of pointers
>
> Objects must be created. Default non-intrusive load uses a default
> constructor ( which may be private). If an application requires
> a non - default constructor a non - default load and save must
> be specified. Usually this will
>
> a) save construction parameters before saving the object.
> b) load the saved construction parameters before creating the object.
> At the time of object creation the construction parameters are
> available so all is well.

I think part of the issue is how the memory is managed. As I understand
it, currently the library combines memory allocation with construction in
both cases. The first case has a user-function like:

   template<class Archive, class T>
   void load(Archive & ar, T & t, unsigned int version);

where t is already allocated and constructed before the function is
called. The second case has a user-function like:

    template<class Archive ar, class T>
    void load(Archive & ar, T * &t, unsigned int file_version);

which is responsible for both allocation and construction and loading.

Did you consider separating out allocation in this second case?
Specifically, adding an function which just does construction and loading,
with a default implementation like:

   template<class Archive, class T>
   void load_construct(Archive & ar, T *pt, unsigned int version)
   {
       new( pt ) T(); // Default-construct in place.
       load( ar, *pt, version ); // Call load for constructed object.
   }

Then the pointer version of load could be rewritten something like:

    template<class Archive ar, class T>
    void load(Archive & ar, T * &t, unsigned int file_version)
    {
        char *p = NULL;
        try{
            p = new char[sizeof(T)];
            t = reinterpret_cast<T*>(p);
            load_construct( ar, t, file_version );
        }
        catch(...){
            delete p;
            throw;
        }
    }

which does not need to be overridden. Users can override load_construct
instead. That is less work for them because load_construct is smaller and
simpler.

Given that, the vector case could be handled by allocating some memory on
the stack, using load_construct to initialise it, and then using push_back
to copy it into the vector. Presumably this is similar to how it got into
the vector in the first place. Users who did not want to provide a default
constructor would override load_construct, just as they would for the
pointer case, and this would handle the vector case too. Eg:

   template<class Archive>
   void load_construct(Archive & ar, my_class *pt, unsigned int version)
   {
       int a;
       ar >> a;
       new( pt ) my_class( a );
       load( ar, *pt, version );
   }

Load_construct may also provide some support for the intrusive idiom of
passing the Archive right through to the constructor:

   template<class Archive>
   void load_construct(Archive & ar, my_class *pt, unsigned int version)
   {
       new( pt ) my_class( ar, version );
   }

although I don't want to restart the argument about whether this is a good
idea :-)

We can probably improve on copying the object into the vector by using
move semantics. Where we have a C array, we can probably allocate it as
bytes and construct the objects in-place.

   
> In such a case the most natural (to me) interpretation was to clear the
> vector and re-create the vector elements - STL calls default constructor
> in this case which you chose to make private to inhibit other potential
> callers.
>
> My response was that I didn't believe that one should even have
> a default constructor in such cases. I showed how to override
> the collection load function to implement such a point of view.
>
> Upon reflection, I've come to believe my response was beside the point
> and confused the issue. I am going to tweak the library so that
> loading of collections is handled specially and given access to
> any private default constructor. I now believe that this will not
> create any of the problems I originally envisioned.

I think your initial instincts were right. Some classes should not have a
default constructor, not even a private one. Nobody, not even
serialisation, should have the ability to create the class in an invalid
state.

It complicates the class invariant. In effect we would have two class
invariants, a normal one from the normal constructor, and a "special" one
from the private default constructor. This affects two functions: the
destructor, and the pointer version of load. Both of these would have to
know which constructor was used to construct their object.

It may not be a big deal in practice but we should avoid this situation if
we can. I think load_construct does that.

-- Dave Harris, Nottingham, UK


Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk