Search code examples
c++boostautomationboost-asioboost-serialization

Is there a way to auto-generate or at least shorten (some parameter like __all__) the serialize function?


I am dealing with a network where messages(request as well as responses) are transferred as structs. For achieving this, I turned to boost-serialization, which works great! but with so many types of messages and responses, it's hard to include the serialize function in all of them, is there a shortcut to auto generate the method for all structs or at least exposing every member variable at once?

Example:

#pragma once

#include <boost/archive/binary_oarchive.hpp>
#include <boost/serialization/serialization.hpp>
struct Test
{
    public:
        int a;
        int b;
        template<typename archive> void serialize(archive& ar, const unsigned version) {
        ar & a; 
        ar & b;
    }
};

Solution

  • I believe I answered this yesterday:

    struct Test {
        int a,b;
        template<typename Ar> void serialize(Ar& ar, unsigned) { ar & a & b; }
    };
    

    Note that if you make serialize a free function as well (ADL lookup is used):

    struct Test {
        int a,b;
    };
    
    template<typename Ar> void serialize(Ar& ar, Test& o, unsigned) {
        ar & o.a & o.b;
    }
    

    So you can have the serialization code separately. Finally, if you have a predefined set of archives the serialize function doesn't need to be a template at all:

    using OArchive = boost::archive::binary_oarchive;
    using IArchive = boost::archive::binary_iarchive;
    
    struct Test {
        int a,b;
        void serialize(OArchive& ar, ...) const { ar & a & b; }
        void serialize(IArchive& ar, ...)       { ar & a & b; }
    };
    

    Of course this creates some duplication. I got cute to ignore the version argument with variadics, but on the flip size it's more const-correct.

    Other thoughts

    If your structs are binary serializable, mark them as such:

    • Boost serialization bitwise serializability
    • Or explicitly treat them as blobs: make_binary_object - in this case you don't need any serialization methods:

      Live On Coliru

      #include <boost/archive/text_oarchive.hpp>
      #include <boost/serialization/binary_object.hpp>
      #include <iostream>
      using boost::serialization::make_binary_object;
      
      struct Test { int a,b; };
      
      int main() {
          boost::archive::text_oarchive oa(std::cout, boost::archive::no_header);
      
          Test req {13,31};
          oa << make_binary_object(&req, sizeof(req));
      }
      

      Prints

      DQAAAB8AAAA=
      

    META-PROGRAMMING TO THE RESCUE

    Fair warning, as a C programmer you probably want to bail out and just use more preprocessor magic/code generators here

    Say you have more messages (which can nest):

    namespace Messages {
        struct FooMsg { int a, b; };
        struct BarMsg { std::string c; double d; };
        struct QuxMsg { FooMsg e; BarMsg f; };
    }
    

    You can adapt these as Fusion Sequences:

    BOOST_FUSION_ADAPT_STRUCT(Messages::FooMsg, a, b)
    BOOST_FUSION_ADAPT_STRUCT(Messages::BarMsg, c, d)
    BOOST_FUSION_ADAPT_STRUCT(Messages::QuxMsg, e, f)
    

    The good thing is now you can write generic code across these sequences, so let's introduce our very own serialization wrapper:

    namespace Messages {
        template <typename T>
        struct MsgSerializationWrapper {
            T& ref;
        };
    
        template <typename T>
        static inline MsgSerializationWrapper<T> wrap(T& msg) { return {msg}; }
    

    Now you can implement serialization for any wrapped message:

        template <typename Ar, typename Msg>
        void serialize(Ar& ar, MsgSerializationWrapper<Msg> wrapped, unsigned) {
            boost::fusion::for_each(wrapped.ref, [&ar](auto& field) { ar & wrap(field); });
        }
    

    Of course, we need some sfinae to detect when the wrapped type is not a fusion sequence and just serialize that the normal way.

    Full Demo

    Live On Coliru

    #include <boost/archive/text_oarchive.hpp>
    #include <boost/fusion/adapted.hpp>
    #include <boost/fusion/include/for_each.hpp>
    #include <iostream>
    
    namespace Messages {
        struct FooMsg { int a, b; };
        struct BarMsg { std::string c; double d; };
        struct QuxMsg { FooMsg e; BarMsg f; };
    }
    
    BOOST_FUSION_ADAPT_STRUCT(Messages::FooMsg, a, b)
    BOOST_FUSION_ADAPT_STRUCT(Messages::BarMsg, c, d)
    BOOST_FUSION_ADAPT_STRUCT(Messages::QuxMsg, e, f)
    
    namespace Messages {
        template <typename T>
        struct MsgSerializationWrapper {
            T& ref;
        };
    
        template <typename T>
        static inline MsgSerializationWrapper<T> wrap(T& msg) { return {msg}; }
    
        template <typename Ar, typename Msg>
        std::enable_if_t<boost::fusion::traits::is_sequence<Msg>::value>
        serialize(Ar& ar, MsgSerializationWrapper<Msg> wrapped, unsigned) {
            boost::fusion::for_each(wrapped.ref, [&ar](auto& field) { ar & wrap(field); });
        }
    
        template <typename Ar, typename Primitive>
        std::enable_if_t<not boost::fusion::traits::is_sequence<Primitive>::value>
        serialize(Ar& ar, MsgSerializationWrapper<Primitive> wrapped, unsigned) {
            ar & wrapped.ref;
        }
    }
    
    int main() {
        boost::archive::text_oarchive oa(std::cout);
    
        Messages::QuxMsg req { 
            Messages::FooMsg { 42, 99 },
            Messages::BarMsg { "hello world\n", 3.14e100 },
        };
    
        oa << wrap(req);
    }
    

    Prints

    22 serialization::archive 17 0 0 0 0 0 0 42 99 0 0 0 0 12 hello world
     0 0 3.13999999999999984e+100