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:
- allows switching archive types
- optionally providing a wrapping function (in my example we will define
as_map and as_multimap below); The default is the identity function
- serializes a "map-ish container" of 4 entries into a stringstream
- deserializes it back
- compares the replica to the original
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);
}