3

A contrived (minimal) example that I reduced.

Step #1 is to have a base type with an enumerated 'kind' field that must be populated by the derived constructors. Note that there is no reason to have a serializer for BodyPart, since the non-default constructor initializes it.

We do have to have a virtual destructor since we are going to create shared_ptr and pass it around, and we want to ensure that we don't perform a slicing ~BodyPart.

struct BodyPart
{
    enum class Kind {
        Head,
        Shoulder,
        Knee,
        Toe,
    };
    BodyPart(Kind _kind) : kind(_kind) {}
    Kind const kind;
    virtual ~BodyPart() = default;

    /* ---- Should never happen, but added to make Cereal happy that this can be in/out cerealized --- */
    template<class Archive>
    void serialize( Archive & ar )
    {
        (void)ar; assert(0);
    }

    template <class Archive>
    static void load_and_construct( Archive & ar, cereal::construct<BodyPart> & construct )
    {
        (void)ar; (void)construct; assert(0);
    }
    /* ---- end make cereal happy --- */

};

Step #2, add a derived class with some actual data

struct Head : BodyPart
{
    Head(int _eyeCount) : BodyPart(Kind::Head), eyeCount(_eyeCount) {}
    int const eyeCount;

    template<class Archive>
    void serialize( Archive & ar )
    {
        ar( eyeCount );
    }

    template <class Archive>
    static void load_and_construct( Archive & ar, cereal::construct<Head> & construct )
    {
      int x;
      ar( x );
      construct( x );
    }
};

Step #3, perform the registrations required

CEREAL_REGISTER_TYPE(Head)
CEREAL_REGISTER_POLYMORPHIC_RELATION(BodyPart, Head)

Step #4, cerealize:

std::shared_ptr<BodyPart> head = std::make_shared<Head>(3);
std::string data;

{
    std::ostringstream oss(std::ios::binary);
    cereal::PortableBinaryOutputArchive oarchive(oss);
    oarchive(head);
    data = oss.str();
}

decltype(head) head2;
{
    std::istringstream iss(data);
    cereal::PortableBinaryInputArchive iarchive(iss);
    iarchive(head2);
}

Having poked at it for a while, there doesn't seem to be any other way to "convince" Cereal that I don't ever want to directly serialize (cerealize?) BodyPart. I found two solutions that do work

  1. Add a dummy pure virtual function into the base, then define it in all the derived classes. Obviously this is dumb and not something I'm going to do in real code. Also not compatible with the non-intrusive method.

  2. Nicer version of 1, create an extra class to define the override so it doesn't contaminate the derived classes. Still very intrusive.

  3. Do what I did above, and add the Cereal functions for the base class with a runtime error (in real code, I'd throw a std::logic_error or something).

I guess what would be nice is:

  1. Cereal magically does the right thing and defers looking for base class serialization functions until I actually serialize it. This is impossible because translation unit for deserialization doesn't know anything about what was serialized (could be anywhere). This knowledge has to be injected into the output archive somehow.

[ Heh, sometimes writing it out convinces you of the answer, eh? ]

  1. An explicit way to force Cereal to treat a class as an abstract base class, even when it is not. Looking at polymorphic.hpp, there are few places where std::is_abstract is used. It could (?) be possible to add a cereal::is_abstract type trait the client can use to inject the knowledge that a class is never de/serialized directly.

Anyway, this is way TLDR, thx for reading this far. Please tell me if I'm crazy or if I've at least defined the 'problem space' in a sensible fashion.

user566408
  • 57
  • 5

0 Answers0