1

I'm not allowed to put any (template) function definitions in the header. I have a class that doesn't have a standard constructor.

To get serialization to work without a constructor I use save_construct_data and load_construct_data. I have the declarations in the header file like so. But they don't seem to link up with the definitions in the cpp file. I mean, the serialize function does, but that doesn't actually serialize a lot.

Declarations:

// Foo.h
#include <boost/serialization/access.hpp>
#include <boost/serialization/export.hpp>

#include <boost/archive/xml_oarchive.hpp>
#include <boost/archive/xml_iarchive.hpp>

#include <iostream>

class Foo {
    friend class boost::serialization::access;
    int m_bar;

    template<class Archive>
    void serialize(Archive & ar, const unsigned int file_version);
public:
    Foo(int bar);
    int getBar() const;
};

BOOST_CLASS_EXPORT(Foo);

namespace boost_1_69_0 {
namespace serialization {
template<class Archive>
void save_construct_data(Archive& ar, const Foo* t, const unsigned int version);

template<class Archive>
void load_construct_data(Archive& ar, Foo* t, const unsigned int version);
}
}

Definitions:

//Foo.cpp
#include "Foo.hpp"

#include <boost/serialization/nvp.hpp>

Foo::Foo(int bar): m_bar(bar) {};

int Foo::getBar() const { return m_bar; }

template<>
void serialize(boost::archive::xml_oarchive& ar, const unsigned int file_version) {}

template<>
void serialize(boost::archive::xml_iarchive& ar, const unsigned int file_version) {}

namespace boost_1_69_0 {
namespace serialization {
template<>
void save_construct_data(xml_oarchive& ar, const Foo* t, const unsigned int version) {
    auto  bar = t->getBar();
    ar << BOOST_SERIALIZATION_NVP(bar);
}

template<>
void load_construct_data(xml_iarchive& ar, Foo* t, const unsigned int version) {
    int bar;
    ar >> BOOST_SERIALIZATION_NVP(bar);
    ::new(t) Foo(bar);
}

}
#include "Foo.hpp"
#include <iostream>

int main() {
    Foo foo(42);
    boost::archive::xml_oarchive oa(std::cout);
    oa << BOOST_SERIALIZATION_NVP(foo);
    return 0;
}

Oh btw, I don't know why, but if I call the namespace boost instead of boost_1_69_0 I get the error: error: namespace alias ‘boost’ not allowed here, assuming ‘boost_1_69_0’, and I can't figure out why.

Typhaon
  • 828
  • 8
  • 27

1 Answers1

0

There's many things (besides the boost_1_69_0 thing that we've discussed earlier .

  1. First things first, you define specializations in Foo.cpp:

    template <> void serialize(boost::archive::xml_oarchive& ar, unsigned int const file_version) {}
    template <> void serialize(boost::archive::xml_iarchive& ar, unsigned int const file_version) {}
    

    But no such template exists. There's only the member function template serialize. Besides, you can instantiate whatever you want, but unless it is extern-declared in the header the compiler will refuse to instantiate the template without a definition.

  2. The same thing (missing extern declaration) will happen with load/save construct data functions.

  3. You opted to put the load/save construct data inside the boost::serialization namespace. This is odd, since apparently there's a wish to hide implementation detail from the header. I'd define the functions at their least intrusive and least tightly-coupled spot: within the class namespace.

  4. Similar things are at play with BOOST_CLASS_EXPORT(Foo) - since this in a header, you will be defining stuff in multiple translation units.

    Avoid it by splitting declaration and definition (or, you know, opt to not do this rain-dance)

  5. Top-level const is (literally) meaningless in function signatures

  6. Why are you including the entire archives in the header file? I'd say having those includes is 100x more impact than including the definitions of the template functions that you're trying to hide...

  7. Same for iostream

  8. You're exporting a non-polymorphic class. That won't compile. You need a virtual interface.

  9. You're not serializing through a pointer; polymorphic types don't make sense, and neither do load/save construct data

Running with these, I'd arrive at the following "more ideal" header:

// Foo.hpp
#include <boost/serialization/export.hpp>

// forward declarations
namespace boost { namespace archive { class xml_iarchive; } }
namespace boost { namespace archive { class xml_oarchive; } }
namespace boost { namespace serialization { class access; } }

struct IFoo {
    virtual ~IFoo() = default;
};

class Foo : public IFoo {
    int m_bar;

    friend class boost::serialization::access;
    using iarchive = boost::archive::xml_iarchive;
    using oarchive = boost::archive::xml_oarchive;

    friend void serialize(iarchive&, Foo&, unsigned);
    friend void serialize(oarchive&, Foo&, unsigned);
    friend void load_construct_data(iarchive& ar, Foo*, unsigned);
    friend void save_construct_data(oarchive& ar, Foo const*, unsigned);

  public:
    Foo(int bar);
    int getBar() const;
};

BOOST_CLASS_EXPORT_KEY(Foo)

The corresponding implementation:

// Foo.cpp
#include "Foo.hpp"
#include <boost/archive/xml_iarchive.hpp>
#include <boost/archive/xml_oarchive.hpp>

Foo::Foo(int bar) : m_bar(bar){};

int Foo::getBar() const { return m_bar; }

void serialize(Foo::iarchive&, Foo&, unsigned) {}
void serialize(Foo::oarchive&, Foo&, unsigned) {}

void save_construct_data(Foo::oarchive& ar, Foo const* p, unsigned) {
    int bar = p->getBar();
    ar << BOOST_NVP(bar);
}

void load_construct_data(Foo::iarchive& ar, Foo* p, unsigned) {
    int bar;
    ar >> BOOST_NVP(bar);
    ::new (p) Foo(bar);
}

BOOST_CLASS_EXPORT_IMPLEMENT(Foo)

Now, fixing main a little:

#include "Foo.hpp"
#include <boost/archive/xml_oarchive.hpp>
#include <boost/serialization/unique_ptr.hpp>
#include <iostream>

int main() {
    auto foo = std::make_unique<Foo>(42);
    boost::archive::xml_oarchive oa(std::cout);
    oa << BOOST_NVP(foo);
}

Prints:

<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<!DOCTYPE boost_serialization>
<boost_serialization signature="serialization::archive" version="19">
<foo class_id="0" tracking_level="0" version="0">
    <tx class_id="1" tracking_level="1" version="0" object_id="_0">
        <bar>42</bar>
    </tx>
</foo>
</boost_serialization>

See it Live On Wandbox

Bonus

If you're actually hiding implementation, why not hide it fully? That's easier still, can be virtualized etc. It hurts my sensibilities that you're going to these lengths to "hide" details but still including the archive type inside the main program... and allowing the user to mismatch serialization/deserialization e.g. by using a different XML name, or different variable type altogether (which leads to undefined behaviour).

Let's make the ideal header:

#include <iosfwd>
#include <memory>

// Foo.hpp
struct IFoo {
    virtual ~IFoo() = default;

    void save(std::ostream&) const;
    static std::unique_ptr<IFoo> load(std::istream&);
};

class Foo : public IFoo {
    int m_bar;

  public:
    Foo(int bar);
    int getBar() const;
};

I hope you'll agree that this is cutting dependencies and reducing coupling. The rest was lip-service only.

Now everything truly serialization related can be in the cpp. In a real project you'd have a TU dedicated to serializing an entire object graph. (Simplest thing to do would be to put it in a IFoo.cpp).

Let's throw in base_object serialization (that was previously missing):

// Foo.cpp
#include "Foo.hpp"

Foo::Foo(int bar) : m_bar(bar){}

int Foo::getBar() const { return m_bar; }

#include <boost/serialization/base_object.hpp>
#include <boost/serialization/nvp.hpp>
using boost::make_nvp;
using boost::serialization::base_object;

template <typename Archive> void serialize(Archive&, IFoo&, unsigned) {}

template <typename Archive> void serialize(Archive& ar, Foo& f, unsigned) {
    ar & boost::make_nvp("IFoo", base_object<IFoo>(f));
}

template <typename Archive> void save_construct_data(Archive& ar, Foo const* p, unsigned) {
    int bar = p->getBar();
    ar << BOOST_NVP(bar);
}

template <typename Archive> void load_construct_data(Archive& ar, Foo* p, unsigned) {
    int bar;
    ar >> BOOST_NVP(bar);
    ::new (p) Foo(bar);
}

#include <boost/archive/xml_iarchive.hpp>
#include <boost/archive/xml_oarchive.hpp>
#include <boost/serialization/export.hpp>
BOOST_CLASS_EXPORT(IFoo)
BOOST_CLASS_EXPORT(Foo)

/*static*/ std::unique_ptr<IFoo> IFoo::load(std::istream& is) {
    boost::archive::xml_iarchive ia(is);
    IFoo* p = nullptr;
    ia >> boost::make_nvp("p", p);
    return std::unique_ptr<IFoo>(p);
}

void IFoo::save(std::ostream& os) const {
    boost::archive::xml_oarchive oa(os);
    IFoo const* p = this;
    oa << make_nvp("p", p);
}

Extending main to also show round-trip deserialization:

#include "Foo.hpp"
#include <iostream>
#include <sstream>

int main() {
    Foo(42).save(std::cout);

    std::stringstream ss;
    Foo(99).save(ss);
    auto roundtrip = Foo::load(ss);

    if (auto foo = dynamic_cast<Foo*>(roundtrip.get())) {
        std::cout << "roundtrip is a foo, getBar(): " << foo->getBar() << "\n";
    }
}

Prints

<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<!DOCTYPE boost_serialization>
<boost_serialization signature="serialization::archive" version="19">
<p class_id="1" class_name="Foo" tracking_level="1" version="0" object_id="_0">
        <bar>42</bar>
        <IFoo class_id="0" tracking_level="1" version="0" object_id="_1"></IFoo>
</p>
</boost_serialization>

roundtrip is a foo, getBar(): 99

See it Live On Wandbox again.

sehe
  • 374,641
  • 47
  • 450
  • 633