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;
}
}
You're already writing a default-constructed Parent
as m_parent
:
#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 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
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.