Boost logo

Boost :

From: David Abrahams (dave_at_[hidden])
Date: 2005-09-22 16:59:37


Joaquín Mª López Muñoz <joaquin_at_[hidden]> writes:

> David Abrahams ha escrito:
>
>> Joaquín Mª López Muñoz <joaquin_at_[hidden]> writes:
>>
>> > In my serialization stuff for Boost.MultiIndex I actually have a
>> > serializable type that does not conform to the equivalence rule. Its
>> > layout kinda looks like:
>> >
>> > template<typename Value>
>> > struct node
>> > {
>> > value v;
>> >
>> > template<class Archive>
>> > void serialize(Archive& ar,const unsigned int)
>> > {
>> > // do nothing
>> > }
>> > }
>> >
>> > I use this weird construct to make node trackable, but no contents
>> > information is dumped to the archive (that is taken care of somewhere
>> > else in the program). In case you're curious, this arises in connection
>> > with serialization of iterators.
>>
>> I can't imagine why you'd need that; a hint would help me to
>> understand better.
>
> It's a little hard to grasp; this particular issue took me literally
> weeks of thinking, but I'll try to explain it a little more. Beware this
> doesn't add much to our current discussion, you might want to skip:
>
> Suppose we are implemeting serialization for a custom container:
> save and load are straight enough:
>
> class container{
> save(...)
> {
> for(const_iterator it=begin,it_end=end();it!=it_end;++it){
> ar<<*it;
> }
> }
> load(...)
> {
> clear();
> for(iterator it=begin,it_end=end();it!=it_end;++it){
> value_type v;
> ar>>v;
> push_back(v);
> ar.reset_object_address(&v,&back());

Assuming value_type is default constructible and push_back doesn't
invalidate any addresses of other objects, I guess so. But in that
case I'd still preallocate enough elements and deserialize them in
place.

> }
> }
> };
>
> Now we want to add serialization for iterators. One ugly way would be
> as follows:
>
> class iterator{
> save(...){
> ar<<&(operator*()); // save pointer to element
> }
> load(...){
> value_type* pv;
> ar>>pv;
> node* pn;
> // cast from pv to pn: possibly nonportable.
> assign(pn);

Clearly. The "right thing to do" is to serialize all the nodes as part of
serializing the container. Then this "just works," no?

> }
> };
>
> This is potentially nonportable and, besides, won't work for nontracked
> value_types.
> What we want is to archive pointers to the internal nodes, rather than the values:

Right.

> class iterator
> {
> save(...){
> ar<<node_ptr;
> }
> load(...){
> node* pn;
> ar>>pn;
> node_ptr=pn;
> }
> };
>
> But for this to work, nodes must be serialized first so that they can be tracked
> later.
>
> class container{
> save(...)
> {
> for(const_iterator it=begin,it_end=end();it!=it_end;++it){
> ar<<*it; // save value
> ar<<*it.node_ptr; // save node

Why wouldn't your node just implement serialization that serializes
its contained value?

> }
> }
> load(...)
> {
> clear();
> for(iterator it=begin,it_end=end();it!=it_end;++it){
> value_type v;
> ar>>v;
> push_back(v);
> ar.reset_object_address(&v,&back());
> ar>>*(--end()).node_ptr; // "load" node
> }
> }
> };
>
> That's the purpose of node serialization stuff. The implementation does nothing
> except signalling Boost.Serialization where later node pointers must
> point to.

Well, I could probably get this if I thought hard enough about it, but
I don't yet. Of course I could be missing something, it seems like
a hack to me. Serializing and deserializing the nodes directly seems
a lot cleaner.

>> > * An input archive iar is compatible with an output archive oar if
>> > 1. iar allows a sequence of >> ops matching the corresponding << ops
>> > made upon oar (matching defined in terms of types involved and
>> > nesting depth of the call.)
>>
>> Is the nesting depth of the call really relevant?
>
> Ummm... No, we can drop that: the nesting thing is redundant with
> the requirement on serializable types about matching of << and >> ops.
> On the other hand, XML archives do enforce the nesting abidance,
> but this is more of an implementation artifact.

Good. It gets simpler.

>
>> > 2. For primitive serialization types, the restored copies are equivalent
>> > to their original (expand on this, specially with respect to pointers.)
>> > * A type T is serializable if it is primitive serializable or else it defines
>> > the appropriate serialize (load/save) function such that the sequence
>> > of >> ops in load() match the << ops in save().
>> >
>> > [This is not a requirement] For each serializable type, the implementor
>> > can define "equivalence" in terms of its constituent types. For instance,
>> > for std::vector:
>> >
>> > Given a std::vector<T> out, where T is serializable, and a restored copy in,
>> > then in(i).size()==out(i).size() and each in(i)[j] is a restored copy of
>> > out(i)[j].
>>
>> I don't think this latter part is worth much. I think it might be
>> worth defining an EquivalentSerializable concept, though.
>
> One can do the following:
>
> Let T be a serializable type and Pred an associated equality
> predicate inducing an equivalence relationship on T. Then T is said
> to be EquivalentSerializable (under Pred) if
>
> p(x,y)==true
>
> for all p of type Pred

You said Pred was a predicate; now you're saying it's a type. I think
you were right the first time. You'll never satisfy that for all p of
type bool(*)(int,int) for example.

> and x and y of type T such that y is a restored copy of x.
>
> This leaves to the implementor of an UDT the open task of giving the
> appropriate associated equality predicate (by default we can assume
> std::equal_to).

I think you mean ==

> Then we can rewrite the postcondition on std::vector as
>
> if T is EquivalentSerializable under Pred, std::vector<T> is
> EquivalentSerializable.

Nope. You have to say under what predicate it is
EquivalentSerializable. And when a nonstandard predicate is used for
T there may not be any such predicate for the vector.

> (The statement is a little more complex if we take a Pred other than
> the default.) Of course, this EquivalentSerializable concept does
> not save us the task of first providing archive compatibilty and
> Serializable concepts the hard way

Of course not.

> and it is only applicable intraprogram.

That's only true if you consider Pred to be a callable C++ predicate
rather than a logical one.

> Does this sound good to you?

Yes and no. It's crafty, but you have a pretty big gaping hole as
demonstrated by the vector example. I would be very happy with the
good old fuzzy notion of equivalence here, but if you can close the
hole, I don't mind adding predicates to the mix.

Okay, how about this: the predicate is tightly bound to the type. So
the predicate for vector<T> is defined to be that the two vectors have
the same length and that each corresponding element of the two vectors
satisfies the predicate that's bound to T.

-- 
Dave Abrahams
Boost Consulting
www.boost-consulting.com

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