Architecture logicielle & Développement

std::auto_ptr<> et fuite de mémoire | 2 vote(s)

Tags: ,

std::auto_ptr<> est l'un des outils les plus controversé de la librairie standard C++ (la dénomination officielle est CSL, pour C++ Standard Library). Le but de cet outil est de gérer à la place du programmeur la durée de vie d'un objet alloué. Un objet auto_ptr<> encapsule un pointeur, le destructeur de l'auto_ptr<> détruisant l'objet pointé.

Les conditions d'utilisation correcte d'un objet auto_ptr<> sont très limités. Par exemple, il est impossible de créer un vecteur de ce type d'objet. Ce problème est lié à la façon dont l'opérateur d'assignation a été redéfini : il transfert la propriété du pointeur, ce qui rend auto_ptr<> incompatible avec les besoins des différents conteneurs de la CSL (où la sémantique de l'opérateur d'assignation implique une copie de l'objet).

Outre ce problème, auto_ptr<> a une autre particularité. Considérons le code suivant:

#include <memory>
void an_object::a_method() { std::auto_ptr<another_object> an_instance(new another_object()); an_instance->do_something(); an_instance->do_something_else(); }

Lorsque la méthode a_method() se termine, l'objet an_instance se termine aussi car sa portée est limitée à cette méthode. De fait, l'objet pointé sous-jacent est lui aussi détruit. A noter que le destructeur de auto_ptr<> ne fait appel qu'à l'opérateur delete, et non pas delete-array. Par conséquent, il n'est pas souhaitable d'initialiser un auto_ptr<> avec un tableau créé par l'opérateur new-array - toutefois, comme nous le rappelle Bjarne Stroustrup, std::vector<> a été prévu pour remplir cet office.

Considérons maintenant le code suivant:

// file object.h  - - - - - - - - - - - - - - - - - - - - - - - - -
#include <memory> 
class AnotherObject;
class Object { std::auto_ptr<AnotherObject> mAnotherObject; public: Object(); // other methods };
// file object.cpp - - - - - - - - - - - - - - - - - - - - - - - - - #include "Object.h" #include "AnotherObject.h"
Object::Object() : mAnotherObject(new AnotherObject()) { // blah }

Dans l'esprit du programmeur, la destruction d'une instance Object détruit aussi automatiquement l'instance sous jacente de AnotherObject, puisque celle ci est encapsulée dans l'auto_ptr<>. Le destructeur n'ayant rien à faire - rien à détruire - il a été omis (le compilateur fournira un destructeur par défaut).

Alors pourquoi ce code ne fonctionne-t-il pas ? La mémoire liée à l'instance d'AnotherObject n'est pas libérée, et on a une fuite de mémoire.

La réponse tient en peu de mots: non respect du standard C++. Dans un précédent billet, je vous ait décrit le comportement de l'opérateur delete lorsqu'il doit détruire un objet d'un type incomplet. Bien que la norme ne définisse pas explicitement le comportement que le compilateur a adopté, la majorité des compilateurs du marché fournissent au type incomplet un destructeur trivial implicite en suivant (12.4 §3) If a class has no user-declared destructor, a destructor is implicitely declared. An implicitely-declared destructor is an inline public member of its class. - si une classe n'a pas de constructeur déclaré par l'utilisateur, un destructeur est implicitement déclaré. Un destructeur implicitement déclaré est un membre public inline de sa classe.

Pour mieux comprendre l'importance du mot inline dans ce cadre, examinons ce que produit le compilateur lorsqu'on utlise une instance de Object dans le cadre suivant:

#include "Object.h"
void foo() { Object anObject; anObject.doSomething(); // blah blah blah // avant de sortir de la méthode, appel automatique du destructeur // anObject.~Object(); }

Décomposons maintenant les opérations effectuée lors de la detruction de anObject:

anObject.~Object() 
   --> mAnotehrObject.~auto_ptr() 
      --> delete _pointee;

Jusqu'ici, tout semble logique et rien ne choque véritablement. Mais rappelez vous que ~Object() est inline. Par essence, puisque que auto_ptr<> est une classe template, son destructeur est lui aussi inline. Au final, le compilateur génère simplement la dernière ligne (delete _pointee;), qui devrait appeler le destructeur de AnotherObject.

Mais qui ne le fera pas, parce que la seule définition de AnotherObject connue du compilateur à ce moment est celle qui est au début de Object.h: class AnotherObject;. Il s'agit d'une pré-déclaration, et une pré-déclaration crée un type incomplet. Lorsqu'on appelle le destructeur d'un type incomplet, les compilateurs récents ne génèrent pas d'erreur - ils se contente souvent d'un warning[1] et donnent à ce type incomplet un destructeur trivial, qui ne fait rien. Ainsi la destruction de AnotherObject n'appelle pas le véritable destructeur de AnotherObject, d'où les fuites de mémoire.

Comment se prémunir de ce problème ? En suivant bien evidemment le standard C++. Pour celà, il faut que la définition de AnotherObject soit connue au moment de sa destruction. On peut par exemple enlever la pré-déclaration et la remplacer par l'inclusion de AnotherObject.h - la solution n'est guère statisfaisante, car on perd le bénéfice d'une pré-déclaration (moins de dépendances, compilation plus rapide, etc). On peut aussi inclure AnotherObject.h chaque fois qu'on inclut Object.h - cette solution, bien que possible théoriquement, impose de trop lourdes contraintes sur le programmeur.

La solution la plus simple est en fait de faire en sorte que le destructeur de Object ne soit pas inline. Pour celà, une seule solution: définir le destructeur de Object dans object.cpp, et ne pas oublier d'inclure AnotherObject.h dans object.cpp. On s'assure ainsi de deux choses:

  • le compilateur ne crée pas de destructeur implicite pour Object
  • le type AnotherObject sera completement défini au moment ou le compilateur compiler le destructeur.

Ainsi, il n'y a plus de problème de destruction de type incomplet.

// file object.h - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#include <memory>
class AnotherObject;
class Object { std::auto_ptr<AnotherObject> mAnotherObject; public: Object(); ~Object(); // declaration du destructeur // autres méthodes };
// file object.cpp - - - - - - - - - - - - - - - - - - - - - - - - - - #include "Object.h" #include "AnotherObject.h"
Object::Object() : mAnotherObject(new AnotherObject()) { // blah }
// définition du destructeur - il ne peut pas être inliné // le type AnotherObject est complet, on peut donc le // détruire correctement. Object::~Object() { }

Notes

[1] Visual C++ 7.1 (VS.NET 2003) génère le warning C4150, warning de niveau 2

Trackbacks

Aucun trackback.

Les trackbacks pour ce billet sont fermés.

Commentaires

1. Le mardi 6 mars 2007 à 14:15, par fabrizio

Gravatar

attention le lien vers l'article "Dans un précédent billet" pointe sur localhost.

localhost/dotclear/index....

2. Le mercredi 7 mars 2007 à 10:47, par Emmanuel Deloget

Gravatar

Merci ! C'est corrigé maintenant.

Ajouter un commentaire

Si votre navigateur est compatible, vous pouvez vous aider de la barre d'outils placée au-dessus de la zone de saisie pour enrichir vos commentaires.