Boost.Serialize:写一个通用的地图序列化函数

Boost.Serialize: writing a general map serialization function

Boost.Serialize 为 std::map / std::multimap 提供显式序列化,这不适用于其他类似地图的容器。

我想序列化那些而不需要每次都重写这些函数,但是 Boost 抱怨模棱两可。

这是我的代码:


/** 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);
}

serialize 函数与

发生冲突

// 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,我显然无法更改。我尝试(如上面的代码所示)将一些 SFINAE 放入 return 类型和函数参数列表中,但这并不能帮助我避免重载歧义。

我有哪些选择?在 C++20 中我会尝试概念,但在这里我只能使用 C++17。

我的建议是使用 ADL,或者使用 Serialization Wrapper

因为不知道什么时候停下来,所以做了下面的概念验证。一般往返测试允许:

  • 允许切换存档类型
  • 可选地提供包装函数(在我的示例中,我们将在下面定义 as_mapas_multimap);默认是恒等函数
  • 将包含 4 个条目的“map-ish 容器”序列化为字符串流
  • 反序列化它
  • 将副本与原件进行比较
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;
    }
}

现在为了系统化,我们将定义一个测试所有存档类型的测试入口点:

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.

序列化包装器

对于没有内置库支持的 map-ish 容器,让我们创建一个包装器:

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;
    };

添加一些方便的函数as_mapas_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;

现在我们可以为包装器实现泛型 save/load:

// 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);
        }
    }
}

最重要的一点是我们在何处固定对象地址以进行地址跟踪。参见 docs

没有模板BOOST_SERIALIZATION_SPLIT_FREE,所以我们写:

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

运行 一些测试

Live On Coliru

// bunch of tests with various containers

#include #include #include #include #include #include

// 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);
}

版画

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

警告

请注意,在这些示例中,我假设 map-ish 容器将使用可默认构造的comparators/hash function/equality 比较器。否则你必须记住实现 load_/save_construct_data 并且在这一点上我相信你最好只编写特定于类型的实现。

完整列表

(反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);
}