Search code examples
c++serializationboostpolymorphism

Boost serialize class that contains pointer to polymorphic class


I'm trying to serialize the Container class which has a pointer to the polymorphic member m_parent. The problem is that the serialization is done on the Parent class and not on the ChildA class, and the test program that I wrote prints an empty m_parent.

I cannot figure out why it does not work. What am I doing wrong?

#include <iostream>
#include <vector>
#include <memory>
#include <boost/serialization/base_object.hpp>
#include <boost/serialization/access.hpp>
#include <boost/serialization/assume_abstract.hpp>
#include <boost/serialization/export.hpp>
#include <boost/filesystem.hpp>
#include <boost/filesystem/fstream.hpp>
#include <boost/archive/text_iarchive.hpp>
#include <boost/archive/text_oarchive.hpp>
#include <boost/serialization/vector.hpp>
#include <boost/serialization/unique_ptr.hpp>

class Parent
{
public:
    Parent();

    virtual ~Parent() = default;

    virtual std::string print() const;

    friend std::ostream& operator<<(std::ostream& out, const Parent& Parent);

    friend class boost::serialization::access;

    template<class Archive>
    void serialize(Archive& ar, const unsigned int version)
    {

    }
};

Parent::Parent()
{

}

std::string Parent::print() const
{
    return {};
}

std::ostream& operator<<(std::ostream& out, const Parent& Parent)
{
    return out << Parent.print();
}

BOOST_SERIALIZATION_ASSUME_ABSTRACT(Parent)

class ChildA : public Parent
{
private:
    double m_value;

public:
    ChildA() {};

    ChildA(double value);

    std::string print() const override;

    friend class boost::serialization::access;

    template<class Archive>
    void serialize(Archive& ar, const unsigned int version)
    {
        ar.template register_type<ChildA>();
        ar & boost::serialization::base_object<Parent>(*this);
        ar & m_value;
    }
};

ChildA::ChildA(double value) : m_value(value)
{

}

std::string ChildA::print() const
{
    return std::to_string(m_value);
}

BOOST_CLASS_EXPORT(ChildA)

class Container
{
private:
    unsigned long m_id;
    std::unique_ptr<Parent> m_parent;
public:
    Container() {};

    Container(unsigned long id, std::unique_ptr<Parent> parent);

    Container(const Container& container);

    friend std::ostream& operator<<(std::ostream& out, const Container& container);

    friend class boost::serialization::access;

    template<class Archive>
    void serialize(Archive& ar, const unsigned int version)
    {
        ar.template register_type<ChildA>();
        ar & m_id & m_parent;
    }
};



Container::Container(unsigned long id, std::unique_ptr<Parent> parent) : m_id(id), m_parent(std::move(parent))
{

}

Container::Container(const Container& container): m_id(container.m_id), m_parent(std::make_unique<Parent>((*container.m_parent)))
{

}

std::ostream& operator<<(std::ostream& out, const Container& container)
{
    return out << container.m_id << "," << *container.m_parent;
}

BOOST_CLASS_EXPORT(Container)

int main()
{
    Container container_1(1, std::make_unique<ChildA>(2.3));
    std::cout << container_1 << std::endl;

    std::vector<std::unique_ptr<Container>> b;
    b.push_back(std::make_unique<Container>(container_1));

    boost::filesystem::path file = boost::filesystem::current_path() / "test.dat";

    if (boost::filesystem::exists(file))
    {
        boost::filesystem::ifstream ifs(file);
        boost::archive::text_iarchive ta(ifs);
        ta & b;

        std::cout << *b[0] << std::endl;
    }
    else
    {
        boost::filesystem::ofstream ofs(file);
        boost::archive::text_oarchive ta(ofs);
        ta & b;
    }
}

Solution

  • You're already writing a default-constructed Parent as m_parent:

    Live On Coliru

    #include <boost/archive/text_iarchive.hpp>
    #include <boost/archive/text_oarchive.hpp>
    #include <boost/filesystem.hpp>
    #include <boost/filesystem/fstream.hpp>
    #include <boost/serialization/access.hpp>
    #include <boost/serialization/assume_abstract.hpp>
    #include <boost/serialization/base_object.hpp>
    #include <boost/serialization/export.hpp>
    #include <boost/serialization/unique_ptr.hpp>
    #include <boost/serialization/vector.hpp>
    
    #include <iostream>
    #include <memory>
    #include <vector>
    
    class Parent {
      public:
        Parent() = default;
    
        virtual ~Parent() = default;
        virtual std::string  print() const { return {}; }
    
        friend class boost::serialization::access;
        template <class Archive> void serialize(Archive&, unsigned) {}
        friend std::ostream& operator<<(std::ostream& out, Parent const& Parent) { return out << Parent.print(); }
    };
    
    BOOST_SERIALIZATION_ASSUME_ABSTRACT(Parent)
    
    class ChildA : public Parent {
        double m_value;
    
      public:
        ChildA(double value = 0.0) : m_value(value) {}
    
        std::string print() const override { return std::to_string(m_value); }
    
        friend class boost::serialization::access;
    
        template <class Archive> void serialize(Archive& ar, unsigned) {
            // ar.template register_type<ChildA>();
            ar & boost::serialization::base_object<Parent>(*this) & m_value;
        }
    };
    
    BOOST_CLASS_EXPORT(ChildA)
    
    class Container {
      private:
        unsigned long           m_id;
        std::unique_ptr<Parent> m_parent;
    
      public:
        Container(unsigned long id = 0, std::unique_ptr<Parent> parent = {})
            : m_id(id)
            , m_parent(std::move(parent)) {}
    
        Container(Container const& container)
            : m_id(container.m_id)
            , m_parent(std::make_unique<Parent>((*container.m_parent))) {}
    
        friend std::ostream& operator<<(std::ostream& out, Container const& container) {
            return out << container.m_id << "," << *container.m_parent;
        }
    
        friend class boost::serialization::access;
        template <class Archive> void serialize(Archive& ar, unsigned) {
            ar & m_id & m_parent;
        }
    };
    
    // BOOST_CLASS_EXPORT(Container)
    
    int main() {
        boost::filesystem::path file = "test.dat";
        if (exists(file))
            remove(file);
    
        while(true) {
            if (exists(file)) {
                std::vector<std::unique_ptr<Container>> b;
                {
                    boost::filesystem::ifstream   ifs(file);
                    boost::archive::text_iarchive ta(ifs);
                    ta >> b;
                }
    
                std::cout << file << " loaded: " << *b.front() << std::endl;
    
                break; // exits program
            } else {
                Container container_1(23, std::make_unique<ChildA>(2.3));
                std::vector<std::unique_ptr<Container>> b;
                b.push_back(std::make_unique<Container>(container_1));
    
                {
                    boost::filesystem::ofstream  ofs(file);
                    boost::archive::text_oarchive ta(ofs);
                    ta << b;
                }
                std::cout << file << " written: " << *b.front() << std::endl;
            }
        }
    }
    

    Prints

    "test.dat" written: 23,
    "test.dat" loaded: 23,
    

    The Problem?

    The problem is the copy-constructor. Observe that when replacing

    Container container_1(23, std::make_unique<ChildA>(2.3));
    std::vector<std::unique_ptr<Container>> b;
    b.push_back(std::make_unique<Container>(container_1));
    

    With

    std::vector<std::unique_ptr<Container>> b;
    b.push_back(std::make_unique<Container>(23, std::make_unique<ChildA>(2.3)));
    

    The output becomes the expected:

    "test.dat" written: 23,2.300000
    "test.dat" loaded: 23,2.300000
    

    The cause is that

    m_parent(std::make_unique<Parent>(*container.m_parent))
    

    invokes the Parent::Parent(Parent const&) copy constructor - which is obviously not virtual. Use the clone pattern to allow deep-copying of polymorphic types:

    virtual std::unique_ptr<Parent> clone() const { return std::make_unique<Parent>(); }
    

    Which you then override like:

    virtual std::unique_ptr<Parent> clone() const override { return std::make_unique<ChildA>(m_value); }
    

    Now the original code can be fixed by fixing the copy constructor:

    Container(Container const& rhs)
        : m_id(rhs.m_id)
        , m_parent(rhs.m_parent ? rhs.m_parent->clone() : nullptr) {}
    

    Note that I opted to make the code a little safer by checking for nullptr.

    See it Live On Coliru

    #include <boost/archive/text_iarchive.hpp>
    #include <boost/archive/text_oarchive.hpp>
    #include <boost/filesystem.hpp>
    #include <boost/filesystem/fstream.hpp>
    #include <boost/serialization/access.hpp>
    #include <boost/serialization/assume_abstract.hpp>
    #include <boost/serialization/base_object.hpp>
    #include <boost/serialization/export.hpp>
    #include <boost/serialization/unique_ptr.hpp>
    #include <boost/serialization/vector.hpp>
    
    #include <iostream>
    #include <memory>
    #include <vector>
    
    class Parent {
      public:
        Parent() = default;
    
        virtual ~Parent() = default;
        virtual std::string print() const { return {}; }
        virtual std::unique_ptr<Parent> clone() const { return std::make_unique<Parent>(); }
    
        friend class boost::serialization::access;
        template <class Archive> void serialize(Archive&, unsigned) {}
        friend std::ostream& operator<<(std::ostream& out, Parent const& Parent) { return out << Parent.print(); }
    };
    
    BOOST_SERIALIZATION_ASSUME_ABSTRACT(Parent)
    
    class ChildA : public Parent {
        double m_value;
    
      public:
        ChildA(double value = 0.0) : m_value(value) {}
    
        std::string print() const override { return std::to_string(m_value); }
        virtual std::unique_ptr<Parent> clone() const override { return std::make_unique<ChildA>(m_value); }
    
        friend class boost::serialization::access;
    
        template <class Archive> void serialize(Archive& ar, unsigned) {
            // ar.template register_type<ChildA>();
            ar & boost::serialization::base_object<Parent>(*this) & m_value;
        }
    };
    
    BOOST_CLASS_EXPORT(ChildA)
    
    class Container {
      private:
        unsigned long           m_id;
        std::unique_ptr<Parent> m_parent;
    
      public:
        Container(unsigned long id = 0, std::unique_ptr<Parent> parent = {})
            : m_id(id)
            , m_parent(std::move(parent)) {}
    
        Container(Container const& rhs)
            : m_id(rhs.m_id)
            , m_parent(rhs.m_parent ? rhs.m_parent->clone() : nullptr) {}
    
        friend std::ostream& operator<<(std::ostream& out, Container const& container) {
            return out << container.m_id << "," << *container.m_parent;
        }
    
        friend class boost::serialization::access;
        template <class Archive> void serialize(Archive& ar, unsigned) {
            ar & m_id & m_parent;
        }
    };
    
    // BOOST_CLASS_EXPORT(Container)
    
    int main() {
        boost::filesystem::path file = "test.dat";
        if (exists(file))
            remove(file);
    
        while(true) {
            if (exists(file)) {
                std::vector<std::unique_ptr<Container>> b;
                {
                    boost::filesystem::ifstream   ifs(file);
                    boost::archive::text_iarchive ta(ifs);
                    ta >> b;
                }
    
                std::cout << file << " loaded: " << *b.front() << std::endl;
    
                break; // exits program
            } else {
                Container container_1(23, std::make_unique<ChildA>(2.3));
                std::vector<std::unique_ptr<Container>> b;
                b.push_back(std::make_unique<Container>(container_1));
    
                {
                    boost::filesystem::ofstream  ofs(file);
                    boost::archive::text_oarchive ta(ofs);
                    ta << b;
                }
                std::cout << file << " written: " << *b.front() << std::endl;
            }
        }
    }
    

    Printing

    "test.dat" written: 23,2.300000
    "test.dat" loaded: 23,2.300000
    

    Side Notes

    Note that BOOST_CLASS_EXPORT(Container) is not required because that's not a polymorphic type. Also the register_type call is redundant with the type being correctly exported.