Як здійснюється серіалізація об’єкта в C ++?


85

У мене є невелика ієрархія об’єктів, які мені потрібно серіалізувати та передавати через сокетне з’єднання. Мені потрібно обидві серіалізувати об’єкт, а потім десеріалізувати його залежно від типу. Чи є простий спосіб зробити це в C ++ (як у Java)?

Чи існують зразки чи навчальні посібники для серіалізації C ++ в Інтернеті?

EDIT: Щоб зрозуміти, я шукаю методи перетворення об’єкта в масив байтів, а потім назад в об’єкт. Я впораюся з передачею розетки.


3
Перевірте google :: protobuf , це дуже потужна і швидка бібліотека для двійкової серіалізації. Ми успішно використали його з boost :: asio тощо
Кетан

Погляньте на [STLPLUS] [1], lib із постійною реалізацією. [1]: stlplus.sourceforge.net
Ілсаламон

4
Надані відповіді насправді не пояснюють, як серіалізувати. Один пропонує бібліотеку посилення серіалізації, інший пояснює проблеми в наївній реалізації. Оскільки це запитання про c ++ - FAQ , чи може хтось насправді відповісти на нього?
анонімно

Відповіді:


55

Говорячи про серіалізацію, мені спадає на думку API посилення серіалізації . Що стосується передачі серіалізованих даних через мережу, я б використовував сокети Берклі або бібліотеку asio .

Редагувати:
якщо ви хочете серіалізувати свої об’єкти в байтовий масив, ви можете скористатися серіалізатором boost наступним чином (взято з сайту підручника):

#include <boost/archive/binary_oarchive.hpp>
#include <boost/archive/binary_iarchive.hpp>
class gps_position
{
private:
    friend class boost::serialization::access;
    template<class Archive>
    void serialize(Archive & ar, const unsigned int version)
    {
        ar & degrees;
        ar & minutes;
        ar & seconds;
    }
    int degrees;
    int minutes;
    float seconds;

public:
    gps_position(){};
    gps_position(int d, int m, float s) :
    degrees(d), minutes(m), seconds(s)
    {}
};

Фактична серіалізація тоді досить проста:

#include <fstream>
std::ofstream ofs("filename.dat", std::ios::binary);

    // create class instance
    const gps_position g(35, 59, 24.567f);

    // save data to archive
    {
        boost::archive::binary_oarchive oa(ofs);
        // write class instance to archive
        oa << g;
        // archive and stream closed when destructors are called
    }

Десеріалізація працює аналогічним чином.

Існують також механізми, які дозволяють обробляти серіалізацію покажчиків (складні структури даних, такі як tress тощо, не є проблемою), похідні класи, і ви можете вибрати між бінарною та текстовою серіалізацією. Крім того, всі контейнери STL підтримуються з коробки.


Це питання на c ++, чому так клас gps_position перевантажує оператор <<. Не визначено жодної функції друзів
Вісенте Болеа

зверніть увагу на "підвищення класу друзів :: серіалізація :: доступ". Це забезпечує доступ до функцій бібліотеки серіалізації членам класу, навіть якщо вони приватні.
Роберт Рамі,

13

У деяких випадках, маючи справу з простими типами, ви можете зробити:

object o;
socket.write(&o, sizeof(o));

Це нормально як доказ концепції чи перший проект, тому інші члени вашої команди можуть продовжувати працювати над іншими частинами.

Але рано чи пізно, як правило , це швидше зашкодить вам!

Ви стикаєтесь із проблемами з:

  • Віртуальні таблиці покажчиків будуть пошкоджені.
  • Покажчики (на дані / члени / функції) будуть пошкоджені.
  • Відмінності в забиванні / вирівнюванні на різних машинах.
  • Проблеми з упорядкуванням байтів великого / маленького ендіана.
  • Варіації у реалізації float / double.

(Плюс вам потрібно знати, що ви розпаковуєте на приймаючій стороні.)

Ви можете покращити це, розробивши власні методи маршалювання / демаршалювання для кожного класу. (В ідеалі віртуальний, тому їх можна розширити за підкласами.) Кілька простих макросів дозволять досить швидко виписувати різні основні типи у великому / мало-ендіанському нейтральному порядку.

Але така робота з бурчанням набагато краща і легша за допомогою бібліотеки серіалізації Boost .


Це було те, про що я думав. Але оскільки я хочу здійснити серіалізацію до мережевого потоку, це взагалі не працює. Найбільше через ендіанність та різні платформи. Але я не знав, що це псує віртуальні вказівники. Дякую =)
Атмокрації

4

Існує загальний шаблон, який можна використовувати для серіалізації об’єктів. Основним примітивом є ці дві функції, які ви можете читати та писати з ітераторів:

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)
{
    // Call putbyte or other serialize overloads.
}

template <class InputCharIterator>
void deserialize(YourType &obj, InputCharIterator &&it, InputCharIterator &&end)
{
    // Call getByte or other deserialize overloads.
}

Для класів ви можете використовувати шаблон функції друга, щоб дозволити знаходити перевантаження за допомогою ADL:

class Foo
{
    int internal1, internal2;

    // So it can be found using ADL and it accesses private parts.
    template <class OutputCharIterator>
    friend void serialize(const Foo &obj, OutputCharIterator &&it)
    {
        // Call putByte or other serialize overloads.
    }

    // Deserialize similar.
};

У вашій програмі ви можете серіалізувати і об'єкт у такий файл:

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.

В основному те, що ми зробили тут, замінило покажчики на індекси масивів. Тепер при серіалізації ви можете серіалізувати цей індекс масиву, як зазвичай. Вам не потрібно турбуватися про те, де знаходиться об’єкт у пам’яті в системі призначення. Просто переконайтеся, що у них теж однаковий пул об’єктів.

Отже, нам потрібно серіалізувати пули об’єктів. Але які? Ну, коли ви серіалізуєте графік об’єкта, ви серіалізуєте не просто об’єкт, а серіалізуєте цілу систему. Це означає, що серіалізація системи не повинна починатися з частин системи. Ці об'єкти не повинні турбуватися про решту системи, їм потрібно лише серіалізувати індекси масивів і все. Ви повинні мати процедуру серіалізатора системи, яка організовує серіалізацію системи та проходить через відповідні пули об’єктів та серіалізує всі їх.

На приймальному кінці всі масиви та об'єкти всередині десеріалізуються, відтворюючи потрібний графік об'єкта.

Вказівники на функції серіалізації

Не зберігайте покажчики в об’єкті. Майте статичний масив, який містить вказівники на ці функції та зберігає індекс в об’єкті.

Оскільки в обох програмах ця таблиця скомпільована на полиці, використання лише індексу має працювати.

Серіалізуючі поліморфні типи

Оскільки я сказав, що вам слід уникати покажчиків у серіалізованих типах, а замість цього слід використовувати індекси масивів, поліморфізм просто не може працювати, оскільки для цього потрібні вказівники.

Вам потрібно обійти це з тегами типів та об’єднаннями.

Версія

Поверх усього вищесказаного. Можливо, вам захочуть взаємодіяти різні версії програмного забезпечення.

У цьому випадку кожен об'єкт повинен написати номер версії на початку їх серіалізації, щоб вказати версію.

Завантажуючи об'єкт з іншого боку, новіші об'єкти можуть обробляти старіші уявлення, але старші не можуть обробляти новіші, тому вони повинні створити виняток щодо цього.

Кожного разу, коли щось змінюється, ви повинні вказати номер версії.


Тож для завершення серіалізація може бути складною. Але на щастя, вам не потрібно серіалізувати все у вашій програмі, найчастіше серіалізуються лише повідомлення протоколу, які часто є звичайними старими структурами. Тож вам не потрібні складні трюки, про які я згадував надто часто.


1
Дякую. Ця відповідь містить чудовий огляд концепцій, що мають відношення до серіалізації структурованих даних на C ++.
Шон

0

Для навчання я написав простий серіалізатор C ++ 11. Я пробував різні інші більш важкі пропозиції, але хотів чогось, що я міг насправді зрозуміти, коли він пішов не так або не вдалося скомпілювати з останньою g ++ (що трапилося для мене з зерновими; справді приємна бібліотека, але складна, і я не міг гаркнути помилки, які видав компілятор під час оновлення.) У будь-якому випадку, це лише заголовок і обробляє типи POD, контейнери, карти тощо ... Немає версій, і він буде завантажувати лише файли з тієї самої арки, в якій він був збережений.

https://github.com/goblinhack/simple-c-plus-plus-serializer

Приклад використання:

#include "c_plus_plus_serializer.h"

static void serialize (std::ofstream out)
{
    char a = 42;
    unsigned short b = 65535;
    int c = 123456;
    float d = std::numeric_limits<float>::max();
    double e = std::numeric_limits<double>::max();
    std::string f("hello");

    out << bits(a) << bits(b) << bits(c) << bits(d);
    out << bits(e) << bits(f);
}

static void deserialize (std::ifstream in)
{
    char a;
    unsigned short b;
    int c;
    float d;
    double e;
    std::string f;

    in >> bits(a) >> bits(b) >> bits(c) >> bits(d);
    in >> bits(e) >> bits(f);
}

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.