Чтобы сериализовать объекты в байты и из них, вам в конечном итоге понадобятся 2 функции для КАЖДОГО типа, который вы должны поддерживать (примитивы, объекты и т. Д.).Это «Load ()» и «Store ()».
В идеале вы используете фиксированный интерфейс для байтов - iostream, char *, некоторый буферный объект и т. Д. Для удобства чтения давайте вызовемэто «ByteBuffer», поскольку по логике именно в этом его роль.
Теперь у нас есть что-то вроде шаблонных функций для концепции Serializable:
template<typename T>
ByteBuffer Store(const T& object) { // BUT, What goes here...? }
template<typename T>
T Load(const ByteBuffer& bytes);
Хорошо, это не сработает длячто угодно, кроме примитивных типов - даже если мы сделали этих «посетителей» или что-то еще, что они буквально должны знать каждую деталь о внутренностях объекта, чтобы выполнять свою работу.Кроме того, «Load ()» является логически конструктором (на самом деле, FACTORY, поскольку он может легко потерпеть неудачу).Мы должны связать их с реальными объектами.
Чтобы сделать Serializable базовым классом, нам нужно использовать шаблон «любопытно повторяющийся шаблон».Для этого мы требуем, чтобы все производные классы имели конструктор вида:
T(const ByteBuffer& bytes);
Чтобы проверить на наличие ошибок, мы можем предоставить защищенный флаг "valid" в базовом классе, который могут установить производные конструкторы.Обратите внимание, что ваш объект все равно должен поддерживать конструкцию в заводском стиле, чтобы Load () хорошо с ним работала.
Теперь мы можем сделать это правильно, предоставив «Load» как фабрику:
template<typename T>
class Serializable // If you do reference-counting on files & such, you can add it here
{
protected:
bool valid;
// Require derived to mark as valid upon load
Serializable() : valid(false) {}
virtual ~Serializable() { valid = false; }
public:
static T Load(const ByteBuffer& bytes); // calls a "T(bytes)" constructor
// Store API
virtual ByteBuffer Store() = 0; // Interface details are up to you.
};
Теперь, просто производный от базового класса, вот так, и вы можете выбрать все, что вам нужно:
class MyObject : public Serializable<MyObject>
{
protected:
// .. some members ...
MyObject(const ByteBuffer& bytes)
{
//... Actual load logic for this object type ...
// On success only:
valid = true;
}
public:
virtual ByteBuffer Store() {
//... store logic
}
};
Что здорово, что вы можете вызвать «MyObject :: Load ()», и он будет делатьименно то, что вы ожидаете.Более того, «Загрузить» можно сделать ЕДИНСТВЕННЫМ способом построения объекта, позволяя очищать API-интерфейсы для файлов, доступных только для чтения и т. Д.
Расширение этого до полных файловых API-интерфейсов требует немного больше работы, а именно добавление«Load ()», которая может читать из большего буфера (содержащего другие объекты), и «Store ()», который добавляет к существующему буферу.
В качестве примечания, НЕ используйте для этого API-интерфейсы boost.В хорошем дизайне сериализуемые объекты должны отображать 1-к-1 на упакованные структуры примитивных типов на диске - это единственный способ, которым полученные файлы действительно будут использоваться другими программами или на других машинах.Boost дает вам ужасный API, который в основном позволяет вам делать то, о чем вы пожалеете позже.