Існує загальний шаблон, який можна використовувати для серіалізації об’єктів. Основним примітивом є ці дві функції, які ви можете читати та писати з ітераторів:
template <class OutputCharIterator>
void putByte(char byte, OutputCharIterator &&it)
{
*it = byte;
++it;
}
template <class InputCharIterator>
char getByte(InputCharIterator &&it, InputCharIterator &&end)
{
if (it == end)
{
throw std::runtime_error{"Unexpected end of stream."};
}
char byte = *it;
++it;
return byte;
}
Тоді функції серіалізації та десериалізації слідують шаблону:
template <class OutputCharIterator>
void serialize(const YourType &obj, OutputCharIterator &&it)
{
}
template <class InputCharIterator>
void deserialize(YourType &obj, InputCharIterator &&it, InputCharIterator &&end)
{
}
Для класів ви можете використовувати шаблон функції друга, щоб дозволити знаходити перевантаження за допомогою ADL:
class Foo
{
int internal1, internal2;
template <class OutputCharIterator>
friend void serialize(const Foo &obj, OutputCharIterator &&it)
{
}
};
У вашій програмі ви можете серіалізувати і об'єкт у такий файл:
std::ofstream file("savestate.bin");
serialize(yourObject, std::ostreambuf_iterator<char>(file));
Тоді читайте:
std::ifstream file("savestate.bin");
deserialize(yourObject, std::istreamBuf_iterator<char>(file), std::istreamBuf_iterator<char>());
Моя стара відповідь тут:
Серіалізація означає перетворення об’єкта на двійкові дані. Тоді як десериалізація означає відтворення об'єкта з даних.
Під час серіалізації ви засовуєте байти у uint8_t
вектор. При десеріалізації ви читаєте байти з uint8_t
вектора.
Звичайно, є шаблони, які можна використовувати при серіалізації матеріалів.
Кожен клас, який можна серіалізувати, повинен мати serialize(std::vector<uint8_t> &binaryData)
подібну або подібну сигналізовану функцію, яка запише своє двійкове представлення у наданий вектор. Тоді ця функція може передавати цей вектор до серіалізуючих функцій свого члена, щоб вони могли також писати в них свої речі.
Оскільки представлення даних може бути різним для різних архітектур. Вам потрібно з’ясувати схему подання даних.
Почнемо з основ:
Серіалізація цілочисельних даних
Просто запишіть байти в маленькому порядку. Або використовуйте представлення varint, якщо розмір має значення.
Серіалізація в маленькому ендіанському порядку:
data.push_back(integer32 & 0xFF);
data.push_back((integer32 >> 8) & 0xFF);
data.push_back((integer32 >> 16) & 0xFF);
data.push_back((integer32 >> 24) & 0xFF);
Десериалізація з малого ендіанського порядку:
integer32 = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24);
Серіалізація даних із плаваючою точкою
Наскільки мені відомо, IEEE 754 має тут монополію. Я не знаю жодної основної архітектури, яка б використовувала щось інше для поплавців. Єдине, що може бути різним - це порядок байтів. Деякі архітектури використовують малий ендіан, інші використовують великий байтовий порядок ендіан. Це означає, що вам слід бути обережним, який порядок вам підсилює байти на приймальному кінці. Іншою відмінністю може бути обробка деннормальних значень та значень нескінченності та NAN. Але поки ви уникаєте цих значень, у вас все буде в порядку.
Серіалізація:
uint8_t mem[8];
memcpy(mem, doubleValue, 8);
data.push_back(mem[0]);
data.push_back(mem[1]);
...
Десериалізація робить це назад. Зверніть увагу на порядок байтів вашої архітектури!
Серіалізація рядків
Спочатку потрібно домовитись про кодування. UTF-8 є загальним явищем. Потім збережіть його як префікс довжини: спочатку ви зберігаєте довжину рядка за допомогою методу, про який я згадав вище, а потім пишемо рядок байт за байтом.
Серіалізація масивів.
Вони те саме, що струни. Спочатку серіалізується ціле число, що представляє розмір масиву, а потім серіалізується кожен об’єкт у ньому.
Серіалізація цілих об'єктів
Як я вже говорив раніше, вони повинні мати serialize
метод, який додає вміст до вектора. Щоб десеріалізувати об'єкт, він повинен мати конструктор, який приймає потік байтів. Це може бути, istream
але в найпростішому випадку це може бути просто опорний uint8_t
вказівник. Конструктор зчитує потрібні байти з потоку та встановлює поля в об'єкті. Якщо система добре розроблена і серіалізує поля в порядку об'єктних полів, ви можете просто передати потік конструкторам поля в списку ініціалізаторів і десеріалізувати їх у правильному порядку.
Серіалізація графіків об’єктів
Спочатку потрібно переконатися, чи справді ці об’єкти - це те, що ви хочете серіалізувати. Вам не потрібно їх серіалізувати, якщо екземпляри цих об’єктів присутні в пункті призначення.
Тепер ви виявили, що вам потрібно серіалізувати цей об’єкт, на який вказує вказівник. Проблема покажчиків полягає в тому, що вони дійсні лише в програмі, яка їх використовує. Ви не можете серіалізувати покажчик, вам слід припинити їх використання в об’єктах. Натомість створіть пули об’єктів. Цей пул об'єктів є в основному динамічним масивом, який містить "коробки". Ці поля мають кількість посилань. Ненульовий відлік посилань позначає живий об'єкт, нуль - порожній слот. Потім ви створюєте розумний вказівник, подібний до shared_ptr, який зберігає не вказівник на об’єкт, а індекс у масиві. Вам також потрібно узгодити індекс, який позначає нульовий покажчик, наприклад. -1.
В основному те, що ми зробили тут, замінило покажчики на індекси масивів. Тепер при серіалізації ви можете серіалізувати цей індекс масиву, як зазвичай. Вам не потрібно турбуватися про те, де знаходиться об’єкт у пам’яті в системі призначення. Просто переконайтеся, що у них теж однаковий пул об’єктів.
Отже, нам потрібно серіалізувати пули об’єктів. Але які? Ну, коли ви серіалізуєте графік об’єкта, ви серіалізуєте не просто об’єкт, а серіалізуєте цілу систему. Це означає, що серіалізація системи не повинна починатися з частин системи. Ці об'єкти не повинні турбуватися про решту системи, їм потрібно лише серіалізувати індекси масивів і все. Ви повинні мати процедуру серіалізатора системи, яка організовує серіалізацію системи та проходить через відповідні пули об’єктів та серіалізує всі їх.
На приймальному кінці всі масиви та об'єкти всередині десеріалізуються, відтворюючи потрібний графік об'єкта.
Вказівники на функції серіалізації
Не зберігайте покажчики в об’єкті. Майте статичний масив, який містить вказівники на ці функції та зберігає індекс в об’єкті.
Оскільки в обох програмах ця таблиця скомпільована на полиці, використання лише індексу має працювати.
Серіалізуючі поліморфні типи
Оскільки я сказав, що вам слід уникати покажчиків у серіалізованих типах, а замість цього слід використовувати індекси масивів, поліморфізм просто не може працювати, оскільки для цього потрібні вказівники.
Вам потрібно обійти це з тегами типів та об’єднаннями.
Версія
Поверх усього вищесказаного. Можливо, вам захочуть взаємодіяти різні версії програмного забезпечення.
У цьому випадку кожен об'єкт повинен написати номер версії на початку їх серіалізації, щоб вказати версію.
Завантажуючи об'єкт з іншого боку, новіші об'єкти можуть обробляти старіші уявлення, але старші не можуть обробляти новіші, тому вони повинні створити виняток щодо цього.
Кожного разу, коли щось змінюється, ви повинні вказати номер версії.
Тож для завершення серіалізація може бути складною. Але на щастя, вам не потрібно серіалізувати все у вашій програмі, найчастіше серіалізуються лише повідомлення протоколу, які часто є звичайними старими структурами. Тож вам не потрібні складні трюки, про які я згадував надто часто.