c++serializationboostc++17boost-serialization

Boost.Serialize: writing a general map serialization function


Boost.Serialize provides explicit serialization for std::map / std::multimap, which won't work with other map-like containers.

I'd like to serialize those without needing to rewrite these functions every time, but Boost complains about ambiguousness.

Here is my code:


/** Some sfinae to detect types that behave like std::map **/
template<typename T, typename = void>
struct is_map_ish : std::false_type { };

template<template<typename K, typename V> typename Map, typename K, typename V>
struct is_map_ish<
    Map<K,V>,
    std::void_t<
        typename Map<K,V>::key_type,
        typename Map<K,V>::value_type
    >
    > : std::true_type {
};

template<class Archive, class Map>
inline auto save(
    Archive & ar,
    const Map& t,
    const unsigned int /* file_version */,
        std::enable_if_t<is_map_ish<Map>::value>* = 0
) -> std::enable_if_t<is_map_ish<Map>::value>
{
    boost_155::serialization::stl::save_collection<
        Archive,
        Map
    >(ar, t);
}

template<class Archive, class Map>
inline auto load(
    Archive & ar,
    Map& t,
    const unsigned int /* file_version */,
        std::enable_if_t<is_map_ish<Map>::value>* = 0
) -> std::enable_if_t<is_map_ish<Map>::value> {
    boost_155::serialization::stl::load_collection<
        Archive,
        Map,
        boost_155::serialization::stl::archive_input_map<
            Archive, Map >,
            boost_155::serialization::stl::no_reserve_imp<Map>
    >(ar, t);
}

// split non-intrusive serialization function member into separate
// non intrusive save/load member functions
template<class Archive, class Map>
inline auto serialize(
    Archive & ar,
    Map &t,
    const unsigned int file_version,
    std::enable_if_t<is_map_ish<Map>::value>* = 0
) -> std::enable_if_t<is_map_ish<Map>::value> {
    boost_155::serialization::split_free(ar, t, file_version);
}

the serialize function collides with


// default implementation - call the member function "serialize"
template<class Archive, class T>
inline void serialize(
    Archive & ar, T & t, const BOOST_155_PFTO unsigned int file_version
){
    access::serialize(ar, t, static_cast<unsigned int>(file_version));
}

in boost, which I obviously cannot change. I tried (as visible in the above code) to put some SFINAE in the return type, and in the function arguments list, but that does not help me escape the overload ambiguity.

What are my options ? In C++20 I'd try concepts but here I can only use C++17.


Solution

  • My suggestion would be to employ ADL, or use a Serialization Wrapper.

    Because I don't know when to stop, I made the following proof-of-concept. A general roundtrip test that allows:

    template <typename OA, typename IA, template <typename...> class Map,
              typename WrapFun = std::identity>
    void roundtrip_impl(std::string_view archive_type, WrapFun apply_wrap = {})
    {
        using Container = Map<int, std::string>;
        Container original{{1, "one"}, {2, "two"}, {3, "three"}, {4, "four"}};
    
        using namespace boost::archive;
        std::stringstream ss;
    
        {
            OA oa(ss, archive_flags::no_header);
            auto&& payload = apply_wrap(original);
            oa << boost::make_nvp("data", payload);
        }
    
        std::cout << archive_type << " "
                  << (typename boost::serialization::is_wrapper<decltype(
                              apply_wrap(original))>::type{}
                          ? "Wrapped "
                          : "Builtin ")
                  << std::setw(3) << ss.str().length() << " bytes" << std::flush;
    
        {
            IA ia(ss, archive_flags::no_header);
            Container replica;
            auto&& payload = apply_wrap(replica);
            ia >> boost::serialization::make_nvp("data", payload);
    
            std::cout << " - roundtrip verified: " << std::boolalpha
                      << (original == replica) << std::endl;
        }
    }
    

    Now to get systematic, we will define an test entry point that exercises all the archive type:

    template <template <typename...> class Map, typename WrapFun = std::identity>
    void roundtrips(WrapFun do_wrap = {})
    {
        roundtrip_impl<
            boost::archive::binary_oarchive,
            boost::archive::binary_iarchive,
            Map>("Binary", do_wrap);
        roundtrip_impl<
            boost::archive::text_oarchive,
            boost::archive::text_iarchive,
            Map>("Text  ", do_wrap);
        roundtrip_impl<
            boost::archive::xml_oarchive,
            boost::archive::xml_iarchive,
            Map>("XML   ", do_wrap);
    }
    

    This specifically verifies that the wrapper works in the presence of NVP wrappers.

    A Serialization Wrapper

    For map-ish containers without builtin library support, let's create a wrapper:

    namespace Serialization {
        template <typename Map, typename Unique = std::true_type> struct map_wrapper {
            Map& ref;
            map_wrapper(Map& m) : ref(m) {}
    
            constexpr static bool is_unique = Unique::value;
        };
    

    Adding some convenience functions as_map and as_multimap:

    struct {
        constexpr auto operator()(auto& ref) const { return map_wrapper(ref); }
    } static inline constexpr as_map;
    
    struct {
        constexpr auto operator()(auto& ref) const {
            return map_wrapper<decltype(ref), std::false_type>(ref);
        }
    } static inline constexpr as_multimap;
    

    Now we can implement generic save/load for the wrapper:

    // ADL finds these
    template <typename Ar, typename... Args>
    void save(Ar& ar, map_wrapper<Args...> const& w, unsigned v) {
        size_t n = std::size(w.ref);
        ar & boost::make_nvp("size", n);
        for (auto& element : w.ref) {
            ar & boost::make_nvp("item", element);
        }
    }
    

    That was straightforwad. Of course, deserialization is somewhat more varied:

    template <typename Ar, typename Map, typename Unique>
    void load(Ar& ar, map_wrapper<Map, Unique>& w, unsigned v)
    {
        using V = typename boost::range_value<Map>::type;
    
        w.ref.clear(); // optionally?
        size_t n;
        ar & boost::make_nvp("size", n);
    
        while (n--) {
            V element;
            ar & boost::make_nvp("item", element);
    
            // insert and fix object tracking!
            if constexpr (Unique()) {
                auto [it, inserted] = w.ref.insert(std::move(element));
                if (inserted)
                    ar.reset_object_address(std::addressof(*it), &element);
            } else {
                auto it = w.ref.insert(std::end(w.ref), std::move(element));
                ar.reset_object_address(std::addressof(*it), &element);
            }
        }
    }
    

    The most important bit is where we fixup the object address for address tracking. See docs.

    There is no templatey BOOST_SERIALIZATION_SPLIT_FREE, so we write:

    template <typename Ar, typename... Args>
    void serialize(Ar& ar, map_wrapper<Args...>& m, unsigned v)
    {
        if constexpr (typename Ar::is_saving())
            save(ar, m, v);
        else
            load(ar, m, v);
    }
    
    } // namespace Serialization
    

    NEARLY done. We just need to help the library realize we're a wrapper:

    namespace boost::serialization {
        template <typename... Args>
        struct is_wrapper<Serialization::map_wrapper<Args...>> {
            using type = std::true_type;
        };
        // it's heinous, but we have to do it for const qualified too
        template <typename... Args>
        struct is_wrapper<Serialization::map_wrapper<Args...> const> {
            using type = std::true_type;
        };
    } // namespace boost::serialization
    

    Running Some Tests

    Live On Coliru

    // bunch of tests with various containers
    

    #include <boost/serialization/map.hpp> #include <boost/serialization/unordered_map.hpp> #include <boost/container/map.hpp> #include <boost/container/flat_map.hpp> #include <boost/container/set.hpp> #include <boost/unordered_map.hpp>

    // including fantasy "maps"
    template <typename K, typename V>
        using FakeMap = boost::container::set<std::pair<K, V>>;
    template <typename K, typename V>
        using FakeMultiMap = std::vector<std::pair<K, V>>;
    
    int main() 
    {
        roundtrips<std::map>();
        roundtrips<std::multimap>();
    
        roundtrips<std::unordered_map>();
        roundtrips<std::unordered_multimap>();
    
        // no direct support, but we can wrap!
        using Serialization::as_map;
        using Serialization::as_multimap;
        roundtrips<boost::container::map>(as_map);
        roundtrips<boost::container::multimap>(as_multimap);
    
        roundtrips<boost::unordered_map>(as_map);
        roundtrips<boost::unordered_multimap>(as_multimap);
    
        roundtrips<FakeMap>(as_map);
        roundtrips<FakeMultiMap>(as_multimap);
    }
    

    Prints

    Binary Builtin  85 bytes - roundtrip verified: true
    Text   Builtin  47 bytes - roundtrip verified: true
    XML    Builtin 393 bytes - roundtrip verified: true
    Binary Builtin  85 bytes - roundtrip verified: true
    Text   Builtin  47 bytes - roundtrip verified: true
    XML    Builtin 393 bytes - roundtrip verified: true
    Binary Builtin  93 bytes - roundtrip verified: true
    Text   Builtin  49 bytes - roundtrip verified: true
    XML    Builtin 425 bytes - roundtrip verified: true
    Binary Builtin  93 bytes - roundtrip verified: true
    Text   Builtin  49 bytes - roundtrip verified: true
    XML    Builtin 425 bytes - roundtrip verified: true
    Binary Wrapped  81 bytes - roundtrip verified: true
    Text   Wrapped  45 bytes - roundtrip verified: true
    XML    Wrapped 359 bytes - roundtrip verified: true
    Binary Wrapped  81 bytes - roundtrip verified: true
    Text   Wrapped  45 bytes - roundtrip verified: true
    XML    Wrapped 359 bytes - roundtrip verified: true
    Binary Wrapped  81 bytes - roundtrip verified: true
    Text   Wrapped  45 bytes - roundtrip verified: true
    XML    Wrapped 359 bytes - roundtrip verified: true
    Binary Wrapped  81 bytes - roundtrip verified: true
    Text   Wrapped  45 bytes - roundtrip verified: true
    XML    Wrapped 359 bytes - roundtrip verified: true
    Binary Wrapped  81 bytes - roundtrip verified: true
    Text   Wrapped  45 bytes - roundtrip verified: true
    XML    Wrapped 359 bytes - roundtrip verified: true
    Binary Wrapped  81 bytes - roundtrip verified: true
    Text   Wrapped  45 bytes - roundtrip verified: true
    XML    Wrapped 359 bytes - roundtrip verified: true
    

    Caveat

    Note in these examples I have assumed that the map-ish containers will be using default-constructible comparators/hash function/equality comparators. Otherwise you have to remember to implement load_/save_construct_data and at that point I believe you're better off just writing type-specific implementations.

    Full Listing

    (anti bitrot)

    #include <boost/archive/basic_archive.hpp>
    #include <boost/serialization/nvp.hpp>
    #include <iostream>
    #include <iomanip>
    #include <sstream>
    #include <cassert>
    
    template <typename OA, typename IA, template <typename...> class Map,
              typename WrapFun = std::identity>
    void roundtrip_impl(std::string_view archive_type, WrapFun apply_wrap = {})
    {
        using Container = Map<int, std::string>;
        Container original{{1, "one"}, {2, "two"}, {3, "three"}, {4, "four"}};
    
        using namespace boost::archive;
        std::stringstream ss;
    
        {
            OA oa(ss, archive_flags::no_header);
            auto&& payload = apply_wrap(original);
            oa << boost::make_nvp("data", payload);
        }
    
        std::cout << archive_type << " "
                  << (typename boost::serialization::is_wrapper<decltype(
                              apply_wrap(original))>::type{}
                          ? "Wrapped "
                          : "Builtin ")
                  << std::setw(3) << ss.str().length() << " bytes" << std::flush;
    
        {
            IA ia(ss, archive_flags::no_header);
            Container replica;
            auto&& payload = apply_wrap(replica);
            ia >> boost::serialization::make_nvp("data", payload);
    
            std::cout << " - roundtrip verified: " << std::boolalpha
                      << (original == replica) << std::endl;
        }
    }
    
    #include <boost/archive/binary_iarchive.hpp>
    #include <boost/archive/binary_oarchive.hpp>
    #include <boost/archive/text_iarchive.hpp>
    #include <boost/archive/text_oarchive.hpp>
    #include <boost/archive/xml_iarchive.hpp>
    #include <boost/archive/xml_oarchive.hpp>
    
    template <template <typename...> class Map, typename WrapFun = std::identity>
    void roundtrips(WrapFun do_wrap = {})
    {
        roundtrip_impl<
            boost::archive::binary_oarchive,
            boost::archive::binary_iarchive,
            Map>("Binary", do_wrap);
        roundtrip_impl<
            boost::archive::text_oarchive,
            boost::archive::text_iarchive,
            Map>("Text  ", do_wrap);
        roundtrip_impl<
            boost::archive::xml_oarchive,
            boost::archive::xml_iarchive,
            Map>("XML   ", do_wrap);
    }
    
    #include <boost/range/value_type.hpp>
    
    namespace Serialization {
        template <typename Map, typename Unique = std::true_type> struct map_wrapper {
            Map& ref;
            map_wrapper(Map& m) : ref(m) {}
    
            constexpr static bool is_unique = Unique::value;
        };
    
        struct {
            constexpr auto operator()(auto& ref) const { return map_wrapper(ref); }
        } static inline constexpr as_map;
    
        struct {
            constexpr auto operator()(auto& ref) const {
                return map_wrapper<decltype(ref), std::false_type>(ref);
            }
        } static inline constexpr as_multimap;
    
        // ADL finds these
        template <typename Ar, typename... Args>
        void save(Ar& ar, map_wrapper<Args...> const& w, unsigned v) {
            size_t n = std::size(w.ref);
            ar & boost::make_nvp("size", n);
            for (auto& element : w.ref) {
                ar & boost::make_nvp("item", element);
            }
        }
    
        template <typename Ar, typename Map, typename Unique>
        void load(Ar& ar, map_wrapper<Map, Unique>& w, unsigned v)
        {
            using V = typename boost::range_value<Map>::type;
    
            w.ref.clear(); // optionally?
            size_t n;
            ar & boost::make_nvp("size", n);
    
            while (n--) {
                V element;
                ar & boost::make_nvp("item", element);
    
                // insert and fix object tracking!
                if constexpr (Unique()) {
                    auto [it, inserted] = w.ref.insert(std::move(element));
                    if (inserted)
                        ar.reset_object_address(std::addressof(*it), &element);
                } else {
                    auto it = w.ref.insert(std::end(w.ref), std::move(element));
                    ar.reset_object_address(std::addressof(*it), &element);
                }
            }
        }
    
        template <typename Ar, typename... Args>
        void serialize(Ar& ar, map_wrapper<Args...>& m, unsigned v)
        {
            if constexpr (typename Ar::is_saving())
                save(ar, m, v);
            else
                load(ar, m, v);
        }
    
    } // namespace Serialization
    
    namespace boost::serialization {
        template <typename... Args>
        struct is_wrapper<Serialization::map_wrapper<Args...>> {
            using type = std::true_type;
        };
        // it's heinous, but we have to do it for const qualified too
        template <typename... Args>
        struct is_wrapper<Serialization::map_wrapper<Args...> const> {
            using type = std::true_type;
        };
    } // namespace boost::serialization
    
    // bunch of tests with various containers
    #include <boost/serialization/map.hpp>
    #include <boost/serialization/unordered_map.hpp>
    #include <boost/container/map.hpp>
    #include <boost/container/flat_map.hpp>
    #include <boost/container/set.hpp>
    #include <boost/unordered_map.hpp>
    
    // including fantasy "maps"
    template <typename K, typename V>
        using FakeMap = boost::container::set<std::pair<K, V>>;
    template <typename K, typename V>
        using FakeMultiMap = std::vector<std::pair<K, V>>;
    
    int main() 
    {
        roundtrips<std::map>();
        roundtrips<std::multimap>();
    
        roundtrips<std::unordered_map>();
        roundtrips<std::unordered_multimap>();
    
        // no direct support, but we can wrap!
        using Serialization::as_map;
        using Serialization::as_multimap;
        roundtrips<boost::container::map>(as_map);
        roundtrips<boost::container::multimap>(as_multimap);
    
        roundtrips<boost::unordered_map>(as_map);
        roundtrips<boost::unordered_multimap>(as_multimap);
    
        roundtrips<FakeMap>(as_map);
        roundtrips<FakeMultiMap>(as_multimap);
    }