Le module core. (Partie 2)

Sérialiser des objets consiste à écrire chaque variable pour ces objets dans une archive, cette archive pourrait très bien sérialiser les données en plusieurs formats. (binaire, texte, xml, etc…)

Les attributs peuvent être de deux types : des nombres ou bien des caractères, si l’attribut est un objet, alors, il sera redécomposé jusqu’à arrivé aux types fondamentaux, au final ce sont donc juste des types fondamentaux qui seront écris dans l’archive, et non pas des objets, ceci est le but essentiel de la sérialisation.

Ceci évite de devoir récupérer chaque valeur de chaque attribut pour chaque objet pour les écrire et ensuite les lire, avec des méthodes get et set.

On va juste passer une archive à une méthode qui va passer toutes les variables des objets à l’archive, et si la variable est un objet alors il sera redécomposé.

Je ne vais pas utiliser le formatage des flux standard du langage c++ pour écrire et lire les variables fondamentales, je vais donc utiliser mon propre format qui sera le suivant :

Si la variable est un nombre, alors on écrit le nombre suivit d’un \n pour le séparer de la variable suivante.

Si la variable est un caractère, alors le caractère sera écrit le caractère suivit d’un \n pour le séparer du caractère suivante.

Si la variable est une liste de nombres ou bien de caractères, alors, on écrit la taille de la liste suivit d’un \n et ensuite on écrit tout les éléments de la liste.

Ecrire des variables dans une archive.

Pour écrire je vais utiliser une classe que je vais appeler OTextArchive, et lui passer un flux mémoire ou bien un flux sur un fichier dans lequel écrire les données :

Pour la structure has_typedef_key, on verra après à quoi elle sert, elle pour le moment retenez juste qu’elle permet de savoir si la bibliothèque doit sérialiser aussi les variables de la classe dérivée ou pas si l’objet est polymorphique.

Ensuite, il va falloir distinguer le type de variable à sérialiser pour cela je vais utiliser les classes de traits de la STL ainsi que l’idiome de programmation SFINAE.

Les variable simples.

On les écris simplement dans l’archive.

Les énumérations :

Pareil que pour les variables simple :

Les listes :

Pour les listes j’écris la taille de la liste, et j’écris ensuite chaque élément de la liste :

Les chaînes de caractères :

Pour les chaînes de caractère je place chaque caractère de la chaîne de caractère dans une liste et j’écris cette liste :

Pour les objets je vais appeler une méthode serialize que je vais redéfinir dans la classe de l’objet, cette méthode va prendre l’archive en paramètre, et l’utilisateur n’aura plus qu’à passer les variables membres de la classe de l’objet à écrire dans l’archive avec l’operateur().

Les pointeurs :

Il reste un dernier type, les pointeurs, on ne peut pas écrire les adresses des objets car celles-ci ne seront plus forcément les même lors l’écriture et de la lecture.

Il va falloir aussi éviter d’écrire un objet deux fois, si deux classe possède un pointeur sur le même objet.

La solution est d’écrire un id unique et l’adresse mémoire de la variable dans une std::map, chaque id servira de clé pour récupérer l’adresse mémoire de la variable, et si l’adresse mémoire n’est pas présente dans la std::map ça veut dire que la variable n’a pas encore été écrite dans l’archive, si la variable a déjà été écrite dans l’archive, j’écris juste l’id de la variable sinon j’écris l’id de la variable et son contenu, ainsi à la lecture, si l’id de la variable est déjà présent dans la std::map, il n’y aura qu’à rechercher l’adresse de l’objet et l’affecter au pointeur avec un reinterpret_cast, sinon, il faudra allouer le pointeur et lui affecter le contenu de la variable.

Pour stocker les adresses j’utilise le type unisgned long long int pour assurer une compatibilité avec les plateformes 64 bits qui utilisent des adresses en 64 bits.

Voici ce que cela donne donc à l’écriture : (Je prend soin de vérifier si le pointeur n’est pas null, sinon, j’écris juste -1 ainsi à la lecture, le pointeur ne sera pas lu car il était null lors de l’écriture)

L’objet prend comme id le nombre d’objet écris dans l’archive.

Pour tout les pointeurs sur les autres types, le procéder à suivre est exactement le même! (Il n’y a que pour les listes ou l’on est pas obligé de spécialiser leux méthodes différente car la sémentique de la liste est la même que celle des éléments de la liste, ce qui est d’ailleurs très bien en c++, contrairement au java qui cache la sémantique des éléments de la liste et ou il aurait fallut utiliser arraycopy.

Je laisse l’écriture des autres types de contenaire de la STL ainsi que l’écriture des types wstring et wchar en guise d’exercice.

Par contre je ne ferai pas d’écriture pour les tableaux classique qui peuvent poser problème surtout pour les tableaux à plusieurs dimensions, je vais donc forcer une utilisation de la classe std::array qui est conseillée en c++14 et qui de plus possède une capsule RAII.

Lire des objets dans une archive :

Lecture des types fondamentaux.

Pour la lecture il va falloir utiliser plus de méthode car celle-ci pourrait poser problème dans les case suivants :

Si le caractère est un caractère spécial du style \n, il sera ignoré lors de la lecture.

On va donc devoir utiliser les fonctions non formaté pour les types char et unsigned char, je vais donc spécialiser les fonctions template pour ces deux types qui posent problème, il ne faut pas oublié de lire aussi le \n qui sépare les données des variables dans le flux : (je le lis et le stocke dans une variable bidon appelé space que je n’utilise pas)

Ceci permet d’écrire n’importe quel type de variable dans une archive. (ici j’ai utilisé une archive au format texte car c’est ce qui est le plus utilisé, et c’est portable)

Mais il reste un cas à gérer, ce sont les objets polymorphiques, en effet quel variables doit on écrire dans l’archive, celles de la classe de base uniquement, celle de la classe dérivée uniquement, ou bien celles des 2 classes ?

Si il faut  écrire les attributs de la classe dérivée, il faut aussi, appeler la fonction serialize de la classe dérivée. (et non celle de la classe de base)

Pour faire cela, j’ai décidé de faire un système très similaire à celui de boost qui offre un max de flexibilité en permettant d’enregistrer les classes avec une macro, mais mon système permet en plus de gérer l’héritage multiple et utilise du c++ moderne.

Il solution plus simple et qui m’a déjà été proposée est la redéfinition des operateurs de flux, mais cette solution n’est pas pratique car il faut redéfinir les deux opérateurs de flux pour la lecture et l’écriture, pas pratique lorsque la lecture et l’écriture sont symétrique et au pire des cas si l’écriture et la lecture sont assymétrique on pourra toujours testé si c’est une archive d’écriture ou de lecture dans la méthode serialize.

La seconde solution est de faire un système de réflexibilité et d’utiliser son propre système de RTTI, ce qui est plus lent à l’exécution mais offre 100 fois plus de flexibilité, et puis, un système de sauvegarde n’a pas forcément besoin d’être super rapide mais plutôt flexible!

Faire un système de réflexibilité :

La réflexibilité consiste à enregister chaque fonction ainsi que chaque types dans une factory, et d’appeler la fonction en recherchant sont type à l’exécution.

Etant donné que il faut recherché la fonction à l’exécution il nous font un tableau sur un pointeur de fonction, ça tombe bien, on peut en créer un facilement grâce à la classe FastDelegate. 😀

La factory aura deux std::map, une comportant le type à l’exécution de la classe et le type de la classe de base de la fonction membre et l’autre comportant le type de la fonction membre et un foncteur sur la fonction membre à appeler.

On ne fera la réflexibilité que lorsqu’on en aura besoin (contrairement au java qui l’implémente sur toutes ses classes), on y gagnera donc en rapidité.

Je vais donc juste créer une classe que je vais appelé factory, et toutes les classes réflexibles devront hériter de cette classe et enregister les fonctions des classes réflexibles.

Je vais aussi faire un allocateur, pour pouvoir alloué un pointeur sur un objet d’une classe  en envoyant son type à l’exécution.

On voit donc ici que les tableaux sur nos pointeur de fonctions sont super utile les seules fonction que l’on ne pourra pas enregistrées sont celles qui ont un type de retour. (Mais la méthode serialize ne renvoie rien donc ce n’est pas nécessaire dans mon cas.)

Pour éviter de devoir utiliser plus de code, j’ai également défini quelques macros.

Une macro pour enregistrer un type, et une autre pour enregistrer une fonction.

Pour rendre une classe réflexible il suffit alors maintenant de la faire héritée de Factory en passant le nom de la classe en paramètre template comme ceci : (Et d’enregistrer les fonctions membre de la classe dans le constructeur par exemple, vu qu’il faut de toute façon un objet pour appeler la fonction)

Je fais précéder chaque fonction ici, du préfixe vt pour indiquer que la fonction est réflexible, ou encore, virtuelle template. (Car en général c’est dans ce cas là que l’on doit faire recours à la réflexibilité)

class C : public Factory<C> {

C() {

REGISTER_FUNC(CVTFUNCTIONARGTYPE, serialize, ArgType, C, C,(ARGTYPE), this, ArgT())

}

template <typename Arg>

void vtfunction (Arg) {

}

}

Il y a pas beaucoup de paramètres à envoyer à la macro : le 1er  est juste un identifiant unique pour la fonction membre de la classe à enregistrer, le second est le nom de la fonction à enregistrer (sans le préfixe vt), le troisième est un identifiant pour le type du paramètre  de la fonction, car, le paramètre  peut être de n’importe quel type,  les deux paramètres suivants sont les types de la classe de base et de la classe dérivée, ici on indique deux fois le type de la classe de base car, on est dans le constructeur de la classe de base, le paramètre suivant est la liste des types des arguments de la fonction à enregistrer, l’avant dernier paramètre est un pointeur sur un objet de la classe réflexible (celui sur lequel appeler la fonction réflexible), et enfin, le dernier paramètre est la valeur de l’argument de la fonction réflexible.

Cela fait beaucoup de paramètres à passé à la macro mais on a une flexibilité totale, on pourrait par exemple enregistrer chaque fonction membres de même nom de plusieurs classes dérivée pour chaque type possible pour les paramètres template des fonctions membre (si les fonctions membres sont template) et ensuite appeler la bonne fonction template à l’exécution en fonction du type de l’objet dérivé à l’exécution! (Et c’est ce que je vais faire par la suite!)

Ceci permet de régler le problème avec les fonctions virtuelles qui ne peuvent pas être template.

Pour appeler la fonction réflexible il suffira de faire :

C c;

Factory<C>::callFunction(typeid(c).name(), serialize, ArgType,&c,ArgT());

Le 1er paramètre est le type de l’objet à l’exécution, le second le nom de la fonction membre à appeler, le troisième le type de l’argument, l’avant dernier l’adresse de l’objet sur lequel appeler la fonction réflexible et le dernier, la valeur du paramètre de la fonction réflexible.

On peut également alloué un object de la classe dérivée à l’aide d’un allocateur au cas ou l’on ne connaît pas le type dynamique de l’objet à lire en compilation. (Ce qui est le cas lors de la lecture)

Grâce à cela on va pouvoir enregistrer chaque fonction template manuellement pour tout les types dérivant d’une classe de base pour tout les types d’archives passés à la fonction serialize, mais il va falloir faire deux classes supplémentaire, la première sera la classe de l’objet qui va savoir comment sérialiser l’objet polymorphique (la classe Serializer) et la seconde sera juste une interface qui va contenir des fonctions pour allouer les objets à lire, appeler les fonctions enregistrée, etc…, bref, cette classe n’est autre que la classe Registered.

Chaque classe de base devra hériter de la classe Registered en lui passant le nom de la classe en paramètre template, la classe de base ainsi que les classe dérivées n’aurons ainsi plus qu’à redéfinir la fonction virtuelle template vtserialize.

J’ai créer une macro pour simplifier l’enregistrement des fonctions de serialization à appeler, il n’y aura qu’à appeler cette macro dans le main comme ont le fait aussi avec la librairie boost.

Voici comment utiliser cette macro pour par exemple, définir une relation d’héritage pour un classe B et D :

EXPORT_CLASS_GUID(BD, B, D)

Le 1er paramètre est un identifiant unique définissant la relation d’héritage, le second paramètre est le type de la classe de base, et le dernier paramètre, le type de la classe dérivée.

Si héritage multiple il y a alors il faut exporter toutes les classes de base :

EXPORT_CLASS_GUID(B1D, B1, D)

EXPORT_CLASS_GUID(B2D, B2, D)

Finalement le code source des classes OTextArchive et ITextArchive est tout bête, il suffit de tester si l’objet est polymorphique en utilisant l’idiome SFINAE (c’est le cas si il contient un typedef que j’ai défini dans la classe Registered), et si l’objet est polymorphique on appelle la bonne version de l’opérateur() pour écrire ou lire l’objet, et comme les fonctions réflexible sont enregistrée grâce à la macro EXPORT_CLASS_GUID, il n’y a plus qu’à appeler la bonne fonction en fonction du type de l’archive et du type de l’objet à l’exécution.

Il faut écrire le type dynamique de l’objet dans l’archive, pour pouvoir réalloué le pointeur en lecture. (De ce fait l’utilisateur n’est pas obligé de réalloué le pointeur lui même.)

En lecture il faut gérer un cas supplémentaire : celui des classes abstraite car les classes abstraites ne peuvent pas être instanciée directement, il faut donc éviter une erreur en compilation on utilisant SFINAE et std::is_abstract.

Voilà, cet article ne traite pas des cas particulier avec std::unique_ptr et std::shared_ptr mais le principe est simple, il suffit d’appeler l’opérateur get pour récupérer le pointeur  lors de l’écriture et d’utiliser la fonction reset pour changer le pointeur lors de la lecture.

Voilà donc ce qui tourne une autre grosse page du module core de ODFAEG, la page suivante traitera de la dernière grosse partie du module core de ma bibliothèque, la chargement et le stockage des ressources externes.

Leave a comment