1

I need to serialize all the options on my CommunicationLayer, which is basically a wrapper around serial port, which I will use in an initialization file of sorts.

This class doesn't have a default constructor, but implements a pure virtual class. When I first ran this I got the exception that I didn't register/export the derived class. So I did that. But when I added the export I got the exception that it couldn't find a save/load function:

no matching function for call to ‘save(boost_1_69_0::archive::xml_oarchive&, const amp::communication::RS485CommunicationLayer&, const boost_1_69_0::serialization::version_type&)’
         save(ar, t, v);

This is true! But I can't use the save/load and use save_construct_data and load_construct_data, because the absence of a default constructor. How do I tell boost/serde to use these instead?? Is it the SPLIT_FREE macro?

I'm trying to work myself through the documentation, but it's really hard to find everything.

Following are the base and derived classes, I tried to shorten the derived class as much as possible.

//BASE
class ICommunicationLayer {
    friend class boost::serialization::access;
    template<class Archive>
    inline void serialize(Archive & ar, const unsigned int file_version) {};
public:
    virtual ~ICommunicationLayer();
    virtual std::size_t write(const char* const buffer, std::size_t buffSize) = 0;
    virtual std::size_t readUntil(std::vector<char>& buffer, char delim, std::chrono::microseconds timeout) = 0;
};
// DERIVED
#include "ICommunicationLayer.h"

#include <boost/asio.hpp>

#include <boost/serialization/access.hpp>
#include <boost/serialization/split_free.hpp>
#include <boost/serialization/export.hpp>
#include <boost/serialization/base_object.hpp>
#include <boost/serialization/nvp.hpp>
namespace amp {
namespace communication {

class RS485CommunicationLayer final : public ICommunicationLayer {
public:
    RS485CommunicationLayer(
            const std::string& path,
            unsigned int baud_rate,
            // other options
    );

    ~RS485CommunicationLayer();

    std::size_t write(const char* const buffer, const size_t size) override;
    std::size_t readUntil(std::vector<char>& buffer, char delim,std::chrono::microseconds timeout) override;
    std::string getPath() const;
private:
    friend class boost::serialization::access;
    std::string path;
  // rest of impl
};

} /* namespace communication */
} /* namespace amp */

BOOST_SERIALIZATION_SPLIT_FREE(amp::communication::RS485CommunicationLayer)
BOOST_CLASS_EXPORT(amp::communication::RS485CommunicationLayer)

namespace boost_1_69_0 {
namespace serialization {

template<class Archive>
inline void save_construct_data(Archive& ar, const amp::communication::RS485CommunicationLayer* comLayer, const unsigned int version) {
    ar << boost::serialization::make_nvp(
        BOOST_PP_STRINGIZE(amp::communication::ICommunicationLayer), 
        boost::serialization::base_object<amp::communication::ICommunicationLayer>(*comLayer)
    );

    ar << boost::serialization::make_nvp("path", const_cast<std::string>(comLayer->getPath());

    // other options
}

template<class Archive>
inline void load_construct_data(Archive& ar, amp::communication::RS485CommunicationLayer* comLayer, const unsigned int version) {
    ar >> boost::serialization::base_object<amp::communication::ICommunicationLayer>(*comLayer);

    std::string path;
    ar >> path;

    // other options

    new(comLayer) amp::communication::RS485CommunicationLayer(path, baudrate, /* other options */);
}
}

}
Typhaon
  • 828
  • 8
  • 27

1 Answers1

1

The construct data is an addition to regular serialization. You still need to serialize. As a matter of fact, serializing base_object needs to be in the constructed-object serialize implementation.

Also keep in mind that you need to EXPORT after including the relevant archive type(s).

You have a number of spots with redundant top-level const (even a const-cast). Top-level const on return type/arguments isn't part of the function signature.

Finally, I don't know what namespace boost_1_69_0 is, but that should not work. Use namespace boost. If you're playing with macros to define it like that, I'd suggest to stop doing that. Instead consider putting the overloads in the ADL-associated namespace for your types!

Here's the whole thing made self-contained and working:

Live On Coliru

#include <boost/serialization/access.hpp>
#include <boost/serialization/assume_abstract.hpp>
#include <boost/serialization/export.hpp>
#include <boost/serialization/serialization.hpp>

#include <boost/serialization/base_object.hpp>
#include <boost/serialization/vector.hpp>
#include <boost/serialization/shared_ptr.hpp>

#include <chrono>

// BASE
namespace amp::communication {
    using duration = std::chrono::steady_clock::duration;

    class ICommunicationLayer {
        friend class boost::serialization::access;
        template <class Archive> inline void serialize(Archive&, unsigned){}

      public:
        virtual ~ICommunicationLayer();
        virtual size_t write(char const* const buffer, size_t buffSize)                   = 0;
        virtual size_t readUntil(std::vector<char>& buffer, char delim, duration timeout) = 0;
    };
} // namespace amp::communication

// DERIVED
//#include "ICommunicationLayer.h"

#include <boost/asio.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/split_free.hpp>

namespace amp { namespace communication {

    class RS485CommunicationLayer final : public ICommunicationLayer {
      public:
        RS485CommunicationLayer(std::string const& path, unsigned baud_rate)
            : path(path)
            , baud_rate(baud_rate) {}

        ~RS485CommunicationLayer();

        size_t      write(char const* const buffer, const size_t size) override;
        size_t      readUntil(std::vector<char>& buffer, char delim, duration timeout) override;
        std::string getPath() const { return path; }
        unsigned    getBaudrate() const { return baud_rate; }

      private:
        friend class boost::serialization::access;
        template <typename Ar> void serialize(Ar& ar, unsigned) {
            ar& boost::make_nvp("ICommunicationLayer",
                                boost::serialization::base_object<ICommunicationLayer>(*this));
        }

        std::string path;
        unsigned    baud_rate;
        // rest of impl
    };

    // ADL resolved overloads
    template <class Archive>
    inline void save_construct_data(Archive& ar, RS485CommunicationLayer const* p, unsigned) {
        auto path = p->getPath();
        unsigned baud_rate = p->getBaudrate();
        ar& BOOST_NVP(path) & BOOST_NVP(baud_rate);
    }

    template <class Archive>
    inline void load_construct_data(Archive& ar, RS485CommunicationLayer* p, unsigned) {
        std::string path;
        unsigned baud_rate;
        ar >> BOOST_NVP(path) >> BOOST_NVP(baud_rate);
        new (p) RS485CommunicationLayer(path, baud_rate);
    }
}} // namespace amp::communication

BOOST_SERIALIZATION_ASSUME_ABSTRACT(amp::communication::ICommunicationLayer)

#include <boost/archive/xml_iarchive.hpp>
#include <boost/archive/xml_oarchive.hpp>
#include <iostream>
#include <sstream>
BOOST_CLASS_EXPORT(amp::communication::ICommunicationLayer)
BOOST_CLASS_EXPORT(amp::communication::RS485CommunicationLayer)

int main() {
    using namespace amp::communication;
    using Ptr = std::shared_ptr<ICommunicationLayer>;

    std::vector<Ptr> ptrs{nullptr, std::make_shared<RS485CommunicationLayer>("hello", 115'200)}, roundtrip;
    ptrs.push_back(ptrs.back()); // one duplicate for testing purposes

    std::stringstream xml;
    {
        boost::archive::xml_oarchive oa(xml);
        oa << boost::make_nvp("root", ptrs);
    }

    std::cout << xml.str();
    {
        boost::archive::xml_iarchive ia(xml);
        ia >> boost::make_nvp("root", roundtrip);
    }

    bool verify = roundtrip.size() == ptrs.size();
    verify &= (roundtrip.at(0) == nullptr);
    verify &= (roundtrip.at(1) == roundtrip.at(2));

    std::cout << "Roundtrip verify " << (verify?"OK":"FAILED") << std::endl;
}

// to satisfy linker
namespace amp { namespace communication {
    ICommunicationLayer::~ICommunicationLayer()         = default;
    RS485CommunicationLayer::~RS485CommunicationLayer() = default;
    size_t RS485CommunicationLayer::write(char const*, size_t) { return 0; }
    size_t RS485CommunicationLayer::readUntil(std::vector<char>&, char, duration) { return 0; }
}} // namespace amp::communication

Prints

<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<!DOCTYPE boost_serialization>
<boost_serialization signature="serialization::archive" version="19">
<root class_id="0" tracking_level="0" version="0">
    <count>3</count>
    <item_version>1</item_version>
    <item class_id="1" tracking_level="0" version="1">
        <px class_id="-1"></px>
    </item>
    <item>
        <px class_id="2" class_name="amp::communication::RS485CommunicationLayer" tracking_level="1" version="0" object_id="_0">
            <path>hello</path>
            <baud_rate>115200</baud_rate>
            <ICommunicationLayer class_id="3" tracking_level="0" version="0"></ICommunicationLayer>
        </px>
    </item>
    <item>
        <px class_id_reference="2" object_id_reference="_0"></px>
    </item>
</root>
</boost_serialization>

Roundtrip verify OK

Afterthought

Perhaps the best location for load/save construct data is as hidden friends. That way you don't even need public accessors for every bit of construct-data:

Live On Coliru

class RS485CommunicationLayer final : public ICommunicationLayer {
  public:
    RS485CommunicationLayer(std::string const& path, unsigned baud_rate)
        : path(path)
        , baud_rate(baud_rate) {}

    ~RS485CommunicationLayer() override;

    size_t write(char const* const buffer, const size_t size) override;
    size_t readUntil(std::vector<char>& buffer, char delim, duration timeout) override;

  private:
    std::string path;
    unsigned    baud_rate;
    // rest of impl

    template <class Archive>
    friend void save_construct_data(Archive& ar, RS485CommunicationLayer const* p, unsigned) {
        auto     path      = p->path;
        unsigned baud_rate = p->baud_rate;
        ar&      BOOST_NVP(path) & BOOST_NVP(baud_rate);
    }

    template <class Archive>
    friend void load_construct_data(Archive& ar, RS485CommunicationLayer* p, unsigned) {
        std::string path;
        unsigned    baud_rate;
        ar >> BOOST_NVP(path) >> BOOST_NVP(baud_rate);
        new (p) RS485CommunicationLayer(path, baud_rate);
    }

    friend class boost::serialization::access;
    template <typename Ar> void serialize(Ar& ar, unsigned) {
        ar& boost::make_nvp("ICommunicationLayer",
                            boost::serialization::base_object<ICommunicationLayer>(*this));
    }
};
sehe
  • 374,641
  • 47
  • 450
  • 633
  • Hey thanks a lot for taking all this time! This is great! I have a question about the `friend void load_construct_data` though. I've never seen friend functions, but when I read boost's docs I see that that function needs to be specified in their namespace. Wouldn't that be a problem? Also, I'm not allowed to put any (template) function definitions in the header files and I tried doing the declaration in .h and the definition in .cpp. But they don't seem to link up and I got an (almost) empty xml – Typhaon Jan 12 '23 at 15:03
  • Oh, btw the reason for boost_1_69_0 is because otherwise I get: `error: namespace alias ‘boost’ not allowed here, assuming ‘boost_1_69_0’`. I didn't change any macro's but my company might've. – Typhaon Jan 12 '23 at 15:04
  • 1
    Okay. Assuming they know what they're doing then. Probably to allow side-by-side packaging of versions. It's fine as long as all your code (including any shared boost libraries) compiles with the same assumptions. It's not something I've encountered before – sehe Jan 12 '23 at 16:50
  • 1
    (Mmm. If this were my responsibility, I'd say you need to be including headers/build config that already contains `#define boost boost_1_69_0`. Obviously, it is not, just worth checking that you didn't miss some config/include) – sehe Jan 12 '23 at 16:52
  • 1
    I missed the first comment: the friend *is* in the surrounding namespace, except it can **only** be found via ADL, which is exactly what you want. See [GotW "What's in a class - The Interface Principle](http://www.gotw.ca/publications/mill02.htm) and also related [How Non-Member Functions Improve Encapsulation](https://embeddedartistry.com/fieldatlas/how-non-member-functions-improve-encapsulation/). – sehe Jan 12 '23 at 16:55
  • 1
    Re.: "I'm not allowed to put any (template) function definitions in the header files" there must be some confusion. Template functions can practically [*only* be defined in header files](https://stackoverflow.com/questions/495021/why-can-templates-only-be-implemented-in-the-header-file). The only exception is when you extern-declare explicit instantiations, which you might do if you use only a limited set of archive types. – sehe Jan 12 '23 at 16:57
  • 1
    Apropos of nothing: see also https://stackoverflow.com/questions/478668/boost-serialization-using-polymorphic-archives – sehe Jan 12 '23 at 16:58
  • Hey thanks for all the comments! These really help! Regarding the boost namespace part: I am including boost/access.hpp which almost starts with the line `namespace boost_1_69_0 {} namespace boost = boost_1_69_0;` But it doesn't seem to stick well with the compiler. – Typhaon Jan 12 '23 at 17:00
  • Regarding the header/source seperation. I've been trying to change the mind of my supervisor but he won't budge. I'm an intern, so I don't have a lot of say on the matter. I needed to work around this situation for the last 3 months. But if you say that this is the only way, I might try again. – Typhaon Jan 12 '23 at 17:04
  • Regarding the friend function. With surrounding namespace you mean that it's in `::amp` or `::` (root). Doesn't that mean the line should be `friend void boost::serialization::load_construct_data(Archive& ar, RS485CommunicationLayer* p, unsigned)`? (with boost::serialization:: added in front of the function). Just checking. – Typhaon Jan 12 '23 at 17:09
  • I didn't say it's the only way, it's just that templates come with these constraints (so, look at explicit instantiations) – sehe Jan 12 '23 at 18:15
  • I cannot find any trace of `boost_1_69_0` in *any version* of *any boost library* (outside release notes/CI scripts). This is not a boost thing (note `boost/access.hpp` doesn't exist either, but you probably meant `boost/serialization/access.hpp`) – sehe Jan 12 '23 at 21:25