Jak serializować obiekt w C ++?

85

Mam małą hierarchię obiektów, które muszę serializować i przesyłać przez połączenie przez gniazdo. Muszę zarówno serializować obiekt, a następnie deserializować go na podstawie tego, jakiego typu jest. Czy jest na to łatwy sposób w C ++ (tak jak w Javie)?

Czy są jakieś przykłady kodu lub samouczki online dotyczące serializacji C ++?

EDYCJA: Żeby było jasne, szukam metod konwersji obiektu na tablicę bajtów, a następnie z powrotem na obiekt. Poradzę sobie z transmisją przez gniazdo.

Bill the Lizard
źródło
3
Sprawdź google :: protobuf , jest to bardzo silna i szybka biblioteka do serializacji binarnej. Użyliśmy go z powodzeniem z boost :: asio itp.
Ketan,
Spójrz na [STLPLUS] [1], bibliotekę z implementacją trwałości. [1]: stlplus.sourceforge.net
lsalamon
4
Podane odpowiedzi w rzeczywistości nie wyjaśniają, jak serializować. Jeden oferuje bibliotekę zwiększania serializacji, drugi wyjaśnia problemy w naiwnej implementacji. Skoro to jest c ++ - najczęściej zadawane pytanie, czy ktoś może na nie odpowiedzieć?
anonimowy

Odpowiedzi:

55

Mówiąc o serializacji, przychodzi mi na myśl interfejs API serializacji boost . Jeśli chodzi o transmisję serializowanych danych przez sieć, użyłbym gniazd Berkeley lub biblioteki asio .

Edycja:
Jeśli chcesz serializować swoje obiekty do tablicy bajtów, możesz użyć serializatora przyspieszenia w następujący sposób (wzięty z witryny samouczka):

#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)
    {}
};

Rzeczywista serializacja jest wtedy całkiem łatwa:

#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
    }

W analogiczny sposób działa deserializacja.

Istnieją również mechanizmy, które pozwalają obsłużyć serializację wskaźników (złożone struktury danych, takie jak tres itp. Nie stanowią problemu), klasy pochodne i możesz wybierać między serializacją binarną i tekstową. Poza tym wszystkie kontenery STL są obsługiwane po wyjęciu z pudełka.

newgre
źródło
To jest pytanie c ++, dlaczego klasa gps_position przeciąża operator <<. Nie ma zdefiniowanej funkcji znajomego
Vicente Bolea
zwróć uwagę na "friend class boost :: serialization :: access". Zapewnia to dostęp do funkcji biblioteki serializacji członkom klasy, nawet jeśli są prywatni.
Robert Ramey,
13

W niektórych przypadkach, gdy masz do czynienia z prostymi typami, możesz:

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

To jest w porządku, jako dowód koncepcji lub pierwsza wersja robocza, więc inni członkowie Twojego zespołu mogą dalej pracować nad innymi częściami.

Ale prędzej czy później, zwykle wcześniej , zranisz cię!

Masz problemy z:

  • Tabele wskaźników wirtualnych zostaną uszkodzone.
  • Wskaźniki (do danych / członków / funkcji) zostaną uszkodzone.
  • Różnice w wypełnieniu / wyrównaniu na różnych komputerach.
  • Problemy z kolejnością bajtów Big / Little-Endian.
  • Różnice w implementacji float / double.

(Dodatkowo musisz wiedzieć, do czego rozpakowujesz po stronie odbiorcy).

Możesz to poprawić, opracowując własne metody krosowania / usuwania błędów dla każdej klasy. (Idealnie wirtualne, więc można je rozszerzać w podklasy.) Kilka prostych makr pozwoli ci dość szybko napisać różne podstawowe typy w porządku duży / little-endian-neutralny.

Ale ten rodzaj podstawowej pracy jest znacznie lepszy i łatwiejszy do wykonania za pomocą biblioteki serializacji boost .

Panie Ree
źródło
To było coś, o czym myślałem. Ale ponieważ chcę serializować do strumienia sieciowego, to w ogóle nie działa. Co najwyżej z powodu endianizmu i różnych platform. Ale nie wiedziałem, że psuje wirtualne wskaźniki. Thanks =)
Atmocreations
4

Istnieje ogólny wzorzec, którego można użyć do serializacji obiektów. Podstawowym prymitywem są te dwie funkcje, które można odczytywać i zapisywać z iteratorów:

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;
}

Następnie funkcje serializacji i deserializacji są zgodne ze wzorcem:

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.
}

W przypadku klas możesz użyć wzorca funkcji znajomego, aby umożliwić znalezienie przeciążenia za pomocą 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.
};

W swoim programie możesz serializować i obiektować do pliku takiego jak ten:

std::ofstream file("savestate.bin");
serialize(yourObject, std::ostreambuf_iterator<char>(file));

Potem przeczytaj:

std::ifstream file("savestate.bin");
deserialize(yourObject, std::istreamBuf_iterator<char>(file), std::istreamBuf_iterator<char>());

Moja stara odpowiedź tutaj:

Serializacja oznacza przekształcenie obiektu w dane binarne. Podczas deserializacji oznacza ponowne utworzenie obiektu z danych.

Podczas serializacji wypychasz bajty do uint8_twektora. Podczas odserializacji odczytujesz bajty z uint8_twektora.

Z pewnością istnieją wzorce, których możesz użyć podczas serializacji rzeczy.

Każda klasa serialize(std::vector<uint8_t> &binaryData)możliwa do serializacji powinna mieć podpisaną lub podobną funkcję, która zapisze swoją reprezentację binarną do podanego wektora. Następnie ta funkcja może przekazać ten wektor do funkcji serializacji swojego elementu członkowskiego, aby mogli również zapisywać w nim swoje rzeczy.

Ponieważ reprezentacja danych może być różna na różnych architekturach. Musisz znaleźć schemat reprezentacji danych.

Zacznijmy od podstaw:

Serializacja danych całkowitych

Po prostu zapisz bajty w kolejności little endian. Lub użyj reprezentacji varint, jeśli rozmiar ma znaczenie.

Serializacja w kolejności little endian:

data.push_back(integer32 & 0xFF);
data.push_back((integer32 >> 8) & 0xFF);
data.push_back((integer32 >> 16) & 0xFF);
data.push_back((integer32 >> 24) & 0xFF);

Deserializacja z Little Endian Order:

integer32 = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24);

Serializacja danych zmiennoprzecinkowych

O ile wiem, IEEE 754 ma tutaj monopol. Nie znam żadnej architektury głównego nurtu, która używałaby czegoś innego dla pływaków. Jedyne, co może się różnić, to kolejność bajtów. Niektóre architektury używają little endian, inne używają kolejności bajtów big endian. Oznacza to, że musisz uważać, w której kolejności zwiększasz liczbę bajtów na końcu odbierającym. Inną różnicą może być obsługa wartości denormalnych i nieskończoności oraz NAN. Ale dopóki unikasz tych wartości, powinieneś być w porządku.

Serializacja:

uint8_t mem[8];
memcpy(mem, doubleValue, 8);
data.push_back(mem[0]);
data.push_back(mem[1]);
...

Deserializacja robi to wstecz. Uważaj na kolejność bajtów w swojej architekturze!

Szeregowanie ciągów

Najpierw musisz zgodzić się na kodowanie. UTF-8 jest powszechny. Następnie zapisz go jako długość z prefiksem długości: najpierw przechowujesz długość ciągu przy użyciu metody, o której wspomniałem powyżej, a następnie zapisujesz ciąg bajt po bajcie.

Serializacja tablic.

Są takie same jak struny. Najpierw serializujesz liczbę całkowitą reprezentującą rozmiar tablicy, a następnie serializujesz każdy obiekt w niej.

Serializacja całych obiektów

Jak powiedziałem wcześniej, powinni mieć serializemetodę dodającą zawartość do wektora. Aby odserializować obiekt, powinien mieć konstruktor, który pobiera strumień bajtów. Może to być, istreamale w najprostszym przypadku może to być tylko uint8_twskaźnik referencyjny . Konstruktor odczytuje żądane bajty ze strumienia i konfiguruje pola w obiekcie. Jeśli system jest dobrze zaprojektowany i serializuje pola w kolejności obiektów, możesz po prostu przekazać strumień do konstruktorów pola na liście inicjalizatora i deserializować je we właściwej kolejności.

Serializacja grafów obiektów

Najpierw musisz się upewnić, czy te obiekty są naprawdę czymś, co chcesz serializować. Nie musisz ich serializować, jeśli wystąpienia tych obiektów znajdują się w miejscu docelowym.

Teraz dowiedziałeś się, że musisz serializować ten obiekt wskazywany przez wskaźnik. Problem ze wskazówkami, że są one ważne tylko w programie, który ich używa. Nie możesz serializować wskaźnika, powinieneś przestać używać ich w obiektach. Zamiast tego twórz pule obiektów. Ta pula obiektów jest w zasadzie tablicą dynamiczną, która zawiera „pudełka”. Te pudełka mają liczbę referencyjną. Niezerowa liczba referencyjna wskazuje na aktywny obiekt, zero wskazuje na pusty slot. Następnie tworzysz inteligentny wskaźnik podobny do shared_ptr, który nie przechowuje wskaźnika do obiektu, ale indeks w tablicy. Musisz również uzgodnić indeks, który oznacza pusty wskaźnik, np. -1.

Zasadniczo to, co tutaj zrobiliśmy, to zastąpienie wskaźników indeksami tablicowymi. Teraz podczas serializacji możesz jak zwykle serializować ten indeks tablicy. Nie musisz martwić się o to, gdzie obiekt znajdzie się w pamięci w systemie docelowym. Upewnij się tylko, że mają też tę samą pulę obiektów.

Musimy więc serializować pule obiektów. Ale które z nich? Cóż, kiedy serializujesz graf obiektu, nie serializujesz tylko obiektu, serializujesz cały system. Oznacza to, że serializacja systemu nie powinna rozpoczynać się od części systemu. Te obiekty nie powinny martwić się o resztę systemu, wystarczy serializować indeksy tablic i to wszystko. Powinieneś mieć procedurę serializatora systemu, która organizuje serializację systemu i przechodzi przez odpowiednie pule obiektów i serializuje je wszystkie.

Po stronie odbiorczej wszystkie tablice i obiekty wewnątrz są deserializowane, odtwarzając żądany wykres obiektów.

Wskaźniki funkcji serializacji

Nie przechowuj wskaźników w obiekcie. Miej tablicę statyczną, która zawiera wskaźniki do tych funkcji i przechowuj indeks w obiekcie.

Ponieważ oba programy mają tę tabelę wkompilowaną na półkach, użycie samego indeksu powinno działać.

Serializacja typów polimorficznych

Ponieważ powiedziałem, że należy unikać wskaźników w typach możliwych do serializacji, a zamiast tego należy używać indeksów tablicowych, polimorfizm po prostu nie może działać, ponieważ wymaga wskaźników.

Musisz to obejść za pomocą znaczników typu i związków.

Wersjonowanie

Na dodatek wszystkie powyższe. Możesz chcieć, aby różne wersje oprogramowania współpracowały.

W takim przypadku każdy obiekt powinien zapisać numer wersji na początku swojej serializacji, aby wskazać wersję.

Podczas ładowania obiektu po drugiej stronie, nowsze obiekty mogą być w stanie obsłużyć starsze reprezentacje, ale starsze nie mogą obsłużyć nowszych, więc powinny zgłosić wyjątek w tej sprawie.

Za każdym razem, gdy coś się zmieni, należy podnieść numer wersji.


Podsumowując, serializacja może być złożona. Ale na szczęście nie musisz serializować wszystkiego w swoim programie, najczęściej tylko komunikaty protokołu są serializowane, które często są zwykłymi starymi strukturami. Więc nie potrzebujesz zbyt często skomplikowanych sztuczek, o których wspomniałem powyżej.

Calmarius
źródło
1
Dziękuję Ci. Ta odpowiedź zawiera doskonały przegląd pojęć związanych z serializacją danych strukturalnych w C ++.
Sean
0

W ramach nauki napisałem prosty serializator C ++ 11. Próbowałem różnych innych ofert wagi ciężkiej, ale chciałem czegoś, co mógłbym zrozumieć, gdy poszło źle lub nie udało mi się skompilować z najnowszym g ++ (co zdarzyło się w przypadku Cereal; naprawdę fajna biblioteka, ale złożona i nie mogłem błędy, które kompilator wyskoczył podczas aktualizacji.) W każdym razie, to tylko nagłówek i obsługuje typy POD, kontenery, mapy itp ... Bez wersjonowania i ładuje tylko pliki z tego samego arch, w którym został zapisany.

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

Przykładowe użycie:

#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);
}

Neil McGill
źródło