c++boostc++14xml-serialization

How to Serialize/Deserialize an unordered_map member of a derived Class


So I'm building a simulated file-system in C++ to study the language better and maybe some system level programming. Im using Boost::Serialization to save the state of the file system when the user exits, but I'm having trouble in saving/loading my classes. Here is my base class:

enum filetype { FSFILE, FSDIRECTORY, ROOT };

class FileObject {
private:
  friend class boost::serialization::access;
  template<class Archive>
  void serialize(Archive & ar, const unsigned int) {
    ar & BOOST_SERIALIZATION_NVP(name);
    ar & BOOST_SERIALIZATION_NVP(date_of_creation);
    ar & BOOST_SERIALIZATION_NVP(type);
  }
protected:
  std::string name;
  std::string date_of_creation;
  filetype type;

Here is my first derived class that will be basically a .txt file in the system:

class File : public FileObject {
private:
  friend class boost::serialization::access;
  
  template<class Archive>
  void serialize(Archive & ar, const unsigned int) {
    ar & BOOST_SERIALIZATION_BASE_OBJECT_NVP(FileObject);
    ar & BOOST_SERIALIZATION_NVP(content);
    ar & BOOST_SERIALIZATION_NVP(size);
  }

protected:

  std::string content;
  int size;

And finally here is the Directory class that will act as the directory that holds files and/or other directories:


class Directory : public FileObject {
private:
  friend class boost::serialization::access;
  template<class Archive>
  void serialize(Archive & ar, const unsigned int) {
    ar & BOOST_SERIALIZATION_BASE_OBJECT_NVP(FileObject);
    ar & BOOST_SERIALIZATION_NVP(num_of_contents);
    ar & BOOST_SERIALIZATION_NVP(size_of_contents);
    for (auto it = this->contents.begin(); it != this->contents.end(); it++) {
      ar & BOOST_SERIALIZATION_NVP(it->second);
    }
  }

protected:
  int num_of_contents;
  int size_of_contents;
public:
  std::unordered_map<std::string, FileObject *> contents;

In my main.cc file I have 2 functions, one for saving and one for loading

void save_state(const Directory &s, const char * filename){
    // make an archive
    std::ofstream ofs(filename);
    assert(ofs.good());
    boost::archive::xml_oarchive oa(ofs);
    oa << BOOST_SERIALIZATION_NVP(s);
}
void
restore_state(Directory &s, const char * filename)
{
    // open the archive
    std::ifstream ifs(filename);
    assert(ifs.good());
    boost::archive::xml_iarchive ia(ifs);

    ia >> BOOST_SERIALIZATION_NVP(s);
}

And finally here is the rest main.cc file that creates some files and dirs and saves/loads for testing purposes :

int main() {

  char c;

  scanf("%c", &c);
  std::string filename(boost::archive::tmpdir());
  filename += "/demo_save.xml";
  if (c == 's') {
    File file("test_file1", filetype::FSFILE);
    File file2("test_file2", filetype::FSFILE);
    File file3("test_file3", filetype::FSFILE);
    File file4("test_file4", filetype::FSFILE);
    Directory dir("test_dir1", filetype::FSDIRECTORY);
    Directory dir2("test_dir2", filetype::FSDIRECTORY);
    dir.insertContent(file.getName(), &file);
    dir.insertContent(file2.getName(), &file2);
    dir2.insertContent(file3.getName(), &file3);
    dir2.insertContent(file4.getName(), &file4);
    dir.insertContent(dir2.getName(), &dir2);
    save_state(dir, filename.c_str());
  }
  Directory newd;
  if (c == 'l') {
    restore_state(newd, filename.c_str());
    for (auto it = newd.contents.begin(); it != newd.contents.end(); it++) {
      if (it->second->getType() == filetype::FSFILE) {
    std::cout << it->first << std::endl;
    }
      else if (it->second->getType() == filetype::FSDIRECTORY) {
    for (auto jt = ((Directory *)it->second)->contents.begin(); jt != ((Directory *)it->second)->contents.end(); jt++) {
      std::cout << jt->second->getName() << std::endl;
    }
      }
    }
  }

  return 0;
}

The programm compiles fine but i get seg fault in the second loop. And from reading the .xml file, the files inside the dir2 dont get properly serialized.

Are my classes and functions correct? Is this the correct way to serialize an unordered_map that holds pointers to other classes?


Solution

  • As others have pointed out you have ownership issues. You can serialize pointers but deserializing will result in memory leaks.

    Instead, make the pointers owned. I will use unique_ptr to manage that instead of writing lots of code to manage the lifetimes correctly.

    std::unordered_map<std::string, std::unique_ptr<FileObject> > contents;
    

    Then, you must make sure the hierarchy is virtual, by at least adding

    virtual ~FileObject() = default;
    

    I've elected to make the FileObject streamable by also supplying a print virtual method.

    Lastly, register the types, or mark them abstract as appropriate:

    BOOST_SERIALIZATION_ASSUME_ABSTRACT(FileObject)
    BOOST_CLASS_EXPORT(File)
    BOOST_CLASS_EXPORT(Directory)
    

    Full Demo

    Live On Coliru

    #include <boost/archive/xml_iarchive.hpp>
    #include <boost/archive/xml_oarchive.hpp>
    
    #include <boost/serialization/access.hpp>
    #include <boost/serialization/base_object.hpp>
    #include <boost/serialization/export.hpp>
    #include <boost/serialization/nvp.hpp>
    #include <boost/serialization/unique_ptr.hpp>
    #include <boost/serialization/unordered_map.hpp>
    
    #include <filesystem>
    #include <iostream>
    #include <set>
    namespace fs = std::filesystem;
    
    enum filetype { FSFILE, FSDIRECTORY, ROOT };
    
    class FileObject {
      public:
        std::string getName() const { return name; }
        virtual ~FileObject() = default;
        virtual void print(std::ostream& os, std::string const& prefix = "") const {
            os << "[" << prefix << "/]" << getName() << std::endl;
        }
    
      private:
        friend class boost::serialization::access;
        template <class Ar> void serialize(Ar& ar, unsigned) {
            ar& BOOST_NVP(name) & BOOST_NVP(date_of_creation) & BOOST_NVP(type);
        }
    
      protected:
        FileObject() = default; // only for deserialization
        FileObject(std::string name, filetype type) : name(std::move(name)), type(type) {
            // TODO date_of_creation
        }
    
        std::string name;
        std::string date_of_creation;
        filetype    type = ROOT;
    
        friend std::ostream& operator<<(std::ostream& os, FileObject const& fo) {
            fo.print(os);
            return os;
        }
    };
    
    class File : public FileObject {
      public:
        File(std::string name, size_t size) : FileObject(std::move(name), FSFILE), size(size) {}
    
      private:
        File() = default; // only for deserialization
        friend class boost::serialization::access;
    
        template <class Ar> void serialize(Ar& ar, unsigned) {
            ar& BOOST_SERIALIZATION_BASE_OBJECT_NVP(FileObject) & BOOST_NVP(content) & BOOST_NVP(size);
        }
    
      protected:
        std::string content;
        size_t      size = 0;
    };
    
    class Directory : public FileObject {
      public:
        Directory() : FileObject("/", ROOT) {}
        Directory(std::string name, filetype type = FSDIRECTORY) : FileObject(name, type) {
            assert(FSDIRECTORY == type);
        }
    
        bool insertContent(std::unique_ptr<FileObject> object) {
            std::string name = object->getName();
            auto [it, ok] = contents.emplace(std::move(name), std::move(object));
            if (ok)
                num_of_contents += 1; // TODO size_of_contents?
            assert(contents.size() == num_of_contents);
            return ok;
        }
    
      private:
        std::unordered_map<std::string, std::unique_ptr<FileObject> > contents;
    
        friend class boost::serialization::access;
        template <class Ar> void serialize(Ar& ar, unsigned) {
            ar& BOOST_SERIALIZATION_BASE_OBJECT_NVP(FileObject) & BOOST_NVP(num_of_contents) &
                BOOST_NVP(size_of_contents) & BOOST_NVP(contents);
        }
    
      protected:
        size_t num_of_contents  = 0;
        size_t size_of_contents = 0;
    
        virtual void print(std::ostream& os, std::string const& prefix) const override {
            FileObject::print(os, prefix);
            for (auto const& [n, obj] : contents)
                if (obj)
                    obj->print(os, prefix + "/" + getName());
        }
    };
    
    BOOST_SERIALIZATION_ASSUME_ABSTRACT(FileObject)
    BOOST_CLASS_EXPORT(File)
    BOOST_CLASS_EXPORT(Directory)
    
    #include <fstream>
    
    static inline void save_state(Directory const& s, fs::path const& filename) {
        std::ofstream                ofs(filename);
        boost::archive::xml_oarchive oa(ofs);
        oa << BOOST_NVP(s);
    }
    
    static inline void restore_state(Directory& s, fs::path const& filename) {
        std::ifstream                ifs(filename);
        boost::archive::xml_iarchive ia(ifs);
        ia >> BOOST_NVP(s);
    }
    
    int main(int argc, char** argv) {
        std::set<std::string_view> const args(argv + 1, argv + argc);
    
        auto filename = fs::temp_directory_path() / "demo_save.xml";
    
        if (args.contains("save")) {
            Directory dir("test_dir1", filetype::FSDIRECTORY);
            for (auto name : {"test_file1", "test_file2", "test_file3", "test_file4"})
                dir.insertContent(std::make_unique<File>(name, filetype::FSFILE));
            dir.insertContent(std::make_unique<Directory>("test_dir2"));
    
            save_state(dir, filename);
        }
    
        if (args.contains("load")) {
            Directory newd;
            restore_state(newd, filename);
    
            std::cout << newd;
        }
    }
    

    Prints

    [/]test_dir1
    [/test_dir1/]test_file1
    [/test_dir1/]test_file2
    [/test_dir1/]test_file3
    [/test_dir1/]test_dir2
    [/test_dir1/]test_file4
    

    And the xml contains:

    <?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
    <!DOCTYPE boost_serialization>
    <boost_serialization signature="serialization::archive" version="19">
    <s class_id="0" tracking_level="1" version="0" object_id="_0">
        <FileObject class_id="1" tracking_level="1" version="0" object_id="_1">
            <name>test_dir1</name>
            <date_of_creation></date_of_creation>
            <type>1</type>
        </FileObject>
        <num_of_contents>5</num_of_contents>
        <size_of_contents>0</size_of_contents>
        <contents class_id="2" tracking_level="0" version="0">
            <count>5</count>
            <bucket_count>13</bucket_count>
            <item_version>0</item_version>
            <item class_id="3" tracking_level="0" version="0">
                <first>test_file4</first>
                <second class_id="4" tracking_level="0" version="0">
                    <tx class_id="5" class_name="File" tracking_level="1" version="0" object_id="_2">
                        <FileObject object_id="_3">
                            <name>test_file4</name>
                            <date_of_creation></date_of_creation>
                            <type>0</type>
                        </FileObject>
                        <content></content>
                        <size>0</size>
                    </tx>
                </second>
            </item>
            <item>
                <first>test_dir2</first>
                <second>
                    <tx class_id_reference="0" object_id="_4">
                        <FileObject object_id="_5">
                            <name>test_dir2</name>
                            <date_of_creation></date_of_creation>
                            <type>1</type>
                        </FileObject>
                        <num_of_contents>0</num_of_contents>
                        <size_of_contents>0</size_of_contents>
                        <contents>
                            <count>0</count>
                            <bucket_count>1</bucket_count>
                            <item_version>0</item_version>
                        </contents>
                    </tx>
                </second>
            </item>
            <item>
                <first>test_file3</first>
                <second>
                    <tx class_id_reference="5" object_id="_6">
                        <FileObject object_id="_7">
                            <name>test_file3</name>
                            <date_of_creation></date_of_creation>
                            <type>0</type>
                        </FileObject>
                        <content></content>
                        <size>0</size>
                    </tx>
                </second>
            </item>
            <item>
                <first>test_file2</first>
                <second>
                    <tx class_id_reference="5" object_id="_8">
                        <FileObject object_id="_9">
                            <name>test_file2</name>
                            <date_of_creation></date_of_creation>
                            <type>0</type>
                        </FileObject>
                        <content></content>
                        <size>0</size>
                    </tx>
                </second>
            </item>
            <item>
                <first>test_file1</first>
                <second>
                    <tx class_id_reference="5" object_id="_10">
                        <FileObject object_id="_11">
                            <name>test_file1</name>
                            <date_of_creation></date_of_creation>
                            <type>0</type>
                        </FileObject>
                        <content></content>
                        <size>0</size>
                    </tx>
                </second>
            </item>
        </contents>
    </s>
    </boost_serialization>