Les pointeurs intelligents de Qt

| Thiago Macieira | Harald Fernengel | Qt Labs |

Traducteur

Thibaut Cuvelier

Les pointeurs intelligents sont très nombreux dans Qt : pas moins de huit classes différentes sont disponibles. Tous ne sont pas publics, tous ne sont pas conseillés. Cela en laisse quand même une belle flopée.
Après la description de tous ces pointeurs intelligents, un approfondissement du dernier arrivé, QScopedPointer, sera effectué.

N'hésitez pas à commenter cet article !
Commentez

- Titre : Les pointeurs intelligents de Qt

- Auteurs : Thiago Macieira, Harald Fernengel, Qt Labs

- Traducteur : Thibaut Cuvelier

- Parution : 27 août 2009

- Mise à jour : 1 février 2020

- Licence : 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.

Sommaire

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

- I-A. Introduction

- I-B. Pointeurs ou données partagés ?

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

- I-D. Les classes Qt de pointeurs intelligents

- I-D-1. QPointer

- I-D-2. QSharedDataPointer

- I-D-3. QExplicitlySharedDataPointer

- I-D-4. QtPatternist::AutoPtr

- I-D-5. QSharedPointer

- I-D-6. QWeakPointer

- I-D-7. QGuard

- I-D-8. QScopedPointer

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

- I-F. Conclusion

- II. Introduction à QScopedPointer

- III. Un peu plus de pointages

- IV. L'article original

- V. Remerciements

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

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

- QPointer (4.0) ;

- QSharedDataPointer (4.0) ;

- QExplicitlySharedDataPointer (apparition : 4.3, documentation : 4.4) ;

- QtPatternist::AutoPtr (réservée à un usage interne, 4.4) ;

- QSharedPointer (4.5) ;

- QWeakPointer (4.5) ;

- QGuard (réservée à un usage interne, 4.6) ;

- QScopedPointer (4.6).

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

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

I-B - Pointeurs ou données partagés ?

Commençons par attaquer directement une chose : les pointeurs 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 utilitaires 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.

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

I-D - Les classes Qt de pointeurs intelligents

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

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

I-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 COW thread-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 Qt, avec des données implicitement partagées, thread-safe, COW, comme QNetworkProxy. Si elle n'est pas utilisée dans d'autres classes 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.

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

I-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 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é.

I-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 que 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.

I-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 :

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.

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

I-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érences atomique. Ce n'est pas très important qu'il soit atomique : le comptage de références n'est pas très utile dans les cas gérés par QScopedPointer.

I-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 seuls deux compilateurs le supportent : GCC 4.3, et Visual C++ 10 (encore en bêta !). 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 bibliothèque 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.

I-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érences de pointeurs à référence forte

QWeakPointer

Partage à comptage de références de pointeurs à référence faible

QScopedPointer & QScopedArrayPointer

Emballage sans comptage de références de pointeurs à référence forte

II - Introduction à QScopedPointer

En général, Qt s'occupe de l'embarrassante partie d'allocation et désallocation. Soit par les conteneurs implicitement partagés, soit par le modèle de relations parent-enfant. Mais, de temps à autre, on doit allouer une variable sur le tas. Et là, le stress commence. Où la supprimer ? Comment être sûr de ne pas laisser une fuite de mémoire ?

Une solution est née : QScopedPointer. Il supprimera l'objet dès qu'il sera hors de portée.

void foo()
{
QScopedPointer<int> i(new int(42));
…
if (someCondition)
return; // XX
…
} // … XX

L'entier pourra ici être supprimé à deux endroits, marqués par XX. Une nouvelle sortie dans notre code ne nous fera pas perdre la trace de cet entier : il sera automatiquement supprimé.

Comment accéder à l'objet pointé ? Deux opérateurs sont implémentés : operator* et operator->. On peut donc y accéder comme à tout pointeur.

QScopedPointer<int> i(new int(42));
*i = 43;

Quelques opérateurs ne sont pas implémentés, cela est voulu par le design.

QScopedPointer<int> i(new int(42));
i = new int(43); // Ne compilera pas
i.reset(new int(43)); // Correct

L'opérateur d'assignation n'existe pas.

Nous avons pensé que reset() était assez effrayant pour que le lecteur réalise bien que l'objet pointé est bien détruit.

Pour la même raison, l'opérateur T* n'existe pas. Il permet de récupérer le contenu du pointeur. Cela évite les énormités comme celle-ci :

int *foo()
{
QScopedPointer<int> i(new int(42));
…
return i; // Heureusement, ceci ne compile pas
}

Voyez-vous l'erreur ?

Au moment de retourner, l'objet serait détruit, le pointeur sortant de son champ. Nous aurions retourné un pointeur bancal, menant potentiellement à un crash désagréable.

Cependant, nous pouvons dire à QScopedPointer que son travail est fini et prendre possession de l'objet pointé par la fonction take(). Nous pouvons en prendre parti pour réécrire la fonction précédente.

int *foo()
{
QScopedPointer<int> i(new int(42));
…
if (someError)
return 0; // Notre entier est détruit ici
return i.take(); // Dès maintenant, notre objet sur le tas existe de lui-même
}

D'autres problèmes apparaissent. Et la mémoire allouée avec malloc() ? Et l'opérateur new[] pour les tableaux ?

Pour ces quelques cas a été introduit un second paramètre template qui définit le nettoyage.

QScopedPointer<int, QScopedPointerPodDeleter> pod(static_cast<int *>(malloc(sizeof int)));

QScopedPointerPodDeleter (POD : Plain Old Data, vieilles et simples données) appellera free() dès que l'objet sera hors de portée.

Il existe aussi QScopedArrayPointer, qui appellera delete[]. On propose aussi l'opérateur [] :

void foo()
{
QScopedArrayPointer<int> i(new int[10]);
i[2] = 42;
…
return; // Le tableau d'entiers sera détruit par delete[]
}

Si l'objet voit ses références comptées, QExplicitelySharedDataPointer peut être utilisé pour s'assurer que l'objet sera détruit quand son compteur sera à 0.

QScopedPointer et QExplicitelySharedDataPointer sont actuellement très utilisés dans la branche S60, mais ils devraient se généraliser dans la branche principale. Avec l'introduction de ces pointeurs intelligents, on pourra enlever des tonnes de code peu lisible sans devenir aérien – vu que toutes les fonctions sont inlinées, le binaire final sera identique à l'approche basée sur les new et delete.

III - Un peu plus de pointages

Utiliser QScopedPointer au lieu de std::auto_ptr ou de boost::scoped_ptr, ce n'est pas le syndrome du pas-inventé-ici. Au tout début de nos discussions sur cette API, nous nous sommes aperçus que la suppression lors de la sortie de portée n'était pas suffisante, nous avions aussi besoin de gestionnaires de suppression personnalisés. Un exemple : QBrush, qui nécessite un peu de magie pour supprimer l'objet privé. Les autres classes de pointeurs n'offraient pas cette fonctionnalité, elles ont donc été évincées.

Pourquoi faire un argument template des suppresseurs personnalisés, et pas simplement un pointeur sur fonction membre ? Nous avons eu deux raisons. Premièrement, nous ne voulions pas la moindre pénalité en comparaison des new et delete. Un nouveau pointeur et son appel dans le destructeur auraient été très coûteux. Deuxièmement, à cause de notre politique de compatibilité binaire, la taille de la classe devait être sizeof (void*). Ajouter un nouveau membre n'était pas possible.

Les nettoyeurs personnalisés peuvent être très utiles. Basiquement, on peut utiliser QScopedPointer pour tout nettoyage lors de la sortie de portée.

Toujours pas convaincu ? Regardez le code suivant, alors :

class ForwardDeclared;
extern ForwardDeclared *myFunc();
…
std::auto_ptr<ForwardDeclared> ptr(myFunc());

Il compile, mais ne s'exécute pas : vu que la classe n'est que déclarée en arrière, son destructeur ne sera pas appelé lors de la suppression ! QScopedPointer possède une protection : une erreur à la compilation en ce cas.

Pour s'assurer que vous puissiez écrire if (myScopedPointer), nous avons dû introduire un opérateur bool(). Mais cela aurait pu permettre l'écriture de int x = myScopedPointer;, à cause de la conversion implicite du booléen à l'entier. Heureusement, nous avons repéré ce problème lors d'une revue de l'API, ce dernier cas ne compilera pas.

Toujours pas convaincu ? Bonne nouvelle ! Tout le code est inliné, template, il n'y a donc pas de pénalité sur la taille de Qt ou sur le comportement à l'exécution si vous choisissez de les ignorer !

IV - L'article original

Les Qt Labs Blogs sont des blogs tenus par les développeurs de Qt, concernant les nouveautés, ou les utilisations un peu extrêmes du framework.

Nokia, Qt et leurs logos sont des marques déposées de Nokia Corporation en Finlande et/ou dans les autres pays. Les autres marques déposées sont détenues par leurs propriétaires respectifs.

Cet article est la traduction de ces trois billets :

1. Count with me: how many smart pointer classes does Qt have?, par Thiage Macieira ;

2. Introducing QScopedPointer, par Harald Fernengel ;

3. Some more pointing, par Harald Fernengel.

V - Remerciements

J'aimerais ici remercier yan et superjaja pour leur soutien dès le début ! Ainsi que RideKick pour sa relecture attentive !