Les pointeurs intelligents de Qt


précédentsommairesuivant

III. Comptez avec moi : combien y a-t-il de pointeurs intelligents dans Qt ?

III-A. Introduction

Ce vendredi 21 août 2009, avec l'intégration de Qt pour Symbian, nous avons eu un nouveau jouet : une classe de pointeurs intelligents, QScopedPointer. Un des auteurs de la classe, Harald Fernengel, nous la présente dans un billet sur son blog, traduit juste après.

Pourquoi avons-nous autant de classes de pointeurs intelligents ? Quelles sont les différences entre ces classes ?

Avant de pouvoir continuer plus avant, nous devons connaître les classes dont nous disposons. Les voici, dans l'ordre chronologique d'apparition.

Ça fait beaucoup, n'est-ce pas ?

Chaque cas a son utilité, et toutes, à l'exception d'une, sont encore valables aujourd'hui.

III-B. Pointeur ou données partagés ?

Commençons par attaquer directement une chose : lespointeurs partagés sont différents des données partagées.

Lors du partage de pointeurs, la valeur du pointeur et sa durée de vie sont protégées par la classe de pointeurs intelligents. En d'autres mots, le pointeur est invariant. Cependant, l'objet pointé est hors de portée. Complètement hors de portée : on ne peut pas savoir s'il est copiable, assignable..., ou non.

Maintenant, le partage des données implique que la classe de pointeur intelligent sache quelque chose des données partagées. En fait, une seule chose est importante : le fait que les données soient partagées. Le comment n'est pas important. Le fait que des pointeurs soient utilisés pour le partage des données est un total hors sujet. Par exemple, peu vous importe la manière dont les classes outil de Qt sont implicitement partagées ? Tout ce qui vous importe, c'est que les données soient partagées (ainsi réduisant la consommation de mémoire), sans que cela soit apparent.

III-C. Le référencement des pointeurs : léger ou fort ?

La différence entre une référence forte ou légère est l'existence ou non d'une classe de pointeur intelligent sur un pointeur donné qui garantit que l'objet ne sera pas supprimé. En d'autres termes, si vous avez ce pointeur intelligent, serez-vous toujours sûr qu'il restera valide ?

Quelques classes de pointeur ne le garantissent pas. Si elles ne le peuvent pas, leur seul objectif dans leur courte vie est de vous dire si l'objet a déjà été détruit ou non. Certaines classes peuvent vous proposer de transformer une référence légère par une référence forte, en garantissant sa non destruction.

III-D. Les classes Qt de pointeurs intelligents

III-D-1. QPointer

Il s'agit d'une classe de pointeur à référence légère, ne partageant que la valeur du pointeur, pas les données. Il ne peut opérer que sur des QObject et sur ses enfants. Ajoutée dans Qt 4.0, cette reprise de QGuardedPtr de Qt 3 (et de http://doc.trolltech.com/2.3/qguardedptr.html, de Qt 2) souffre d'un support peu fonctionnel de la constance, montrant ainsi son grand âge.

Un seul but : dire si, oui ou non, le QObject a été détruit. À l'inverse des versions 2 et 3, Qt 4 autorise le QObject à vivre dans plusieurs threads simultanément. Ce qui signifie que QPointer possède un défaut très sérieux : il laisse connaître l'existence d'un objet... mais sans aucune garantie quant à la ligne suivante ! Par exemple, ce bout de code peut se trouver en situation délicate.

 
Sélectionnez
QPointer<QObject> o = getObject();

// […]
if (!o.isNull())
    o->setProperty(“objectName”, “Object”);

Même si isNull retourne faux, il n'est pas possible d'obtenir la moindre garantie quant à la non destruction de l'objet avant la ligne suivante !

Par conséquent, il n'est possible d'utiliser cette classe que si vous pouvez certifier, par des méthodes externes, que l'objet existe encore. Par exemple, QWidget et ses enfants ne peuvent être créés, manipulés ou détruits que dans le thread de la GUI. Si votre code est exécuté dans le thread de la GUI, ou qu'il est dans un thread bloqué, alors QPointer est sécurisé.

III-D-2. QSharedDataPointer

Maintenant, une jolie petite classe. En fait, c'est la plus importante de toutes les classes de pointeurs intelligents. De loin. Grâce à son ingéniosité : elle fournit un partage implicite, avec un système de COWthread-safe. Elle requiert que votre classe possède un membre ref, offrant une fonction ref() qui augmente le compte de références, et une autre, unref(), qui l'abaisse et renvoie faux dès qu'il est nul. Si vous dérivez votre classe de QSharedData, c'est très exactement ce que vous aurez. De plus, un QSharedDataPointer a la même taille qu'un pointeur ! Ce qui signifie que vous pouvez remplacer vos pointeurs par ce type de pointeurs intelligents sans casser la compatibilité binaire.

Cette classe est la base de toutes les classes récentes de types de Qt, implicitement partagés, thread-safe, COW, comme QNetworkProxy. Si elle n'est pas utilisée dans d'autres classes de type comme QString, QByteArray ou QList, c'est que ces classes ont été développées bien avant que celle-ci soit en projet. Rien n'empêcherait leur portage vers ce type de pointeur intelligent.

Donc, ce type de pointeur est à référence forte, en partageant les données.

III-D-3. QExplicitlySharedDataPointer

Cette classe est très exactement semblable à QSharedDataPointer. À une différence près : un détachement n'est jamais causé implicitement. Avec QExplicitelySharedDataPointer, detach() doit être explicitement appelée. Ceci vous permet le développement de classes de données partagées explicitement, ce que Qt4 ne propose plus, contrairement au QMemArray de Qt3.

Cela vous permet, en sus, de mieux contrôler l'opération de détachement. En fait, si les classes outil de Qt devaient être réimplémentées avec des pointeurs intelligents, elles utiliseraient ce type de pointeur. Utiliser QExplicitelySharedDataPointer permet de retarder le détachement jusqu'à la dernière extrémité, pour s'assurer qu'aucun accès mémoire inutile n'arrive.

III-D-4. QtPatternist::AutoPtr

C'est une classe utilisée en interne au module QtXmlPatterns. Basiquement, c'est votre pointeur de base, standard, inintelligent. Ainsi, c'est une implémentation d'un pointeur à forte référence. Pourtant, il ne le partage pas.

Au début, la seule raison pour laquelle cette classe existe est que le module au module QtXmlPatterns utilise intensivement les exceptions en interne. Pour survivre aux exceptions lancées, en évitant les fuites de mémoire, un emballage de pointeur est indiqué.

III-D-5. QSharedPointer

Cette classe fut créée pour répondre à QtPatternist::AutoPtr. Lors du début de son écriture, elle devait être prête pour Qt 4.4, et remplacer la classe interne, alors perçue comme une mauvaise utilisation de QExplicitelySharedDataPointer : cette dernière n'était utilisée pour partager des données. Elle l'était pour partager des pointeurs. Les objets partagés n'étaient pas copiables. En recherchant un peu plus avant, on peut découvrir que cette classe est utilisée de la même manière dans QtScript, Phonon et Solid. En fait, QtScript l'avait introduite en ce but dans Qt 4.3.

III-D-6. QWeakPointer

Le compagnon de QSharedPointer implémente un contrôle renforcé sur le pointeur, en étant un pointeur intelligent à référence légère, ne partageant que le pointeur. Il travaille en tandem avec QSharedPointer : des QWeakPointer ne peuvent être créés que depuis des QSharedPointer, les premiers laissent savoir quand les seconds sont détruits.

Ils peuvent devenir QSharedPointer très facilement, d'une manière thread-safe. Réécrivons le code précédent.

 
Sélectionnez
QWeakPointer<Data> weak(getSharedPointer());

// […]
QSharedPointer<Data> ptr = weak;
if (!ptr.isNull())
    ptr->doSomething();

Ici, le QWeakPointer deviendra un QSharedPointer ou pas, mais cette décision est thread-safe. Si l'opération réussit, on peut être sûr que l'objet pointé ne sera pas supprimé.

Une belle fonctionnalité a été ajoutée dans Qt 4.6 : sa capacité à traquer les QObject, sans passer par un QSharedPointer. Cela peut être utilisé pour déterminer si un QObject a déjà été supprimé ou pas. Ainsi, il implémente un pointeur à référence légère. Cela vous semble familier ? C'est normal : on peut remplacer les vieillissants QPointer par d'autres pointeurs, plus récents, plus modernes, plus rapides, mais de taille différente.

III-D-7. QGuard

C'est une autre classe interne, ajoutée pour remplacer le lent QPointer. Mais elle est en état de flux : on ne sait pas encore si on va la garder, ni même si on va l'utiliser. De toute façon, elle est interne, cela ne vous préoccupe pas.

III-D-8. QScopedPointer

Voici le dernier né de la série : il implémente un emballage non partagé de pointeur à forte référence. Il a été créé pour les besoins particuliers du portage sur la plateforme Symbian et de ses exceptions : nous avions besoin de libérer les ressources sans mettre de bloc catch / try partout. Un pointeur à portée résout ce genre de problèmes d'une manière très RAII. En fait, QScopedPointer est un parfait remplaçant pour QtPatternist::AutoPtr. Ils implémentent les mêmes fonctionnalités, la version interne peut être supprimée.

Certains pensent que nous aurions pu utiliser QSharedPointer. En fait, non. Ce candidat prend, en mémoire, la place de deux pointeurs. Pour pouvoir garder la compatibilité binaire, il ne peut en prendre qu'un seul. C'est aussi la raison pour laquelle QScopedPointer a un destructeur personnalisé en tant que paramètre template, à l'opposé d'un paramètre au constructeur : il n'a pas les 4 ou 8 bytes de place pour stocker le destructeur personnalisé.

De plus, QScopedPointer implémente un comptage de référence atomique. Ce n'est pas très important qu'il soit atomique : le comptage de référence n'est pas très utile dans les cas gérés par QScopedPointer.

III-E. Et pourquoi pas le C++ 0x ? TR1 ? Boost ?

Certains ont suggéré d'utiliser std::shared_ptr ou std::tr1::shared_ptr. Ces gens ne regardent pas plus loin que le bout de leur nez : nous ne pouvons pas utiliser le C++0x. Il n'est même pas approuvé, et seul deux compilateurs le supportent : GCC 4.3, et Visual C++ 10 (encore en beta !). Il n'est même pas marrant de proposer d'utiliser le C++0x pour Qt pour le moment. Vous pouvez l'utiliser dans votre code, mais nous ne pouvons pas l'utiliser dans Qt.

TR1 est supporté par plus de compilateurs. Cependant, pas assez. Nous devons toujours supporter des compilateurs qui n'implémentent pas entièrement C++98, et des gens qui ne veulent pas changer leurs options de compilation ! Par exemple, la dernière version de Sun Studio (Sun Studio 12, avec CC 5.10) vient toujours avec une implémentation RogueWave de la STL pré-C++98 ! Si on regarde la comparaison de Sun entre libCstd (Rogue Wave) et STLPort 4, on peut voir qu'ils vont garder une implémentation vieille de plus de 11 ans par défaut. Mais le fait est, et nous devons travailler avec lui.

Ce qui signifie que le seul pointeur intelligent de la STL que nous pouvons utiliser est std::auto_ptr. Et même là, il y a des problèmes (la librairie de Rogue Wave n'implémente pas les templates membres).

Ce qui nous laisse Boost. Et il y a de belles choses dans Boost : boost::shared_ptr, boost::intrusive_ptr, boost::scoped_ptr, etc. En fait, il y a beaucoup de belles choses dans Boost. Très souvent, on voit des choses que l'on aimerait avoir dans Qt.

L'un des problèmes principaux de Boost, c'est son API, qui ne ressemble vraiment pas à celle de Qt. En d'autres termes, une API horrible. Mais ce n'est qu'une opinion, pas un fait. Même si l'API de Boost est intuitive pour certains, elle ne correspond pas à celle de Qt. Ce qui signifie que les gens qui veulent utiliser Boost avec Qt doivent apprendre les deux conventions de nommage, les deux façons de faire...

Nous pourrions emballer toute l'API de Boost. Cependant, en continuant ainsi, on peut voir facilement une chose : Qt perd une partie importante de sa technologie. Nous devons ainsi gérer n'importe quel problème qui leur incombe, à leur vitesse. Aussi, cela ajoute une dépendance à Qt, que nous ne pouvons pas justifier à cause de la compatibilité binaire. Un autre des grands problèmes.

Ainsi, Boost non plus n'est pas une solution.

III-F. Conclusion

Qt aurait-il trop de pointeurs intelligents ? Réellement ?

En fait, il n'y a que ceux-ci, en excluant les classes internes, et l'ancêtre QPointer :

Classe Description
QSharedDataPointer & QExplicitelySharedDataPointer Partage de données (implicite et explicite)
QSharedPointer Partage à comptage de référence de pointeurs à référence forte
QWeakPointer Partage à comptage de référence de pointeurs à référence faible
QScopedPointer & QScopedArrayPointer Emballage sans comptage de référence de pointeurs à référence forte

précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Copyright © 2009 Thiago Macieira & Harald Fernengel. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.