boost serialization of non-default constructible t

2019-04-16 03:44发布

问题:

I am serializing a class that has a non-default constructor with boost serialization 1.40. Lets say

typedef struct foo {
    foo(int b) : a(b) {}
    int a;
} foo;

And I would like to have:

namespace boost {
namespace serialization {

template<class Archive>
void save_construct_data(Archive& archive, const foo* f, const unsigned int version)
{
    archive & foo->a;
}

template<class Archive>
void load_construct_data(Archive& archive, foo* f, const unsigned int version)
{
    int a;
    archive & a;
    ::new(f)foo(a);
}

template<class Archive>
void save(Archive& archive, const foo& label, const unsigned int version)
{
    ;
}

template<class Archive>
void load(Archive& archive, foo& f, const unsigned int version)
{
    ;
}

template<class Archive>
void serialize(Archive& archive, foo& f, const unsigned int version)
{
    boost::serialization::split_free(archive, f, version);
}
} }

Is this the way (de)serializing a reference to a class supposed to be implemented? While I have found usage of (load)save_construct_data for (de)serializing pointers to non-default constructible classes here:

http://www.boost.org/doc/libs/1_40_0/libs/serialization/doc/serialization.html#constructors

I have not found how to deal with references to those classes.

回答1:

The documentation states the reason:

References

Classes that contain reference members will generally require non-default constructors as references can only be set when an instance is constructed.

The example of the previous section is slightly more complex if the class has reference members. This raises the question of how and where the objects being referred to are stored and how are they created. Also there is the question about references to polymorphic base classes. Basically, these are the same questions that arise regarding pointers.

This is no surprise as references are really a special kind of pointer. We address these questions by serializing references as though they were pointers.

The framework cannot possibly figure out the lifetime/ownership semantics for references. References need to be manually "guided": you need to figure out who own the instance and serialize any significant references as if pointers.

There are so many issues with this, that it's rarely a good idea to serialize references[1]. Mainly with the fact that references can not be reseated.

This means the reference must be bound at construction time, imposing strict requirements on the (de)serialization order.

References are usually "ok" when they refer to "external" entities that are not logically part of the archive (e.g. global configuration, parent nodes and other bookkeeping overhead). But in such case you'd expect to instate the references independent of the archive data.

Whenever this is not the case, reconsider your design.

CAUTION The documentation cited goes on to show an example of how to force deserialization of references, but this leaves the ownership/lifetime riddle completely open, and as written is a guaranteed memory leak.

For non-primitive types, you will get object tracking (and hence, deduplication of aliased references) within a single archive.


[1] I'd argue that it's rarely a good idea to have reference members in your class

UPDATE

Live On Coliru

#include <boost/archive/text_oarchive.hpp>
#include <boost/archive/text_iarchive.hpp>
#include <boost/serialization/serialization.hpp>
#include <boost/serialization/vector.hpp>
#include <boost/ptr_container/ptr_vector.hpp>
#include <boost/ptr_container/serialize_ptr_vector.hpp>
#include <set>
#include <iostream>

struct foo {
    foo(int b) : a(b) {}
    int a;
    int b = 42;
};

struct refholder {
    foo& _ref;
    refholder(foo& ref) : _ref(ref) {}
};

namespace {
    using FooHandle = std::unique_ptr<foo>;
    static std::set<FooHandle> _instances;

    FooHandle const& register_instance(foo* raw) {
        /* manage lifetime externally, ignore dupes because object tracking can
         * lead to same pointer used >once
         */
        auto existing = std::find_if(_instances.begin(), _instances.end(),
                [raw](FooHandle const& p) { return raw == p.get(); });

        if (existing == _instances.end())
            return *_instances.insert(std::unique_ptr<foo>(raw)).first;
        else
            return *existing;
    }
}

namespace boost {
namespace serialization {

    /// {{{ foo serialization
    template <class Archive> void save_construct_data(Archive &ar, const foo *f, const unsigned int /*version*/) {
        ar & f->a;
    }

    template <class Archive> void load_construct_data(Archive &ar, foo *f, const unsigned int /*version*/) {
        int a;
        ar &a;
        ::new (f) foo(a);
    }

    template <class Archive> void serialize(Archive &ar, foo &f, const unsigned int /*version*/) { ar & f.b; }
    // }}} foo serialization

    /// {{{ refholder serialization
    template <class Archive> void save_construct_data(Archive &ar, const refholder *rh, const unsigned int /*version*/) {
        foo* external = &rh->_ref;
        ar & external;
    }

    template <class Archive> void load_construct_data(Archive &ar, refholder *rh, const unsigned int /*version*/) {
        foo* external = nullptr;
        ar & external;

        register_instance(external);

        ::new (rh) refholder(*external); // pass the dereferenced external pointer.
    }

    template <class Archive> void serialize(Archive &, refholder&, const unsigned int /*version*/) {
    }
    // }}} refholder serialization
}
}

#include <sstream>

int main() {

    std::stringstream ss;
    using data_t = boost::ptr_vector<refholder>;

    {
        boost::archive::text_oarchive oa(ss);

        foo shared1(7), shared2(77);

        data_t data;
        data.push_back(new refholder{shared1}); data.push_back(new refholder{shared1}); data.push_back(new refholder{shared1});
        data.push_back(new refholder{shared2}); data.push_back(new refholder{shared2});

        oa << data;
    }

    std::cout << ss.str();

    {
        assert(_instances.empty());

        boost::archive::text_iarchive ia(ss);

        data_t data;
        ia >> data;

        std::cout << "_instances.size(): " << _instances.size() << "\n";
        assert(_instances.size() == 2); // two unique instances
    }

    // _instances will be destructed, leading to leak-free

}

Prints

22 serialization::archive 13 0 0 5 1 1 0
0 2 1 0
1 7 42 1
2 2 1 1
3 2 1 1
4 2
5 77 42 1
6 2 5
_instances.size(): 2

See also this older answer:

  • Boost serialization of reference member abstract class

  • boost serialization: save_construct_data not called

  • I actually ran into this standard libraryimplementation-dependent issue (again): boost::serialization: object with private default constructor works in a vector, but not in a map