首页 > 解决方案 > 无需外部库的简单结构的序列化

问题描述

我正在尝试序列化像下面这样的简单单级类,没有像 boost 这样的外部库,也不必为每个类实现序列化器函数。尽管我的类很少,我可以轻松地为每个类实现一个序列化程序,但为了将来参考,我希望手头有一个可以很好地扩展的简单解决方案。

每个类被序列化的要求是它的成员只能是可序列化的类型,并且定义了一个成员指针数组,这样在序列化时,无论传递哪个类,都可以迭代成员。

问题是编译失败是因为缺少成员指针被取消引用的强制转换,显然:

esp32.ino: 122:35: error: 'footprint.MessageFootprint<1>::Members[i]' 不能用作成员指针,因为它的类型是 'void*

我不知道如何将成员指针存储在可迭代集合中或如何避免强制转换void*。这就是我的目标。我想在具有单个通用序列化函数的序列化时迭代类成员。我不知道该怎么办。

enum SerializableDataTypes {
    SerInt,
    SerFloat,
    SerString,
    SerIntArray
};

template <int N>
struct MessageFootprint {
    SerializableDataTypes DataTypes[N];
    void* Members[N];
};

template<typename T, typename R>
void* void_cast(R(T::*m))
{
    union
    {
        R(T::*pm);
        void* p;
    };
    pm = m;
    return p;
}

class ControlMessage{};

// first structure to be serialized
class Message1 : public ControlMessage {
public:
    int prop1;
    int prop2;
};
const int Message1MemberCount = 2;
const MessageFootprint<Message1MemberCount> Message1FootPrint = { { SerInt, SerInt }, {void_cast(&Message1::prop1), void_cast(&Message1::prop2)} };

// second structure to be serialized
class Message2 : public ControlMessage {
public:
    int prop1;
    String prop2;
};
const int Message2MemberCount = 2;
const MessageFootprint<Message2MemberCount> Message2FootPrint = { { SerInt, SerInt }, {void_cast(&Message2::prop1), void_cast(&Message2::prop2)} };

template<int N>
void SerializeMessage(MessageFootprint<N> footprint, ControlMessage message) {
    for (int i = 0; i < N; i++) {
        if (footprint.DataTypes[i] == SerInt) {
            // serialization code here based on data type
            // for demonstration purposes it's only written in the serial port
            logLine(String(i));
            Serial.println(*((int*)(message.*(footprint.Members[i]))));
        }
    }
}

void main() {
    // usage example
    Message1 msg = Message1();
    msg.prop1 = 1;
    msg.prop2 = 2;
    SerializeMessage(Message1FootPrint, msg);
}

标签: c++serializationmember-pointers

解决方案


不要擦除类型;也就是说,不要将指针指向void*. 如果您通过模板保留指针的类型,您可以直接从它们的类型中选择反序列化函数,因此您甚至不必指定它们。实际上,您已经有一个错误,您将第二个成员标记Message2 SerIntString. 如果您处理实际类型而不是强制用户复制它们,则可以避免此类错误。此外,公共超类是完全没有必要的。

template<typename T, typename... Parts>
struct MessageFootprint {
    std::tuple<Parts T::*...> parts;
    MessageFootprint(Parts T::*... parts) : parts(parts...) { }
};
template<typename T, typename... Parts>
MessageFootprint(Parts T::*...) -> MessageFootprint<T, Parts...>; // deduction guide

// e.g.
struct Message1 {
    int prop1;
    int prop2;
};
inline MessageFootprint footprint1(&Message1::prop1, &Message1::prop2);
// deduction guide allows type of footprint1 to be inferred from constructor arguments
// it is actually MessageFootprint<Message1, int, int>
// if you are on a C++ standard old enough to not have deduction guides,
// you will have to manually specify them
// this is still better than letting the types be erased, because now the compiler
// will complain if you get it wrong

// e.g. if I replicate your mistake
struct Message2 {
    int prop1;
    std::string prop2;
};
inline MessageFootprint<Message2, int, int> footprint2(&Message2::prop1, &Message2::prop2);
// This does not go through because    ^^^ is wrong

序列化可能最好通过重载来处理。对于 a 中的每个,从Part T::*aMessageFootprint<T, Part...>中提取 a并调用一个重载函数,该函数根据 决定做什么:Part&TPart

// I have no idea what serial port communication stuff you're doing
// but this gets the point across
void SerializeAtom(int i) { std::cout << "I" << i; }
void SerializeAtom(std::string const &s) { std::cout << "S" << s.size() << "S" << s; }

template<typename T, typename... Parts>
void SerializeFootprint(MessageFootprint<T, Parts...> footprint, T const &x) {
    // calls the provided functor with the things in the tuple
    std::apply(
        // this lambda is a template with its own Parts2... template parameter pack
        // and the argument is really Parts2... parts
        // we then do a fold expression over parts
        // we need std::apply because there's no simpler way to get the actual
        // values out (std::get fails when there are duplicates)
        [&x](auto... parts) { (SerializeAtom(x.*parts), ...); },
        footprint.parts);
}
// Trying to write ^^^ before C++17 would probably be a nightmare

这个系统是可扩展的:添加一个新的“原子”类型,只是重载SerializeAtom。无需管理enum或诸如此类。DeserializeAtom反序列化将意味着写入给定引用的一系列重载,并且 aDeserializeFootprint可能看起来完全像SerializeFootprint.

神螺栓演示


推荐阅读