Non-intruisive Boost serialization of labelled enums C++

1k views Asked by At

I would like to serialize struct A where I could save the tag name of the enum expressions instead of its integer in a non intruisive way (without having to change struct A).

enum e_fruit {
   apple,
   banana,
   coconut
};

struct A {
   e_fruit fruit;
   int     num;
};

namespace boost { namespace serialization {

template<class Archive>
void serialize(Archive & ar, A &a, const unsigned int version)
{
  ar & boost::serialization::make_nvp("FruitType", a.fruit); // this will store an integer
  ar & boost::serialization::make_nvp("Number", a.num);
}

}}

I have tried introducing a lookup table locally to the serialize function :

template<class Archive>
void serialize(Archive & ar, A &a, const unsigned int version)
{
  static const char* const labels[] = { "Apple", "Banana", "Coconut" };
  ar & boost::serialization::make_nvp("FruitType", labels[a.fruit]); // this won't work
   ar & boost::serialization::make_nvp("Number", a.num);
}

Unfortunately I get the error :

Error 78 error C2228: left of '.serialize' must have class/struct/union

since the prototype of make_nvp is

nvp< T > make_nvp(const char * name, T & t){
    return nvp< T >(name, t);
}

so T should be a deduced template argument. I then thought about making a structure which would contains these labels but then I would have to add this in struct A which is what I want to avoid...

So how can we achieve this in the most non-intruisive way ?

2

There are 2 answers

4
marom On BEST ANSWER

I think you need to separate loading from saving. This compiles, I wonder it it is worth the game...

struct fruit_serializer
{
    e_fruit &a_;
    fruit_serializer(e_fruit &a) : a_(a) {}
    template<class Archive>
    void save(Archive & ar, const unsigned int version) const
    {
        std::string label = labels[static_cast<int>(a_)];
        ar & boost::serialization::make_nvp("label", label);
    }
    template<class Archive>
    void load(Archive & ar, const unsigned int version)
    {
        std::string label ;
        ar & boost::serialization::make_nvp("label", label);
        a_ = static_cast<e_fruit>(std::find(labels.begin(), labels.end(), label) - labels.begin());
    }
    BOOST_SERIALIZATION_SPLIT_MEMBER();
    static std::vector<std::string> labels ;
};

std::vector<std::string> fruit_serializer::labels({ "Apple", "Banana", "Coconut" });

template<class Archive>
void serialize(Archive & ar, A &a, const unsigned int version)
{
    fruit_serializer a1(a.fruit);
    ar & boost::serialization::make_nvp("FruitType", a1);
}
0
InfinityFrog On

As much as I hate to resurrect an old question, I wanted to do the same thing, but needed to make the enums string-serializable without any decorators. Since I didn't find much else regarding the topic, I'm posting my hacky solution as an option for anyone else who needs to serialize their enums as strings.

First off, some sample types to (eventually) serialize:

#include <iostream>
#include <sstream>
#include <boost/serialization/nvp.hpp>
#include <boost/archive/xml_oarchive.hpp>


// A few dummy enum types to test the solution
enum MyEnum00 {
    Barb, Sarah, Lucy,
};

enum MyEnum01 {
    Corey, Trevor, Jacob = 99,
};
const char* const to_cstring(const MyEnum01 e) {
    switch (e) {
    case Corey:  return "Corey";
    case Trevor: return "Trevor";
    case Jacob:  return "Jacob";
    default:     return "UNKNOWN";
    }
}
inline std::ostream& operator<<(std::ostream& o, const MyEnum01 e) { return o << to_cstring(e); }


enum class MyEnumClass00 {
    Ricky, Julian, Bubbles
};

enum class MyEnumClass01 {
    Jim, Randy, Cyrus
};
const char* const to_cstring(const MyEnumClass01 e) {
    switch (e) {
    case MyEnumClass01::Jim:    return "I let the liquor do the thinking, bud!";
    case MyEnumClass01::Randy:  return "Got any cheeeeeseburgers?";
    case MyEnumClass01::Cyrus:  return "I got work to do";
    default:                    return "UNKNOWN";
    }
}
inline std::ostream& operator<<(std::ostream& o, const MyEnumClass01 e) { return o << to_cstring(e); }

From boost_1_63_0/boost/archive/detail/oserializer.hpp, the function save_enum_type::invoke() is where enums get clobbered into ints.

Boost uses a somewhat clunky combination of templated structs with separately templated members, so it's difficult to make our change apply any time our desired enum types are used. As a workaround, we can specialize boost::archive::detail::save_enum_type for the archive type we're using. Then, we can overload its invoke() function to keep it from clobbering the enum types we need to have archived as strings.

save_enum_type::invoke is called roughly in the middle of boost's callstack, which eventually makes its way down into the basic_text_oprimitive class. There, an insertion operator is finally used to save the value to the ostream underlying the destination archive. We can take advantage of that implementation detail to get our enum types archived as strings by specializing the save_enum_type and impelmenting insertion operators for the target types.

namespace boost {
    namespace archive {
        namespace detail {

            using xml_oarchive_ = boost::archive::xml_oarchive;
            using save_non_pointer_type_ = detail::save_non_pointer_type<xml_oarchive_>;

            template<>
            struct save_enum_type<xml_oarchive_>
            {
                // This is boost's stock function that converts enums to ints before serializing them.  
                // We've added a copy to our specialized version of save_enum_type to maintain the exisitng behavior for any 
                // enum types we don't care to have archived in string form
                template<class T>
                static void invoke(xml_oarchive_& ar, const T& t) {
                    const int i = static_cast<int>(t);
                    ar << boost::serialization::make_nvp(NULL, i);
                }


                ///// specialized enum types /////
                // You could probably reduce all the repeated code with some type-trait magic...

                static void invoke(xml_oarchive_& ar, const MyEnum00 &e) {
                    save_non_pointer_type_::invoke(ar, e);
                }

                static void invoke(xml_oarchive_& ar, const MyEnum01 &e) {
                    save_non_pointer_type_::invoke(ar, e);
                }

                // Won't work -- MyEnumClass00 doesn't have an insertion operator, so the underlying ostream won't know 
                // how to handle it
                //static void invoke(xml_oarchive_& ar, const MyEnumClass00 &e) {
                //  save_non_pointer_type_::invoke(ar, e);
                //}

                static void invoke(xml_oarchive_& ar, const MyEnumClass01 &e) {
                    save_non_pointer_type_::invoke(ar, e);
                }
            };

        } // namespace detail
    } // namespace archive
} // namespace boost

And Finally some code to test everything:

int main()
{
    std::stringstream outstream;
    boost::archive::xml_oarchive ar(outstream);

    MyEnum00 e00_0 = Barb;
    MyEnum00 e00_1 = Sarah;
    MyEnum00 e00_2 = Lucy;

    MyEnum01 e01_0 = Corey;
    MyEnum01 e01_1 = Trevor;
    MyEnum01 e01_2 = Jacob;

    MyEnumClass00 ec00_0 = MyEnumClass00::Ricky;
    MyEnumClass00 ec00_1 = MyEnumClass00::Julian;
    MyEnumClass00 ec00_2 = MyEnumClass00::Bubbles;

    MyEnumClass01 ec01_0 = MyEnumClass01::Jim;
    MyEnumClass01 ec01_1 = MyEnumClass01::Randy;
    MyEnumClass01 ec01_2 = MyEnumClass01::Cyrus;

    ar
        // regular enums get passed down as int even if you don't explicitly convert them
        << BOOST_SERIALIZATION_NVP(e00_0)
        << BOOST_SERIALIZATION_NVP(e00_1)
        << BOOST_SERIALIZATION_NVP(e00_2)

        // regular enums can also get special treatment
        << BOOST_SERIALIZATION_NVP(e01_0)
        << BOOST_SERIALIZATION_NVP(e01_1)
        << BOOST_SERIALIZATION_NVP(e01_2)

        // enum classes that aren't specialized pass down as ints
        << BOOST_SERIALIZATION_NVP(ec00_0)
        << BOOST_SERIALIZATION_NVP(ec00_1)
        << BOOST_SERIALIZATION_NVP(ec00_2)

        // enum classes can also get special treatment
        << BOOST_SERIALIZATION_NVP(ec01_0)
        << BOOST_SERIALIZATION_NVP(ec01_1)
        << BOOST_SERIALIZATION_NVP(ec01_2)
        ;

    std::cout << outstream.str() << std::endl;

    return 0;
}