Search code examples
c++oopserializationboostabstract

Dealing with unordered_map of abstract classes and serialization


Say I have the following structs:

struct Base {
    virtual int get_x() = 0;
}

struct A : Base {
    int get_x() {
         // ...
         return 0;
    }
}

struct B : Base {
    int get_x() {
         // ...
         return 1;
    }
}

I also have serialization methods for these classes:

BOOST_SERIALIZATION_SPLIT_FREE(A);
namespace boost {
    namespace serialization {
        template<class Archive>
        void save(Archive & ar, const A & a,
                        const unsigned int version) {
            // ...
        }
        template<class Archive>
        void load(Archive & ar, A & a,
                        const unsigned int version) {
            // ...
        }
    }
}

// same for B

In my code, I'm making an unordered_map<int, A>. I have functions to save and load this map:

inline void save_map(string filename, unordered_map<int, A>& a_dict) {
    ofstream filestream(filename);
    boost::archive::binary_oarchive archive(filestream,
                                            boost::archive::no_codecvt);

    archive << a_dict;
}

inline void load_map(string filename, unordered_map<int, A>* a_dict) {
    ifstream filestream(filename);
    boost::archive::binary_iarchive archive(filestream,
                                            boost::archive::no_codecvt);

    archive >> *a_dict;
}

I now want to generalize my map definition and these functions in an elegant way so my code is agnostic to whether my objects are A or B. Obviously I run into object slicing issues if I, for example, just start using unordered_map<int, Base>, but I've gotten a bit lost in the weeds of pointers at this point and haven't been able to figure out the solution.


Solution

  • I would probably recommend ready made pointer containers. There are

    Pointer Container

    This is straight-forward:

    Live On Coliru

    #include <boost/archive/binary_oarchive.hpp>
    #include <boost/archive/binary_iarchive.hpp>
    #include <boost/serialization/export.hpp>
    #include <fstream>
    
    #include <boost/ptr_container/ptr_unordered_map.hpp>
    #include <boost/ptr_container/serialize_ptr_unordered_map.hpp>
    
    struct Base {
        virtual ~Base() noexcept = default;
        virtual int get_x() const = 0;
    
        friend auto&  operator<<(std::ostream& os, Base const& b) {
            return os << typeid(b).name() << "[" << b.get_x() << "]";
        }
    };
    
    struct A : Base { int get_x() const override { return 0; } };
    struct B : Base { int get_x() const override { return 1; } };
    
    BOOST_SERIALIZATION_ASSUME_ABSTRACT(Base)
    BOOST_CLASS_EXPORT(A)
    BOOST_CLASS_EXPORT(B)
    
    namespace boost { namespace serialization {
        template <class Ar> void serialize(Ar& ar, Base&, unsigned) { }
        template <class Ar> void serialize(Ar& ar, A& a, unsigned) { ar& base_object<Base>(a); }
        template <class Ar> void serialize(Ar& ar, B& b, unsigned) { ar& base_object<Base>(b); }
    }} // namespace boost::serialization
    
    template <typename Map>
    inline void save_map(std::string const& filename, Map const& map) {
        std::ofstream ofs(filename, std::ios::binary);
        boost::archive::binary_oarchive archive(ofs, boost::archive::no_codecvt);
        archive << map;
    }
    
    template <typename Map>
    inline void load_map(std::string const& filename, Map& map) {
        std::ifstream ifs(filename, std::ios::binary);
        boost::archive::binary_iarchive archive(ifs, boost::archive::no_codecvt);
        archive >> map;
    }
    
    #include <iostream>
    
    int main()
    {
        using Dict = boost::ptr_unordered_map<int, Base>;
    
        {
            Dict dict;
            dict.insert(100, std::make_unique<A>());
            dict.insert(200, std::make_unique<B>());
            dict.insert(300, std::make_unique<B>());
            dict.insert(400, std::make_unique<A>());
    
            save_map("test.bin", dict);
        }
    
        {
            Dict roundtrip;
            load_map("test.bin", roundtrip);
    
            for (auto const& [k,v]: roundtrip) {
                std::cout << k << ": " << *v << "\n";
            }
        }
    }
    

    Prints e.g.

    100: 1A[0]
    200: 1B[1]
    300: 1B[1]
    400: 1A[0]
    

    And no memory leaks.


    Roll your own

    This has some benefits (more modern interfaces sometimes somewhat more "pointery" interface at other points):

    Live On Coliru

    #include <boost/archive/binary_iarchive.hpp>
    #include <boost/archive/binary_oarchive.hpp>
    #include <boost/serialization/boost_unordered_map.hpp>
    #include <boost/serialization/unique_ptr.hpp>
    #include <boost/serialization/export.hpp>
    #include <fstream>
    
    struct Base {
        virtual ~Base() noexcept = default;
        virtual int get_x() const = 0;
    
        friend auto&  operator<<(std::ostream& os, Base const& b) {
            return os << typeid(b).name() << "[" << b.get_x() << "]";
        }
    };
    
    struct A : Base { int get_x() const override { return 0; } };
    struct B : Base { int get_x() const override { return 1; } };
    
    BOOST_SERIALIZATION_ASSUME_ABSTRACT(Base)
    BOOST_CLASS_EXPORT(A)
    BOOST_CLASS_EXPORT(B)
    
    namespace boost { namespace serialization {
        template <class Ar> void serialize(Ar& ar, Base&, unsigned) { }
        template <class Ar> void serialize(Ar& ar, A& a, unsigned) { ar& base_object<Base>(a); }
        template <class Ar> void serialize(Ar& ar, B& b, unsigned) { ar& base_object<Base>(b); }
    }} // namespace boost::serialization
    
    template <typename Map>
    inline void save_map(std::string const& filename, Map const& map) {
        std::ofstream ofs(filename, std::ios::binary);
        boost::archive::binary_oarchive archive(ofs, boost::archive::no_codecvt);
        archive << map;
    }
    
    template <typename Map>
    inline void load_map(std::string const& filename, Map& map) {
        std::ifstream ifs(filename, std::ios::binary);
        boost::archive::binary_iarchive archive(ifs, boost::archive::no_codecvt);
        archive >> map;
    }
    
    #include <iostream>
    
    int main()
    {
        using Dict = boost::unordered_map<int, std::unique_ptr<Base> >;
    
        {
            Dict dict;
            dict.emplace(100, std::make_unique<A>());
            dict.emplace(200, std::make_unique<B>());
            dict.emplace(300, std::make_unique<B>());
            dict.emplace(400, std::make_unique<A>());
    
            save_map("test.bin", dict);
        }
    
        {
            Dict roundtrip;
            load_map("test.bin", roundtrip);
    
            for (auto const& [k,v]: roundtrip) {
                std::cout << k << ": " << *v << "\n";
            }
        }
    }
    

    Prints, again without memory leaks, something like:

    100: 1A[0]
    200: 1B[1]
    300: 1B[1]
    400: 1A[0]