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.
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:
as_map
and as_multimap
below); The default is the identity functiontemplate <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.
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
// 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
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.
(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);
}