J'ai réfléchi depuis l'autre jour sur cette notion de shared_ptr (qui n'est qu'un « fichu stupide pointeur », mais géré d'une manière particulière) et je te propose une autre utilisation possible : s'en servir, effectivement, comme une zone mémoire partagée où tous les possesseurs peuvent écrire dedans pour implémenter une structures de tableaux persistants. :-)
Le principe de l'encodage sera écrit en OCaml (ce sera plus simple pour moi, et je laisse la traduction en C++ à toi ou un autre). Avant cela, je vais revenir sur la notion de constructeurs de type dans ce langage pour que tu puisses faire le lien.
Quand j'ai présenté la définition du type des listes chaînées, je l'ai écrit ainsi :
type'aliste=|Nil|Consof'a*'aliste
Une expression de la forme Cons of ... est en réalité un « pointeur » en lecture seule sur la structure décrite ensuite. On pourrait l'illustrer ainsi :
Ceci dit, ce langage propose également la notion de zone mémoire accessible en lecture et écriture avec la notion de référence (mot clé ref).
leti=ref1;;vali:intref={contents=1}(* lecture avec l'opérateur préfixe !*)!i;;-:int=1(* écriture avec l'opérateur infixe := *)i:=2;;-:unit=()(* le type unit est équivalent à void *)(* nouvelle lecture *)!i;;-:int=2
Avant d'en venir au cœur de ce commentaire, il me faut présenter rapidement une autre structure représentant une zone mémoire accessible en lecture et écriture, commune à tous les langages impératifs : les tableaux (ou array).
lettab=[|0;1;2;3|];;valtab:intarray=[|0;1;2;3|]Array.gettab0;;-:int=0Array.settab04;;-:unit=()Array.gettab0;;-:int=4(* on peut faire la même chose avec cette notation *)tab.(0)<-5;;-:unit=()tab.(0);;-:int=5tab;;-:intarray=[|5;1;2;3|]
Ces présentations préliminaires étant faites, venons en à l'objectif principal : coder une structure de tableaux persistants en recourant à une zone mémoire accessible en lecture-écriture commune à chacun d'eux. On pourrait faire cela en recopiant le tableau à chaque écriture puis en renvoyant la copie modifiée, mais ce serait vite coûteux en mémoire. L'idée pour obtenir la persistance à moindre coût va être celle qui est derrière les gestionnaires de versions (git, svn, mercurial...) : une écriture sera vu comme un patch et on conservera en mémoire la structure des diff.
On commence par définir le type polymorphe 'a t de nos tableaux :
On a deux types mutuellement récursifs : le type 'a data qui pointe en lecture soit sur un tableau, soit sur un triplet correspondant à une écriture atomique sur un tableau, qui lui est un pointeur en lecture-écriture sur une structure 'a data.
Tout le code sur cette structure va reposer sur la fonction reroot qui modifie le graphe des pointeurs afin de toujours rentrer dans la structure par un pointeur en lecture sur un tableau 'a array. Autrement dit : on applique tous les patchs sur le tableau d'origine pour présenter le tableau résultant.
letrecrerootpa=match!pawith|Arra->a|Diff(i,v,pa')->(* pa est vu comme un diff de pa', on commence par rerooter sur lui *)leta=rerootpa'in(* on applique le patch atomique *)letold=a.(i)ina.(i)<-v;pa:=Arra;(* maintenant c'est pa' qui est vu comme un diff de pa *)pa':=Diff(i,old,pa);(* on retourne le « véritable » tableau correspondant à pa *)a
À partir de cette fonction utilitaire, on peut définir les fonctions usuelles sur les tableaux à partir de celles sur les array.
letlengthpa=Array.length(rerootpa)letgetpai=(rerootpa).(i)letiterfpa=Array.iterf(rerootpa)(* ici la fonction f que l'on itère prend deux paramètres : l'indice et la valeur *)letiterifpa=Array.iterif(rerootpa)
Pour la fonction set, il faut prendre soin de rerooter sur le tableau que l'on veut patcher puis rajouter le diff dans la structure :
On peut également, par exemple, rajouter des fonctions de conversions vers et à partir des tableaux non persistants ainsi qu'une fonction de copie qui oublie tout l'historique des patchs.
Maintenant, à la manière de la POO, on peut masquer une partie de la mécanique interne ainsi que le type implémentant une telle structure à l'utilisateur. On passe pour cela par le système de modules en n'exposant pas certains détails dans l'interface.
(* l'interface du module : * - la définition du type n'est pas exposée * - le type 'a data non plus * - la fonction reroot reste privée *)moduletypePA=sigtype'atvalinit:int->(int->'a)->'atvallength:'at->intvalget:'at->int->'avalset:'at->int->'a->'atvaliteri:(int->'a->unit)->'at->unitvaliter:('a->unit)->'at->unitvalto_array:'at->'aarrayvalof_array:'aarray->'atvalcopy:'at->'atend(* le code complet du module *)modulePersistent_Array:PA=structtype'at='adatarefand'adata=|Arrof'aarray|Diffofint*'a*'atletinitnf=ref(Arr(Array.initnf))letrecrerootpa=match!pawith|Arra->a|Diff(i,v,pa')->leta=rerootpa'inletold=a.(i)ina.(i)<-v;pa:=Arra;pa':=Diff(i,old,pa);aletlengthpa=Array.length(rerootpa)letto_array=rerootletof_arraya=ref(Arra)letcopypa=ref(Arr(Array.copy(rerootpa))letgetpai=(rerootpa).(i)letiterifpa=Array.iterif(rerootpa)letiterfpa=Array.iterf(rerootpa)letsetpaiv=leta=rerootpainletold=a.(i)ina.(i)<-v;letres=ref(Arra)inpa:=Diff(i,old,res);resend
Illustration du module :
modulePA=Persistent_Array;;lettab=PA.of_array[|0;1;2;3|];;valtab:intPA.t=<abstr>(* la représentation est abstraite pour l'utilisateur *)lettab'=PA.settab04;;valtab':intPA.t=<abstr>(* le tableau est bien persistant *)PA.to_arraytab;;-:intarray=[|0;1;2;3|]PA.to_arraytab';;-:intarray=[|4;1;2;3|]
Il reste à noter, tout de même, que la fonction reroot qui réorganise la structure des pointeurs n'est pas thread safe (les merge peuvent créer des conflits ;-), donc la structure ne l'est pas non plus. On pourrait, par exemple, la rendre thread safe en rajoutant un verrou que l'on pose au début de reroot et qu'on libère une fois la réorganisation effectuée.
Voilà, à mon avis, une structure qu'il peut être intéressant d'implémenter avec des shared_ptr plutôt qu'avec de « simples » pointeurs — si j'en ai bien compris le principe. Et encore une fois : la persistance des données, c'est le bien ! :-)
Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.
[^] # Re: Donc pour résumer...
Posté par kantien . En réponse à la dépêche C++17, Genèse d’une version mineure. Évalué à 3. Dernière modification le 09 octobre 2016 à 14:44.
J'ai réfléchi depuis l'autre jour sur cette notion de
shared_ptr(qui n'est qu'un « fichu stupide pointeur », mais géré d'une manière particulière) et je te propose une autre utilisation possible : s'en servir, effectivement, comme une zone mémoire partagée où tous les possesseurs peuvent écrire dedans pour implémenter une structures de tableaux persistants. :-)Le principe de l'encodage sera écrit en OCaml (ce sera plus simple pour moi, et je laisse la traduction en C++ à toi ou un autre). Avant cela, je vais revenir sur la notion de constructeurs de type dans ce langage pour que tu puisses faire le lien.
Quand j'ai présenté la définition du type des listes chaînées, je l'ai écrit ainsi :
Une expression de la forme
Cons of ...est en réalité un « pointeur » en lecture seule sur la structure décrite ensuite. On pourrait l'illustrer ainsi :Ceci dit, ce langage propose également la notion de zone mémoire accessible en lecture et écriture avec la notion de référence (mot clé
ref).Avant d'en venir au cœur de ce commentaire, il me faut présenter rapidement une autre structure représentant une zone mémoire accessible en lecture et écriture, commune à tous les langages impératifs : les tableaux (ou
array).Ces présentations préliminaires étant faites, venons en à l'objectif principal : coder une structure de tableaux persistants en recourant à une zone mémoire accessible en lecture-écriture commune à chacun d'eux. On pourrait faire cela en recopiant le tableau à chaque écriture puis en renvoyant la copie modifiée, mais ce serait vite coûteux en mémoire. L'idée pour obtenir la persistance à moindre coût va être celle qui est derrière les gestionnaires de versions (
git,svn,mercurial...) : une écriture sera vu comme unpatchet on conservera en mémoire la structure desdiff.On commence par définir le type polymorphe
'a tde nos tableaux :On a deux types mutuellement récursifs : le type
'a dataqui pointe en lecture soit sur un tableau, soit sur un triplet correspondant à une écriture atomique sur un tableau, qui lui est un pointeur en lecture-écriture sur une structure'a data.Tout le code sur cette structure va reposer sur la fonction
rerootqui modifie le graphe des pointeurs afin de toujours rentrer dans la structure par un pointeur en lecture sur un tableau'a array. Autrement dit : on applique tous lespatchssur le tableau d'origine pour présenter le tableau résultant.À partir de cette fonction utilitaire, on peut définir les fonctions usuelles sur les tableaux à partir de celles sur les
array.Pour la fonction
set, il faut prendre soin de rerooter sur le tableau que l'on veut patcher puis rajouter lediffdans la structure :On peut également, par exemple, rajouter des fonctions de conversions vers et à partir des tableaux non persistants ainsi qu'une fonction de copie qui oublie tout l'historique des patchs.
Maintenant, à la manière de la POO, on peut masquer une partie de la mécanique interne ainsi que le type implémentant une telle structure à l'utilisateur. On passe pour cela par le système de modules en n'exposant pas certains détails dans l'interface.
Illustration du module :
Il reste à noter, tout de même, que la fonction
rerootqui réorganise la structure des pointeurs n'est pas thread safe (lesmergepeuvent créer des conflits ;-), donc la structure ne l'est pas non plus. On pourrait, par exemple, la rendre thread safe en rajoutant un verrou que l'on pose au début dererootet qu'on libère une fois la réorganisation effectuée.Voilà, à mon avis, une structure qu'il peut être intéressant d'implémenter avec des
shared_ptrplutôt qu'avec de « simples » pointeurs — si j'en ai bien compris le principe. Et encore une fois : la persistance des données, c'est le bien ! :-)Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.