boost::serialization: object with private default constructor works in a vector, but not in a map

一世执手 提交于 2019-12-10 10:08:45

问题


Consider the following code:

#include <boost/serialization/nvp.hpp>
#include <boost/archive/xml_iarchive.hpp>
#include <boost/archive/xml_oarchive.hpp>

class Foo{
    friend class boost::serialization::access;
    template<class Archive>
    void serialize(Archive & ar, const unsigned int)
    {
        ar & BOOST_SERIALIZATION_NVP(i);
    }
    int i;
    Foo():i(0){}
public:
    Foo(int k):i(k){}
};

int main(int argc, char *argv[])
{
    std::vector< Foo> f;
    f.push_back(Foo(12));
    std::ofstream os("path");
    boost::archive::xml_oarchive oa(os);
    oa << boost::serialization::make_nvp("f", f);
    os.close();
    std::vector<Foo> g;
    std::ifstream is("path");
    boost::archive::xml_iarchive ia(is);
    ia >> boost::serialization::make_nvp("f", g);
}

Which works fine when serializing a vector of Foos. However, if I try to serialize a map of Foos, it fails on the private default constructor:

std::map<std::string, Foo> f;
f.insert(std::make_pair("hello", Foo(12)));
std::ofstream os("path");
boost::archive::xml_oarchive oa(os);
oa << boost::serialization::make_nvp("f", f);
os.close();
std::map<std::string, Foo> g;
std::ifstream is("path");
boost::archive::xml_iarchive ia(is);
ia >> boost::serialization::make_nvp("f", g);

fails with

In file included from main.cpp:2:
In file included from /usr/local/include/boost/serialization/nvp.hpp:19:
In file included from /usr/bin/../lib/gcc/x86_64-linux-gnu/4.8/../../../../include/c++/4.8/utility:70:
/usr/bin/../lib/gcc/x86_64-linux-gnu/4.8/../../../../include/c++/4.8/bits/stl_pair.h:109:18: error: field of type 'Foo' has private default constructor
      : first(), second() { }
                 ^
/usr/local/include/boost/serialization/access.hpp:132:17: note: in instantiation of member function 'std::pair<const std::basic_string<char>, Foo>::pair' requested here
        ::new(t)T;
                ^
/usr/local/include/boost/serialization/serialization.hpp:93:13: note: in instantiation of function template specialization 'boost::serialization::access::construct<std::pair<const std::basic_string<char>, Foo> >' requested here
    access::construct(t);
            ^
/usr/local/include/boost/serialization/serialization.hpp:158:9: note: in instantiation of function template specialization 'boost::serialization::load_construct_data<boost::archive::xml_iarchive, std::pair<const std::basic_string<char>, Foo> >' requested here
        load_construct_data(ar, t, v);
        ^
/usr/local/include/boost/serialization/detail/stack_constructor.hpp:58:31: note: in instantiation of function template specialization 'boost::serialization::load_construct_data_adl<boost::archive::xml_iarchive, std::pair<const std::basic_string<char>, Foo> >' requested here
        boost::serialization::load_construct_data_adl(
                              ^
/usr/local/include/boost/serialization/collections_load_imp.hpp:83:48: note: in instantiation of member function 'boost::serialization::detail::stack_construct<boost::archive::xml_iarchive, std::pair<const std::basic_string<char>, Foo> >::stack_construct' requested here
        detail::stack_construct<Archive, type> t(ar, v);
                                               ^
/usr/local/include/boost/serialization/collections_load_imp.hpp:158:16: note: (skipping 12 contexts in backtrace; use -ftemplate-backtrace-limit=0 to see all)
        hint = ifunc(ar, s, item_version, hint);
               ^
/usr/local/include/boost/archive/detail/common_iarchive.hpp:66:18: note: in instantiation of function template specialization 'boost::archive::load<boost::archive::xml_iarchive, std::map<std::basic_string<char>, Foo, std::less<std::basic_string<char> >, std::allocator<std::pair<const std::basic_string<char>, Foo> > > >' requested here
        archive::load(* this->This(), t);
                 ^
/usr/local/include/boost/archive/basic_xml_iarchive.hpp:86:39: note: in instantiation of function template specialization 'boost::archive::detail::common_iarchive<boost::archive::xml_iarchive>::load_override<std::map<std::basic_string<char>, Foo, std::less<std::basic_string<char> >, std::allocator<std::pair<const std::basic_string<char>, Foo> > > >' requested here
        this->detail_common_iarchive::load_override(t.value(), 0);
                                      ^
/usr/local/include/boost/archive/xml_iarchive.hpp:93:38: note: in instantiation of function template specialization 'boost::archive::basic_xml_iarchive<boost::archive::xml_iarchive>::load_override<std::map<std::basic_string<char>, Foo, std::less<std::basic_string<char> >, std::allocator<std::pair<const std::basic_string<char>, Foo> > > >' requested here
        basic_xml_iarchive<Archive>::load_override(t, 0);
                                     ^
/usr/local/include/boost/archive/detail/interface_iarchive.hpp:60:23: note: in instantiation of function template specialization 'boost::archive::xml_iarchive_impl<boost::archive::xml_iarchive>::load_override<const boost::serialization::nvp<std::map<std::basic_string<char>, Foo, std::less<std::basic_string<char> >, std::allocator<std::pair<const std::basic_string<char>, Foo> > > > >' requested here
        this->This()->load_override(t, 0);
                      ^
main.cpp:50:8: note: in instantiation of function template specialization 'boost::archive::detail::interface_iarchive<boost::archive::xml_iarchive>::operator>><const boost::serialization::nvp<std::map<std::basic_string<char>, Foo, std::less<std::basic_string<char> >, std::allocator<std::pair<const std::basic_string<char>, Foo> > > > >' requested here
    ia >> boost::serialization::make_nvp("f", g);
       ^
main.cpp:34:5: note: implicitly declared private here
    Foo():i(0){}
    ^

I'm using clang Ubuntu clang version 3.4-1ubuntu3 (tags/RELEASE_34/final) (based on LLVM 3.4)

and boost version 1.55 as shipped with Ubuntu 14.04LTS.

I have tried providing the load_construct_data() function as follows:

namespace boost
{
    namespace serialization
    {
        template<class Archive>
        inline void load_construct_data(Archive &archive, Foo*a, unsigned int
file_version)
        {
            ::new(a)Foo(0);
        }
    }
}

but I'm still getting the same error, because it requires the constructor when instantiating a std::pair


回答1:


Oh. Aha.

I just used Boost 1.57.0 to compare the situation with map<string, Foo>.

Well, you're in luck. You've found another library version dependency (likely a bug).

  • Not using that, but providing the private default constructor, GCC 4.8.2 compiles it just fine: Live On Coliru [1]

  • GCC 4.9.0 fails to compile it though (it uses a newer version of the standard library too). The std::pair<> default constructor fails to compile there, since Foo is not default constructible: Live On Coliru

Solution

Luckily the solution with save_construct_data/load_construct_data saves the day, again.

However, you need to cater for the fact that the element type is actually not Foo, but std::pair<T const, Foo>.

template <class Archive, typename K> inline friend void save_construct_data(Archive& ar, std::pair<K, Foo> const* v, const unsigned int) {
    std::cerr << __PRETTY_FUNCTION__ << "\n";
    ar & boost::serialization::make_nvp("first", v->first);
    ar & boost::serialization::make_nvp("second", v->second.i);
}
template <class Archive, typename K> inline friend void load_construct_data(Archive& ar, std::pair<K, Foo>* v, const unsigned int) {
    std::cerr << __PRETTY_FUNCTION__ << "\n";
    typename std::remove_cv<K>::type first;
    ar & boost::serialization::make_nvp("first", first);
    int tmp;
    ar & boost::serialization::make_nvp("second", tmp);
    new(v) std::pair<K, Foo>(first, tmp);
}

Now it all works:

Live On Coliru

#include <boost/serialization/nvp.hpp>
#include <boost/serialization/serialization.hpp>
#include <boost/archive/xml_iarchive.hpp>
#include <boost/archive/xml_oarchive.hpp>
#include <boost/serialization/access.hpp>
#include <boost/serialization/vector.hpp>
#include <boost/serialization/map.hpp>
#include <boost/version.hpp>
#include <fstream>
#include <iostream>

class Foo {
    friend class boost::serialization::access;

    template <class Archive> void serialize(Archive &, const unsigned int) {
        std::cerr << __PRETTY_FUNCTION__ << "\n";
    }

    template <class Archive, typename K> inline friend void save_construct_data(Archive& ar, std::pair<K, Foo> const* v, const unsigned int) {
        std::cerr << __PRETTY_FUNCTION__ << "\n";
        ar & boost::serialization::make_nvp("first", v->first);
        ar & boost::serialization::make_nvp("second", v->second.i);
    }
    template <class Archive, typename K> inline friend void load_construct_data(Archive& ar, std::pair<K, Foo>* v, const unsigned int) {
        std::cerr << __PRETTY_FUNCTION__ << "\n";
        typename std::remove_cv<K>::type first;
        ar & boost::serialization::make_nvp("first", first);
        int tmp;
        ar & boost::serialization::make_nvp("second", tmp);
        new(v) std::pair<K, Foo>(first, tmp);
    }

    int i;

  public:
    Foo(int k) : i(k) {}

    friend std::ostream& operator<<(std::ostream& os, Foo const& foo) {
        return os << "Foo { " << foo.i << " }";
    }
};

namespace boost { namespace serialization {


} }

int main() {
    using Data = std::map<std::string, Foo>;
    std::cout << "Boost version: " << BOOST_VERSION << "\n";

    {
        auto f = Data { {"a", 12 }, {"b", 42} };
        //for (auto& e : f) std::cout << e.first << ", " << e.second << "\n";
        std::ofstream os("path");
        boost::archive::xml_oarchive oa(os);
        oa << boost::serialization::make_nvp("f", f);
    }

    {
        Data g;
        std::ifstream is("path");
        boost::archive::xml_iarchive ia(is);
        ia >> boost::serialization::make_nvp("f", g);

        for (auto& e : g)
            std::cout << e.first << ", " << e.second << "\n";
    }
}

Which prints:

Boost version: 105700
void save_construct_data(Archive&, const std::pair<K, Foo>*, unsigned int) [with Archive = boost::archive::xml_oarchive; K = const std::__cxx11::basic_string<char>]
void Foo::serialize(Archive&, unsigned int) [with Archive = boost::archive::xml_oarchive]
void save_construct_data(Archive&, const std::pair<K, Foo>*, unsigned int) [with Archive = boost::archive::xml_oarchive; K = const std::__cxx11::basic_string<char>]
void Foo::serialize(Archive&, unsigned int) [with Archive = boost::archive::xml_oarchive]
void load_construct_data(Archive&, std::pair<K, Foo>*, unsigned int) [with Archive = boost::archive::xml_iarchive; K = const std::__cxx11::basic_string<char>]
void Foo::serialize(Archive&, unsigned int) [with Archive = boost::archive::xml_iarchive]
void load_construct_data(Archive&, std::pair<K, Foo>*, unsigned int) [with Archive = boost::archive::xml_iarchive; K = const std::__cxx11::basic_string<char>]
void Foo::serialize(Archive&, unsigned int) [with Archive = boost::archive::xml_iarchive]
a, Foo { 12 }
b, Foo { 42 }

[1] (I can't link it on Coliru because the boost libraries there have been recompiled to the GCC 5.0 ABI)

Notes

A better, general, solution would be to do the load/save_construct_data trick generically for non-default-constructible types in the boost::serialization namespace. That way, people won't have to "know" about the std::pair<> implementation detail. They could just implement load/save_construct_data for their own user-types and it would JustWork™ whether they put it in a vector or a map.

Implementing that generically is less-than-trivial though, and might interfere with some other machineries internal to the Boost Serialization framework.

I'll prefer to get some help of the Boost Serialization maintainers to do that in a reliable way. So, it seems I'll be submitting two tickets today.




回答2:


It doesn't necessarily work for vector either. Vector deserialization resizes the vector to the required size first. This requires the elements to be default-constructible.

Note how this is only a problem because

  1. construction is not accessed via the serialization::access friend "token"
  2. the class is default constructible

The Documented Solution

The documentation tells you to use save_construct_data and load_construct_data for types that are not default-constructible.

Specifically they promise it will work for STL containers of these as well:

In addition to the deserialization of pointers, these overrides are used in the deserialization of STL containers whose element type has no default constructor.

In practice, this works well in v1.57.0:

  • Live On Coliru

But in 1.58.0 it isn't true...

A Bug In Boost 1.58.0

Version 1.58.0 seems to have broken this:

The code seems to do required check (from serialization/vector.hpp in the unoptimized version of load(...)):

if(detail::is_default_constructible<U>()){
    t.resize(count);
    // ... snip ... 
}
else{
    t.reserve(count);
    // ... snip ... 
}

However, this does the check at runtime. The method will statically refuse to compile. Oops.

The Fix

Instead of having the branches in the same flow code, it should be dispatched so that only the applicable branch is instantiated. I tested with this simplistic approach:

namespace sehe_bugfix {
    template<class Archive, class U, class Allocator>
    inline void load_elements(
        Archive & ar,
        std::vector<U, Allocator> &t,
        const unsigned int /* file_version */,
        collection_size_type count,
        mpl::true_
    ){
        const boost::archive::library_version_type library_version(
            ar.get_library_version()
        );
        item_version_type item_version(0);
        if(boost::archive::library_version_type(3) < library_version){
            ar >> BOOST_SERIALIZATION_NVP(item_version);
        }
        t.resize(count);
        typename std::vector<U, Allocator>::iterator hint;
        hint = t.begin();
        while(count-- > 0){
            ar >> boost::serialization::make_nvp("item", *hint++);
        }
    }

    template<class Archive, class U, class Allocator>
    inline void load_elements(
        Archive & ar,
        std::vector<U, Allocator> &t,
        const unsigned int /* file_version */,
        collection_size_type count,
        mpl::false_
    ){
        const boost::archive::library_version_type library_version(
            ar.get_library_version()
        );
        item_version_type item_version(0);
        if(boost::archive::library_version_type(3) < library_version){
            ar >> BOOST_SERIALIZATION_NVP(item_version);
        }
        t.reserve(count);
        while(count-- > 0){
            detail::stack_construct<Archive, U> u(ar, item_version);
            ar >> boost::serialization::make_nvp("item", u.reference());
            t.push_back(u.reference());
            ar.reset_object_address(& t.back() , & u.reference());
        }
    }
}

template<class Archive, class U, class Allocator>
inline void load(
    Archive & ar,
    std::vector<U, Allocator> &t,
    const unsigned int file_version,
    mpl::false_
){
    const boost::archive::library_version_type library_version(
        ar.get_library_version()
    );
    // retrieve number of elements
    item_version_type item_version(0);
    collection_size_type count;
    ar >> BOOST_SERIALIZATION_NVP(count);

    sehe_bugfix::load_elements(ar, t, file_version, count, detail::is_default_constructible<U>());
}

And it works.

SUMMARY

Sadly I don't have time now to investigate on the map<> situation. But I suspect that things are similar. The documented solution should still work. And it might still be broken.

I'll report the above issue later today to the issue tracker of boost.

I hope the answer helps you find the/a solution none-the-less



来源:https://stackoverflow.com/questions/30426543/boostserialization-object-with-private-default-constructor-works-in-a-vector

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!