Comment sérialiser un object en C ++?

J’ai une petite hiérarchie d’objects que je dois sérialiser et transmettre via une connexion socket. Je dois à la fois sérialiser l’object, puis le désérialiser en fonction de son type. Y a-t-il un moyen facile de faire cela en C ++ (comme c’est le cas en Java)?

Existe-t-il des exemples de code ou des didacticiels en ligne de sérialisation C ++?

EDIT: Juste pour être clair, je cherche des méthodes pour convertir un object dans un tableau d’octets, puis de nouveau dans un object. Je peux gérer la transmission de socket.

En parlant de sérialisation, l’ API de sérialisation boost me vient à l’esprit. En ce qui concerne la transmission des données sérialisées sur Internet, j’utiliserais les sockets Berkeley ou la bibliothèque asio .

Modifier:
Si vous souhaitez sérialiser vos objects en un tableau d’octets, vous pouvez utiliser le sérialiseur boost de la manière suivante (prise depuis le site du didacticiel):

#include  #include  class gps_position { private: friend class boost::serialization::access; template 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) {} }; 

La sérialisation réelle est alors assez facile:

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

La désérialisation fonctionne de manière analogue.

Il existe également des mécanismes qui vous permettent de gérer la sérialisation des pointeurs (les structures de données complexes comme tress, etc. ne posent aucun problème), les classes dérivées et vous pouvez choisir entre la sérialisation binary et la sérialisation de texte. De plus, tous les conteneurs STL sont supportés hors de la boîte.

Dans certains cas, lorsque vous traitez des types simples, vous pouvez faire:

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

Ce n’est pas une erreur de preuve de concept ou de première version, donc les autres membres de votre équipe peuvent continuer à travailler sur d’autres parties.

Mais tôt ou tard, généralement plus tôt , cela vous blessera!

Vous rencontrez des problèmes avec:

  • Les tables de pointeurs virtuels seront corrompues.
  • Les pointeurs (vers data / members / functions) seront corrompus.
  • Différences de remplissage / alignement sur différentes machines.
  • Problèmes d’ordre des octets Big / Little-Endian.
  • Variations dans la mise en œuvre de float / double.

(De plus, vous devez savoir ce que vous déballez du côté réception).

Vous pouvez améliorer cela en développant vos propres méthodes de formation / désorganisation pour chaque classe. (Idéalement virtuel, ils peuvent être étendus dans des sous-classes.) Quelques macros simples vous permettront d’écrire rapidement différents types de base dans un ordre big / little-endian-neutral.

Mais ce type de travail est bien meilleur et plus facile à gérer via la bibliothèque de sérialisation de Boost .

La sérialisation consiste à transformer votre object en données binarys. Alors que la désérialisation signifie recréer un object à partir des données.

Lors de la sérialisation, vous uint8_t octets dans un vecteur uint8_t . Lors de la désérialisation, vous lisez les octets d’un vecteur uint8_t .

Il existe certainement des modèles que vous pouvez utiliser lors de la sérialisation de choses.

Chaque classe sérialisable doit avoir une fonction serialize(std::vector &binaryData) ou une fonction signée similaire qui écrira sa représentation binary dans le vecteur fourni. Ensuite, cette fonction peut transmettre ce vecteur aux fonctions de sérialisation de ses membres afin qu’ils puissent y écrire également.

Comme la représentation des données peut être différente selon les architectures. Vous devez savoir comment représenter les données.

Commençons par les bases:

Sérialisation des données entières

Écrivez simplement les octets dans le petit ordre endian. Ou utilisez la représentation varint si la taille est importante.

Sérialisation en petit ordre endien:

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

Désérialisation de l’ordre Little Endian:

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

Sérialisation des données à virgule flottante

Pour autant que je sache, l'IEEE 754 a un monopole ici. Je ne connais aucune architecture grand public qui utiliserait autre chose pour les flottants. La seule chose qui peut être différente est l'ordre des octets. Certaines architectures utilisent peu d'endian, d'autres utilisent l'ordre des octets big endian. Cela signifie que vous devez faire attention à l'ordre dans lequel vous entendez les octets à la réception. Une autre différence peut être la gestion des valeurs dénormales et infinies et NAN. Mais tant que vous évitez ces valeurs, vous devriez être d'accord.

Sérialisation:

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

La désérialisation le fait en arrière. Attention à l'ordre des octets de votre architecture!

Sérialisation des chaînes

Vous devez d'abord vous mettre d'accord sur un encodage. UTF-8 est commun. Puis stockez-le comme une longueur préfixée: d'abord vous stockez la longueur de la chaîne en utilisant une méthode que j'ai mentionnée ci-dessus, puis écrivez la chaîne octet par octet.

Sérialisation des tableaux.

Ils sont les mêmes que les cordes. Vous sérialisez d'abord un entier représentant la taille du tableau, puis sérialisez chaque object qu'il contient.

Sérialiser des objects entiers

Comme je l'ai déjà dit, ils devraient avoir une méthode de serialize qui ajoute du contenu à un vecteur. Pour désérialiser un object, il doit avoir un constructeur qui prend le stream d'octets. Il peut s'agir d'un istream mais dans le cas le plus simple, il peut s'agir simplement d'un pointeur de référence uint8_t . Le constructeur lit les octets qu'il souhaite dans le stream et configure les champs dans l'object. Si le système est bien conçu et sérialise les champs dans l'ordre des champs d'object, vous pouvez simplement passer le stream aux constructeurs du champ dans une liste d'initialisation et les désérialiser dans le bon ordre.

Sérialisation des graphes d'object

D'abord, vous devez vous assurer que ces objects sont vraiment quelque chose que vous voulez sérialiser. Vous n'avez pas besoin de les sérialiser si des instances de ces objects sont présentes sur la destination.

Maintenant, vous avez découvert que vous devez sérialiser cet object pointé par un pointeur. Le problème des pointeurs qu'ils ne valent que dans le programme qui les utilise. Vous ne pouvez pas sérialiser le pointeur, vous devez arrêter de les utiliser dans les objects. Au lieu de cela, créez des pools d'objects. Ce pool d'objects est essentiellement un tableau dynamic contenant des "cases". Ces cases ont un compte de référence. Le compte de référence non nul indique un object actif, zéro indique un emplacement vide. Ensuite, vous créez un pointeur intelligent similaire à shared_ptr qui ne stocke pas le pointeur sur l'object, mais l'index dans le tableau. Vous devez également convenir d'un index qui indique le pointeur nul, par exemple. -1.

Fondamentalement, nous avons remplacé les pointeurs par des index de tableau. Maintenant, lors de la sérialisation, vous pouvez sérialiser cet index de tableau comme d'habitude. Vous n'avez pas besoin de vous préoccuper de savoir où l'object sera en mémoire sur le système de destination. Assurez-vous simplement qu'ils ont le même pool d'objects.

Nous devons donc sérialiser les pools d'objects. Mais lesquels? Eh bien, lorsque vous sérialisez un graphique d'object, vous ne sérialisez pas simplement un object, vous sérialisez un système entier. Cela signifie que la sérialisation du système ne doit pas commencer par des parties du système. Ces objects ne doivent pas se soucier du rest du système, ils ont seulement besoin de sérialiser les index du tableau et c'est tout. Vous devez avoir une routine de sérialiseur système qui orchestre la sérialisation du système et parcourt les pools d'objects pertinents et les sérialiser tous.

A la réception, tous les tableaux et les objects sont désérialisés, recréant le graphe object souhaité.

Sérialisation des pointeurs de fonction

Ne stockez pas de pointeurs dans l'object. Avoir un tableau statique qui contient les pointeurs vers ces fonctions et stocker l'index dans l'object.

Étant donné que les deux programmes ont cette table compilée sur les tablettes, l'utilisation de l'index uniquement devrait fonctionner.

Sérialisation des types polymorphes

Comme j'ai dit que vous devriez éviter les pointeurs dans les types sérialisables et que vous devriez plutôt utiliser des index de tableaux, le polymorphism ne peut tout simplement pas fonctionner, car il nécessite des pointeurs.

Vous devez travailler avec les balises de type et les unions.

Versioning

En plus de tout ce qui précède. Vous souhaiterez peut-être que différentes versions du logiciel interagissent.

Dans ce cas, chaque object doit écrire un numéro de version au début de sa sérialisation pour indiquer la version.

Lors du chargement de l'object de l'autre côté, les objects les plus récents peuvent gérer les anciennes représentations, mais les plus anciens ne peuvent pas gérer les nouvelles représentations, ils doivent donc lancer une exception à ce sujet.

Chaque fois que quelque chose change, vous devez sauter le numéro de version.


Donc, pour en finir, la sérialisation peut être complexe. Mais heureusement, vous n'avez pas besoin de sérialiser tout dans votre programme, le plus souvent, seuls les messages de protocole sont sérialisés, ce qui est souvent une vieille structure. Donc, vous n'avez pas besoin des astuces complexes que j'ai mentionnées ci-dessus trop souvent.