I'm trying to serialize simple single level classes like the ones bellow, without external libraries like boost, and without having to implement serializer function for every class. Even though I have so few classes that I could easily implement a serializer for each one, for future reference, I would like to have at hand a simple solution that scales well.
The requirement for each class to be serialized is that its members are only serializable types, and an array of member pointers is defined, so that at serialization the members can be iterated regardless of which class is passed.
The problem is that the compilation fails because of the missing cast where the member pointer is dereferenced, obviously:
esp32.ino: 122:35: error: 'footprint.MessageFootprint<1>::Members[i]' cannot be used as a member pointer, since it is of type 'void*
I don't know how to store the member pointers in a iterable collection or how to avoid the void* cast.
That's my goal. I want to iterate over class members at serialization having a single generic serialization function.
I don't know what to do.
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);
}
Don't erase types; that is, don't cast your pointers to
void*. If you preserve the types of the pointers through templates, you can choose the deserialization functions directly off their types, and thus you won't even have to specify them. Indeed, you already have a bug where you have marked the second member ofMessage2SerIntwhen it is aString. If you work off the actual types instead of forcing the user to duplicate them, you avoid such errors. Also, the common superclass is completely unnecessary.Serialization is probably best handled with overloading. For each
Part T::*in aMessageFootprint<T, Part...>, extract aPart&from theTand call out to an overloaded function that decides what to do based onPart:This system is extensible: to add a new "atomic" type, just overload
SerializeAtom. No need to manage anenumor whatnot. Deserialization would mean a family ofDeserializeAtomoverloads that write into the given reference, and aDeserializeFootprintwhich would probably look exactly likeSerializeFootprint.Godbolt demonstration