Les pointeurs intelligents de Qt


précédentsommaire

IV. 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 à autres, 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.

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

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

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

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

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

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

 
Sélectionnez
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 [] :

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

V. Un peu plus de pointage

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.

 
Sélectionnez
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 ignore !


précédentsommaire

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.