CL90
CL90

Reputation: 135

Boost serialization of interface map

I have created a map of multiple different data types. s64, f64, Arrays, Images, etc. To do so, i used a map of type std::map<std::string, std::unique_ptr<MapEntryInterface>> database;. I want to store it, and reload it from filesystem. But i heard that maps cant be sored in a one-liner. So i tried to store the data part std::unique_ptr<MapEntryInterface> test; of one pair first:

    friend class boost::serialization::access;
    template<class Archive>
    void    serialize(Archive& ar, unsigned int version) {
        ar & test;
    }

The program crashes at ar & test throwing an exception: "unregistered class - derived class not registered or exported". whats the issue? i dont understand it.

here is the minimal code: (deleted)

#include <boost/serialization/vector.hpp>
//....
};

as 463035818_is_not_a_number pointed out, my sniped is not working. i recreated it, and got a lot further i think. But as soon as i insert the load from file function, it does not compile anymore saying: error C2280: "std::pair<const _Kty,_Ty>::pair(const std::pair<const _Kty,_Ty> &)" : Es wurde versucht, auf eine gelöschte Funktion zu verweisen

#include <boost/serialization/vector.hpp>
#include <boost/serialization/unique_ptr.hpp>
#include <boost/serialization/map.hpp>
#include <boost/serialization/utility.hpp>
#include <boost/archive/text_oarchive.hpp>
#include <boost/archive/text_iarchive.hpp>

#include <fstream>
#include <iostream>

class MapEntryInterface {
public:
    MapEntryInterface(std::string type_str) : type(type_str) {}

    std::string type;

    friend class boost::serialization::access;
    template<class Archive>
    void    serialize(Archive& ar, unsigned int version) {
        if (type == "s64") {
            MapEntryS64* cast = (MapEntryS64*)this;
            cast->serialize(ar, version);
        }
        if (type == "f64") {
            MapEntryF64* cast = (MapEntryF64*)this;
            cast->serialize(ar, version);
        }
    }

};

class MapEntryS64 : public MapEntryInterface {
public:

    MapEntryS64(int init_val, const std::string& type_str)
        : data(init_val), MapEntryInterface(type_str)
    {}

    uint64_t data;
    friend class boost::serialization::access;
    template<class Archive>
    void    serialize(Archive& ar, unsigned int version) {
        ar & type;
        ar & data;
    }

};

class MapEntryF64 : public MapEntryInterface {
public:

    MapEntryF64(double init_val, const std::string& type_str)
        : data(init_val), MapEntryInterface(type_str)
    {}

    double data;
    friend class boost::serialization::access;
    template<class Archive>
    void    serialize(Archive& ar, unsigned int version) {
        ar & type;
        ar & data;
    }

};

class MapDataBase {
public:
    MapDataBase()
        //: test(std::unique_ptr<MapEntryInterface>(new MapEntryS64(381, "s64")))
    {
        database["key1"] = std::unique_ptr<MapEntryInterface>(new MapEntryS64(381, "s64"));
        database["key2"] = std::unique_ptr<MapEntryInterface>(new MapEntryF64(3.124512, "f64"));
    };

    bool SaveToFile() {
        std::ofstream ofs("boost_export.dat");
        if (ofs.is_open()) {
            boost::archive::text_oarchive oa(ofs);
            oa & *this;
            return true;
        }
        return false;
    }

    bool loadFromFile() {
        std::ifstream ifs("boost_export.dat");
        if (ifs.is_open())
        {
            try
            {
                boost::archive::text_iarchive ia(ifs);
                ia & *this;
                //std::string yolo;
                //ia >> yolo;
                //ia >> bam;
            }
            catch (std::exception& ex)
            {
                std::cout << ex.what() << std::endl;
                return false;
            }
        }

        return true;
    }

private:

    std::map<std::string, std::unique_ptr<MapEntryInterface>> database;
    //std::unique_ptr<MapEntryInterface> test;

    friend class boost::serialization::access;
    template<class Archive>
    void    serialize(Archive& ar, unsigned int version) {
        ar & database;
    }

};


void main() {

    MapDataBase tmp;
    tmp.SaveToFile();

    MapDataBase tmp2;
    //tmp2.loadFromFile();

}

Upvotes: 1

Views: 297

Answers (1)

sehe
sehe

Reputation: 393739

I spent a large amount of time making things self-contained. Among the many changes:

  1. you should not have the base class serializing the derived (that's classic OOP), instead Boost expects derived classes to serialize their base_object<> (allowing static polymorphism and, incidentally, type registration)

  2. of course, the base class should serialize its data members (type)

  3. the base class SHOULD have a virtual destructor (otherwise deleting through unique_ptr's destructor will be unspecified

    Up till here:

    class MapEntryInterface {
      public:
        virtual ~MapEntryInterface() = default;
    
      protected:
        MapEntryInterface(std::string type_str) : type_(type_str) {}
        std::string type_;
    
        friend class boost::serialization::access;
        template <class Ar> void serialize(Ar& ar, unsigned) { ar& type_; }
    };
    using EntryPtr = std::unique_ptr<MapEntryInterface>;
    
  4. The base/member initialize lists are in misleading order; initialization happens in declaration order anyways

  5. The type_str is a code smell: the whole idea of OOP virtualization is not to have switching on types everywhere. I went half-way for you by at least defaulting the values, but you could probably do without it entirely. After all the type is the type.

  6. Now add the base_object serialization:

    template <class Ar> void serialize(Ar& ar, unsigned)
    {
        ar& boost::serialization::base_object<MapEntryInterface>(*this);
        ar& data_;
    }
    
  7. MapDatabase benefits from several simplifications

    • never use new or delete
    • checking the streams before is redundant since you already handle exceptions
    • since loadFromFile has no useful way of handling the exception, re-throw, or just let is escape,
    • also allowing you to make MapDatabase the return type instead of bool.
  8. saveToFile and loadFromFile should probably take a filename parameter

  9. Not shown: arguably saveToFile and loadFromFile need not be part of MapDatabase

At this point, adding a little bit of code to print database contents:

Live On Wandbox

#include <boost/archive/text_iarchive.hpp>
#include <boost/archive/text_oarchive.hpp>

#include <boost/serialization/map.hpp>
#include <boost/serialization/unique_ptr.hpp>
#include <boost/serialization/utility.hpp>
#include <boost/serialization/vector.hpp>

#include <fstream>
#include <iostream>

class MapEntryInterface {
  public:
    virtual ~MapEntryInterface() = default;
    virtual void print(std::ostream&) const = 0;

  protected:
    MapEntryInterface(std::string type_str) : type_(type_str) {}
    std::string type_;

    friend class boost::serialization::access;
    template <class Ar> void serialize(Ar& ar, unsigned) { ar& type_; }

    friend std::ostream& operator<<(std::ostream& os, MapEntryInterface const& e) {
        e.print(os);
        return os;
    }
};

using EntryPtr = std::unique_ptr<MapEntryInterface>;

class MapEntryS64 : public MapEntryInterface {
  public:
    MapEntryS64(int init_val = 0, const std::string& type_str = "s64")
        : MapEntryInterface(type_str)
        , data_(init_val)
    {
    }

  private:
    uint64_t data_;

    friend class boost::serialization::access;
    template <class Ar> void serialize(Ar& ar, unsigned) {
        ar& boost::serialization::base_object<MapEntryInterface>(*this);
        ar& data_;
    }

    virtual void print(std::ostream& os) const override {
        os << "S64(" << data_ << ", " << std::quoted(type_) << ")";
    }
};

class MapEntryF64 : public MapEntryInterface {
  public:
    MapEntryF64(double init_val = 0, const std::string& type_str = "f64")
        : MapEntryInterface(type_str)
        , data_(init_val)
    {
    }

  private:
    double data_;
    friend class boost::serialization::access;
    template <class Ar> void serialize(Ar& ar, unsigned)
    {
        ar& boost::serialization::base_object<MapEntryInterface>(*this);
        ar& data_;
    }

    virtual void print(std::ostream& os) const override {
        os << "F64(" << data_ << ", " << std::quoted(type_) << ")";
    }
};

class MapDatabase {
  public:
    using Map = std::map<std::string, EntryPtr>;

    MapDatabase() {
        database_.emplace("key1", std::make_unique<MapEntryS64>(381));
        database_.emplace("key2", std::make_unique<MapEntryF64>(3.124512));
    };

    bool SaveToFile(std::string const& filename) const
    {
        try {
            std::ofstream ofs(filename, std::ios::binary);
            boost::archive::text_oarchive oa(ofs);
            oa << *this;
            return true;
        } catch (std::exception& ex) {
            std::cout << ex.what() << std::endl;
            return false;
        }
    }

    static MapDatabase loadFromFile(std::string const& filename)
    {
        MapDatabase db;
        std::ifstream ifs(filename, std::ios::binary);
        boost::archive::text_iarchive ia(ifs);
        ia >> db;
        return db;
    }

    friend std::ostream& operator<<(std::ostream& os, MapDatabase const& mdb)
    {
        for (auto const& [k, b] : mdb.database_)
            if (b) os << std::quoted(k) << " -> " << *b << "\n";
            else   os << std::quoted(k) << " -> NULL\n";

        return os;
    }

  private:
    Map database_;

    friend class boost::serialization::access;
    template <class Ar> void serialize(Ar& ar, unsigned) { ar& database_; }
};

int main() {
    {
        MapDatabase tmp;
        std::cout << "------ tmp:\n" << tmp << "\n";
        if (not tmp.SaveToFile("boost_export.dat"))
            return 1;
    }

    MapDatabase roundtrip = MapDatabase::loadFromFile("boost_export.dat");
    std::cout << "------ roundtrip:\n" << roundtrip << "\n";
}

Prints

------ tmp:
"key1" -> S64(381, "s64")
"key2" -> F64(3.12451, "f64")

unregistered class - derived class not registered or exported

unregistered class runtime error

The message says it all: at time of deserialization, there is no information about the types that can be deserialized. Add that:

#include <boost/serialization/export.hpp>
BOOST_CLASS_EXPORT(MapEntryF64)
BOOST_CLASS_EXPORT(MapEntryS64)

Now it prints

------ tmp:
"key1" -> S64(381, "s64")
"key2" -> F64(3.12451, "f64")

------ roundtrip:
"key1" -> S64(381, "s64")
"key2" -> F64(3.12451, "f64")

Several Translation Units

In the case of separate translation units, split the class export macros as per the documentation. E.g. the class export macro ends up doing

#define BOOST_CLASS_EXPORT_GUID(T, K)                                  \
BOOST_CLASS_EXPORT_KEY2(T, K)                                          \
BOOST_CLASS_EXPORT_IMPLEMENT(T)                                        \
/**/

So, naively (taking another half-hour to split it all op sensibly:)

  • File test.cpp

     #include "MapDatabase.h"
     #include <iostream>
    
     int main() {
         {
             MapDatabase tmp;
             std::cout << "------ tmp:\n" << tmp << "\n";
             if (not tmp.SaveToFile("boost_export.dat"))
                 return 1;
         }
    
         MapDatabase roundtrip = MapDatabase::loadFromFile("boost_export.dat");
         std::cout << "------ roundtrip:\n" << roundtrip << "\n";
     }
    
  • File MapDatabase.h

     #pragma once
     #include "MapEntryS64.h"
     #include "MapEntryF64.h"
     #include <boost/serialization/map.hpp>
     #include <boost/serialization/unique_ptr.hpp>
    
     class MapDatabase {
       public:
         using Map = std::map<std::string, EntryPtr>;
    
         MapDatabase();
    
         bool SaveToFile(std::string const& filename) const;
         static MapDatabase loadFromFile(std::string const& filename);
    
         friend std::ostream& operator<<(std::ostream&, MapDatabase const&);
    
       private:
         Map database_;
    
         friend class boost::serialization::access;
         template <class Ar> void serialize(Ar& ar, unsigned) { ar& database_; }
     };
    
  • File MapDatabase.cpp

     #include "MapDatabase.h"
     #include <boost/archive/text_iarchive.hpp>
     #include <boost/archive/text_oarchive.hpp>
     #include <fstream>
     #include <iostream>
    
     MapDatabase::MapDatabase() {
         database_.emplace("key1", std::make_unique<MapEntryS64>(381));
         database_.emplace("key2", std::make_unique<MapEntryF64>(3.124512));
     }
    
     bool MapDatabase::SaveToFile(std::string const& filename) const
     {
         try {
             std::ofstream ofs(filename, std::ios::binary);
             boost::archive::text_oarchive oa(ofs);
             oa << *this;
             return true;
         } catch (std::exception& ex) {
             std::cout << ex.what() << std::endl;
             return false;
         }
     }
    
     MapDatabase MapDatabase::loadFromFile(std::string const& filename)
     {
         MapDatabase db;
         std::ifstream ifs(filename, std::ios::binary);
         boost::archive::text_iarchive ia(ifs);
         ia >> db;
         return db;
     }
    
     std::ostream& operator<<(std::ostream& os, MapDatabase const& mdb)
     {
         for (auto const& [k, b] : mdb.database_)
             if (b) os << std::quoted(k) << " -> " << *b << "\n";
             else   os << std::quoted(k) << " -> NULL\n";
    
         return os;
     }
    
  • File MapEntryF64.h

     #pragma once
     #include "MapEntryInterface.h"
     #include <boost/serialization/base_object.hpp>
    
     class MapEntryF64 : public MapEntryInterface {
       public:
         MapEntryF64(int init_val = 0, const std::string& type_str = "f64");
    
       private:
         uint64_t data_;
    
         friend class boost::serialization::access;
         template <class Ar> void serialize(Ar& ar, unsigned) {
             ar& boost::serialization::base_object<MapEntryInterface>(*this);
             ar& data_;
         }
    
         virtual void print(std::ostream& os) const override;
     };
    
     #include <boost/serialization/export.hpp>
     BOOST_CLASS_EXPORT_KEY(MapEntryF64)
    
  • File MapEntryInterface.h

     #pragma once
     #include <iosfwd>
     #include <string>
     #include <memory>
     #include <boost/serialization/access.hpp>
    
     class MapEntryInterface {
       public:
         virtual ~MapEntryInterface() = default;
         virtual void print(std::ostream&) const = 0;
    
       protected:
         MapEntryInterface(std::string type_str);
         std::string type_;
    
         friend class boost::serialization::access;
         template <class Ar> void serialize(Ar& ar, unsigned) { ar& type_; }
    
         friend std::ostream& operator<<(std::ostream&, MapEntryInterface const&);
     };
    
     using EntryPtr = std::unique_ptr<MapEntryInterface>;
    
  • File MapEntryS64.h

     #pragma once
     #include "MapEntryInterface.h"
     #include <boost/serialization/base_object.hpp>
    
     class MapEntryS64 : public MapEntryInterface {
       public:
         MapEntryS64(int init_val = 0, const std::string& type_str = "s64");
    
       private:
         uint64_t data_;
    
         friend class boost::serialization::access;
         template <class Ar> void serialize(Ar& ar, unsigned) {
             ar& boost::serialization::base_object<MapEntryInterface>(*this);
             ar& data_;
         }
    
         virtual void print(std::ostream& os) const override;
     };
    
     #include <boost/serialization/export.hpp>
     BOOST_CLASS_EXPORT_KEY(MapEntryS64)
    
  • File MapEntryF64.cpp

     #include "MapEntryF64.h"
     #include <ostream>
     #include <iomanip>
    
     MapEntryF64::MapEntryF64(int init_val, const std::string& type_str)
         : MapEntryInterface(type_str)
         , data_(init_val)
     {
     }
    
     void MapEntryF64::print(std::ostream& os) const {
         os << "F64(" << data_ << ", " << std::quoted(type_) << ")";
     }
    
     BOOST_CLASS_EXPORT_IMPLEMENT(MapEntryF64)
    
  • File MapEntryInterface.cpp

     #include "MapEntryInterface.h"
    
     MapEntryInterface::MapEntryInterface(std::string type_str) : type_(type_str) {}
    
     std::ostream& operator<<(std::ostream& os, MapEntryInterface const& e)
     {
         e.print(os);
         return os;
     }
    
  • File MapEntryS64.cpp

     #include "MapEntryS64.h"
     #include <ostream>
     #include <iomanip>
    
     MapEntryS64::MapEntryS64(int init_val, const std::string& type_str)
         : MapEntryInterface(type_str)
         , data_(init_val)
     {
     }
    
     void MapEntryS64::print(std::ostream& os) const {
         os << "S64(" << data_ << ", " << std::quoted(type_) << ")";
     }
    
     BOOST_CLASS_EXPORT_IMPLEMENT(MapEntryS64)
    

Prints: Live On Wandbox

------ tmp:
"key1" -> S64(381, "s64")
"key2" -> F64(3, "f64")

unregistered class - derived class not registered or exported

UHOH Are We Doomed?

Not at all. Just need to read the documentation closely:

BOOST_CLASS_EXPORT in the same source module that includes any of the archive class headers will instantiate code required to serialize polymorphic pointers of the indicated type to the all those archive classes. If no archive class headers are included, then no code will be instantiated.

So, adding the includes:

#include <boost/archive/text_oarchive.hpp>
#include <boost/archive/text_iarchive.hpp>
BOOST_CLASS_EXPORT_IMPLEMENT(MapEntryF64)

(and the same for MapEntryS64):

Live On Wandbox

------ tmp:
"key1" -> S64(381, "s64")
"key2" -> F64(3, "f64")

------ roundtrip:
"key1" -> S64(381, "s64")
"key2" -> F64(3, "f64")

BONUS: Simplicity

I prefer simplicity. I would probably replace all of the above with:

  • File MapDatabase.h

     #pragma once
     #include <map>
     #include <boost/variant.hpp>
    
     namespace Database {
         struct Nil { void serialize(auto&, unsigned) {} };
         using S64   = uint64_t;
         using F64   = double;
         using Entry = boost::variant<Nil, S64, F64>;
         using Map   = std::map<std::string, Entry>;
    
         std::string_view typeOf(Entry const&);
    
         void SaveToFile(std::string const& filename, Map const& m);
         [[nodiscard]] Map loadFromFile(std::string const& filename);
    
         std::ostream& operator<<(std::ostream&, Nil);
         std::ostream& operator<<(std::ostream&, Map const&);
     } // namespace Database
    
  • File MapDatabase.cpp

     #include "MapDatabase.h"
     #include <boost/archive/text_iarchive.hpp>
     #include <boost/archive/text_oarchive.hpp>
     #include <boost/serialization/map.hpp>
     #include <boost/serialization/variant.hpp>
     #include <fstream>
     #include <iomanip>
     #include <iostream>
    
     namespace Database {
         std::string_view typeOf(Entry const& e) {
             assert(e.which() < 3);
             return std::array{"Nil", "S64", "F64"}[e.which()];
         }
    
         void SaveToFile(std::string const& filename, Map const& m)
         {
             std::ofstream ofs(filename, std::ios::binary);
             boost::archive::text_oarchive oa(ofs);
             oa << m;
         }
    
         Map loadFromFile(std::string const& filename)
         {
             Map                           db;
             std::ifstream                 ifs(filename, std::ios::binary);
             boost::archive::text_iarchive ia(ifs);
             ia >> db;
             return db;
         }
    
         std::ostream& operator<<(std::ostream& os, Nil) { return os << "NULL"; }
         std::ostream& operator<<(std::ostream& os, Map const& m)
         {
             for (auto const& [k, v] : m)
                 os << typeOf(v) << "\t" << std::quoted(k) << " -> " << v << "\n";
             return os;
         }
     } // namespace Database
    
  • File test.cpp

     #include "MapDatabase.h"
     #include <iostream>
    
     int main() {
         SaveToFile("boost_export.dat",
                    Database::Map{
                        {"key1", 381ul},
                        {"key3", {}},
                        {"key2", 3.124512},
                    });
    
         std::cout << Database::loadFromFile("boost_export.dat");
     }
    

Printing Live On Wandbox

S64 "key1" -> 381
F64 "key2" -> 3.12451
Nil "key3" -> NULL

Upvotes: 1

Related Questions