4

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

Philipp
  • 957
  • 1
  • 6
  • 20
  • This turned out to be a much more involved question than I anticipated. Well-done for asking. – sehe May 25 '15 at 12:06
  • 1
    The fix for this has been checked into the develop and master branch and will appear in the next release. If you can't wait for that, you can clone/download the latest versions from github. – Robert Ramey May 26 '15 at 04:19

2 Answers2

5

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.

sehe
  • 374,641
  • 47
  • 450
  • 633
  • 1
    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. – sehe May 25 '15 at 12:02
  • Thanks for digging into this. Please post the trac ticket numbers as soon as they are in the system. – Philipp May 25 '15 at 18:52
  • 1
    Posted this as a feature request here https://svn.boost.org/trac/boost/ticket/11343 – sehe May 26 '15 at 00:03
3

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:

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

sehe
  • 374,641
  • 47
  • 450
  • 633
  • 1
    Issue reported here OT: https://svn.boost.org/trac/boost/ticket/11342 _Boost Trac is debilitatingly slow :(_ – sehe May 25 '15 at 23:47