1

I am using the cereal library. The idea is to have a base class that serializes itself, and the derived class would serialize itself, and the base class. This is an example almost verbatim from the library documentation, but it doesn't work the same way the documentation claims - only the base serialize is ever called.

#include <cereal/archives/json.hpp>
#include <cereal/types/polymorphic.hpp>
#include <fstream>

struct EmptyBase
{
    public:
      virtual void foo() = 0;
      std::string name;
      template <class Archive>
      void serialize( Archive & ar )
      {
        std::cout << "Base serialize" << std::endl;
        ar(name);
      }
  
    void SaveToFile(const std::string& filename) const 
    {
        std::ofstream os(filename);
        cereal::JSONOutputArchive ar(os);
        ar(cereal::make_nvp("settings", *this));
    }

    void LoadFromFile(const std::string& filename) 
    {
        std::ifstream is(filename);
        cereal::JSONInputArchive ar(is);
        ar(cereal::make_nvp("settings", *this));
    }

    

};

struct DerivedTwo: EmptyBase
{
  public:

  void foo() {}

  std::string why;

  template <class Archive>
  void serialize( Archive & ar )
  {
    std::cout << "Child serialize" << std::endl;
    ar(cereal::base_class<EmptyBase>(this));
    ar( why );
  }
};

CEREAL_REGISTER_TYPE(DerivedTwo)
CEREAL_REGISTER_POLYMORPHIC_RELATION(EmptyBase, DerivedTwo)


int main()
{
    DerivedTwo myclass;
    myclass.name = "test";
    myclass.why = "because";
    myclass.SaveToFile("settings.json");
}

Expected output:

Child serialize
Base serialize

Actual output:

Base serialize

I found some similar problems, but their code seems to work this way. What am I missing?

Edit: I want to have a base class with Save() Load() and serialize(). Save()/Load() will de-/serialize the class into a JSON file.

I want to inherit from this class, and use it like this:

DerivedClassInstance.name = "test"; //this property is in the base class
DerivedClassInstance.y = "why"; //this is in the derived class

DerivedClassInstance.Save(); //This will save both y and name into a file in json format

DerivedClassInstance.Load(); //This will load the JSON from the file and populate name and y. 
//Both Save() and Load() are in the base class

I hope that makes sense.

OOgaBooga
  • 23
  • 4
  • You need a virtual function in there somewhere, overriden in every derived class. `serialize` cannot be virtual since it is a template, so you probably want to make `SaveToFile` and `LoadFromFile` virtual. – n. m. could be an AI Mar 07 '23 at 07:40
  • To what end? I specifically need the SaveToFile and LoadFromFile functions in the base class, because they are going to be the same for all derived classes. Reimplementing overriding them in every derived class defeats the purpose. – OOgaBooga Mar 08 '23 at 02:03
  • I apologise, this is indeed not what cereal says you are supposed to do. All necessary works is supposed to be done in the registering macros. (My original idea was that these functions are the same *textually* but they will not have the same *meaning*, because in each derived class `this` has a different type. This is a frequent situation, see e.g. how polymorphic `clone` functions are implemented.) – n. m. could be an AI Mar 08 '23 at 05:18

2 Answers2

1

You have to make your SaveToFile and LoadFromFile functions pure virtual and implement them in derived classes:

#include <cereal/archives/json.hpp>
#include <cereal/types/polymorphic.hpp>
#include <fstream>
#include <memory>

struct EmptyBase
{
    virtual void foo() = 0;
    std::string name;

    template <class Archive>
    void serialize( Archive & ar )
    {
        std::cout << "Base serialize" << std::endl;
        ar(CEREAL_NVP(name));
    }
  
    virtual void SaveToFile(const std::string& filename) const = 0;

    virtual void LoadFromFile(const std::string& filename) = 0;
};

struct DerivedTwo : EmptyBase
{
  void foo() override {}

  std::string why;

  template <class Archive>
  void serialize( Archive & ar )
  {
    std::cout << "Child serialize" << std::endl;
    ar(cereal::base_class<EmptyBase>(this));
    ar( why );
  }

    void SaveToFile(const std::string& filename) const override
    {
        std::ofstream os(filename);
        cereal::JSONOutputArchive ar(os);
        ar(cereal::make_nvp("settings", *this));
        std::cout << (this) << '\n';
    }


    void LoadFromFile(const std::string& filename) override
    {
        std::ifstream is(filename);
        cereal::JSONInputArchive ar(is);
        ar(cereal::make_nvp("settings", *this));
    }
};

// You don't need this - works without this
//CEREAL_REGISTER_TYPE(DerivedTwo)
//CEREAL_REGISTER_POLYMORPHIC_RELATION(EmptyBase, DerivedTwo)

int main()
{
    DerivedTwo myclass;
    myclass.name = "test";
    myclass.why = "because";
    std::cout << (&myclass) << '\n';
    myclass.SaveToFile("settings.json");
    std::shared_ptr<EmptyBase> ptr = std::make_shared<DerivedTwo>();
    ptr->LoadFromFile("settings.json");
    std::cout << dynamic_cast<DerivedTwo*>(ptr.get())->why;
}

Godbolt link: https://godbolt.org/z/xfK148P81

https://uscilab.github.io/cereal/polymorphism.html - Refer to example in Registering from a header file section

kiner_shah
  • 3,939
  • 7
  • 23
  • 37
  • The examples in the polymorphism page of the cereal documentation do not actually compile. The error messages produced are not very friendly to someone who is just learning the library. – OOgaBooga Mar 07 '23 at 00:35
  • OK so is it actually impossible for me to do what I want? I want a base class, that has the Save and Load functions, and the derived class(es) will have serialize(). The reason for this, is I don't want the user of my class to be able to choose a different archiver, or be at all familiar with the cereal library beyond implementing the serialize() function. I have considered making them external template functions, but I prefer the whole thing to be contained in the class. – OOgaBooga Mar 07 '23 at 00:39
  • Please mention your exact use case in the question itself. What is to be exposed to the user and what do you want user to do? – kiner_shah Mar 07 '23 at 06:05
  • OK, I edited the question to explain what I want to happen. – OOgaBooga Mar 07 '23 at 07:27
  • @OOgaBooga They compile just fine for me. – n. m. could be an AI Mar 07 '23 at 11:45
1

From the documentation:

cereal supports serializing smart pointers to polymorphic base classes and will automatically deduce the derived types at runtime.

There is no word on references or raw pointers, and this is indeed not supported. (Cereal does not support serialising raw pointers at all).

You can work around this by creating a temporary unique_ptr inside your load and save functions, and releasing it after (de)serialising is done, but you should ask yourself whether you really should. If you don't need pointers at all, write SaveToFile and LoadFromFile as standalone (non-member) function templates, templated by the type of the object being loaded/saved.

n. m. could be an AI
  • 112,515
  • 14
  • 128
  • 243
  • Oh, it now clicked what they mean by pointers - I thought they don't support serializing pointers that are properties of the class. I didn't have any, so I was confused why this was an issue. I now understand that the class to be serialized cannot be a regular pointer. – OOgaBooga Mar 09 '23 at 01:46
  • If I make them standalone templates, wouldn't it still have a problem with the class being passed as a pointer to the function? – OOgaBooga Mar 09 '23 at 07:46
  • @OOgaBooga You can dereference it like you dereference this, or just pass a reference. If it is of the most derived type you won't have a problem (and will not need to register the inheritance relationship with the library). – n. m. could be an AI Mar 09 '23 at 08:35