7

Let's say I have a class in C++, like the following:

struct Point {
    int x, y, z;
};

I want to use Cereal to serialize that struct to JSON. So I added a serialize function like this:

struct Point {
    int x, y, z;
    template<class Archive>
    void serialize(Archive& ar) {
        ar(CEREAL_NVP(x),
           CEREAL_NVP(y),
           CEREAL_NVP(z));
    }
};

This works fine when the Point is a member of another object or an element of an array. But if I want to make the Point be the primary object of an entire JSON file, it doesn't work properly. For example, with the following code:

Point p { 1, 2, 3 };
cereal::JSONOutputArchive ar(std::cout);
ar(p);

I get the following output:

{
    "value0": {
        "x": 1,
        "y": 2,
        "z": 3
    }
}

I'd like to remove the "value0" key and elevate the object to occupy the entire file, like this:

{
    "x": 1,
    "y": 2,
    "z": 3
}

The only way I can seem to do that, is to basically re-implement the serialization function, manually adding the key names.

Point p {1, 2, 3};
cereal::JSONOutputArchive ar(std::cout);
ar(cereal::make_nvp("x", p.x),
   cereal::make_nvp("y", p.y),
   cereal::make_nvp("z", p.z));

Is there any way to do it utilizing the serialize function that I already implemented for the class?

Benjamin Lindley
  • 101,917
  • 9
  • 204
  • 274

3 Answers3

10

Okay, figured it out. Pretty simple, just needed to call the serialize function directly from the object, passing the archive, instead of passing the object to the archive.

Point p {1, 2, 3};
cereal::JSONOutputArchive ar(std::cout);
p.serialize(ar);
Benjamin Lindley
  • 101,917
  • 9
  • 204
  • 274
  • 1
    Unfortunately, this solution doesn't work for serializing const objects, because the serialize method can't be const. – Michal Fapso Jul 06 '20 at 09:06
  • 2
    The fact that I had to find this answer just speaks about how terrible cereal documentation and samples are. – cen Sep 22 '20 at 20:07
2

Benjamin's answer is perfect solution if you know upfront that the class to be serialized has a serialize() method. Since Cereal supports in-class/out-of-class serialize(), split load()/save(), explicit versioning; that's not always the case. Cereal's internal cereal::InputArchive and cereal::OutputArchive classes both have a bunch of SFINAE template methods to detect the right serialization method to use during compile time. The type-traits there can be used to roll our own template switch:

template< typename Class, typename Archive,
          typename std::enable_if< cereal::traits::has_member_serialize<Class, Archive>::value>::type* = nullptr>
inline static void serializeHelper(Class& cl, Archive& ar)
{
    cl.serialize(ar);
}

template< typename Class, typename Archive,
          typename std::enable_if< cereal::traits::has_member_save<Class, Archive>::value>::type* = nullptr>
inline static void serializeHelper(Class& cl, Archive& ar)
{
    cl.save(ar);
}

// More version could follow for remaining serialization types (external, versioned...)

template< typename Class, typename Archive,
          typename std::enable_if< cereal::traits::has_member_serialize<Class, Archive>::value>::type* = nullptr>
inline static void deserializeHelper(Class& cl, Archive& ar)
{
    cl.serialize(ar);
}

template< typename Class, typename Archive,
          typename std::enable_if< cereal::traits::has_member_load<Class, Archive>::value>::type* = nullptr>
inline static void deserializeHelper(Class& cl, Archive& ar)
{
    cl.load(ar);
}

// More version could follow for remaining deserialization types (external, versioned...)

Calling serializeHelper(p, ar); will automatically select the serialization method provided by Point, the same way as Cereal does internally.

Gyorgy Szekely
  • 2,654
  • 3
  • 23
  • 32
2

You need to define the epilogue and prologue functions for your own type to be no-ops, so that the ones from the chosen archive won't be selected instead.

#include <cereal/archives/json.hpp>
#include <iostream>

struct Point {
    int x, y, z;
    template<class Archive>
    void serialize(Archive& ar) {
        ar(CEREAL_NVP(x),
           CEREAL_NVP(y),
           CEREAL_NVP(z));
    }
};

void epilogue(cereal::JSONOutputArchive&, const Point &){}
void prologue(cereal::JSONOutputArchive&, const Point &){}

int main()
{
    Point p { 1, 2, 3 };
    cereal::JSONOutputArchive ar(std::cout);
    ar(p); 

    return 0;
}
Fabio A.
  • 2,517
  • 26
  • 35
  • Perfect! This is the cleanest solution for me. – Michal Fapso Jul 06 '20 at 09:00
  • I thought that templates could be used to accomplish this for all structs, but then the compiler complains about the ambiguous epilogue() / prologue(). – Michal Fapso Jul 06 '20 at 12:15
  • Your solution works for serializing an object to JSON string, but when I want to deserialize it back from string to an object, it complains with `rapidjson internal assertion failure: IsObject()`. When I put back the enclosing `value0` node into the JSON string, deserialization works again. So is this solution incompatible with deserialization? – Michal Fapso Jul 06 '20 at 13:20
  • 1
    @MichalFapso, if you want to de-serialize as well, the epilogue and prologue functions should be adapted for the JSONInputArchive too. – Fabio A. Jul 07 '20 at 07:39
  • 1
    Ooooh, of course, thanks for pointing that out, @Fabio A. :) – Michal Fapso Jul 07 '20 at 07:43