Architecture logicielle & Développement

Les pièges de l'opérateur delete | 1 vote(s)

Tags: ,

The English version of this ticket is available here.

A première vue, tout parait simple. L'opérateur delete permet de libérer la mémoire précédemment allouée par l'opérateur new. Il exécute en outre le destructeur de l'objet désalloué de manière à libérer les ressources qu'il possède.

Bien évidemment, cela se gâte très vite lorsqu'on s'aperçoit qu'il existe un autre opérateur delete, auquel le standard C++ donne le nom de delete array (noté delete []le premier étant nommé delete object). delete array a pour fonction de désallouer les bloc mémoire alloués par new array (new []).

Le premier constant est le suivant : si un bloc mémoire a été alloué avec new object, il faut le désallouer avec delete object, tandis que si le bloc a été alloué avec new array, il faut le désallouer avec delete array. Se tromper d'opérateur delete n'aura aucun effet à la compilation mais peut avoir des effets dévastateurs au moment de l'exécution, la norme spécifiant clairement que le comportement du programmé étant non défini dans ce cas (5.3.5 §2). Il s'agit donc du premier piège de l'opérateur delete.

Le second piège est plus difficile à cerner : si on détruit un objet dont le type est incomplet et que ce type possède un destructeur non trivial, le comportement du programme est de nouveau non défini (5.3.5 §5). Dans la plupart des cas, le programme fonctionnera mais le compilateur aura pris soin de générer un destructeur trivial pour le type incomplet, ce qui en retour empêchera la libération des ressources possédée par l'objet détruit[1].

Un troisième piège concerne le fonctionnement de l'opérateur delete : la norme ne requiert pas que celui-ci laisse inchangé le pointeur passé en paramètre (5.3.5 §4). Elle laisse le soin à l'implémentation de faire ce choix. Cela signifie qu'il est interdit de comparer la valeur d'un pointeur détruit - une telle valeur est d'ailleurs considérée comme invalide (5.3.5 §4). De fait, le comportement de ce code est non spécifié :

// le vecteur v contient des pointeurs qui peuvent
// être répétés plusieurs fois. je souhaite éviter 
// d'effacer deux fois le même élément (comportement non défini)
std::sort(v.begin(), v.end());
T *p = NULL;
for (size_t i=0; i<v.size(); i++) {
  if (v.at(i) != p) {
     delete v.at(i);
     p = v.at(i);
  }
}

Dans sa FAQ, Bjarne Stroustrup explique que ce paragraphe permet au vendeurs de mettre à 0 le pointeur passé en paramètre à l'opérateur delete - ce qui n'est finalement pas conseillé, sans compter que cela n'est pas systématiquement possible[2].

Pour terminer cette liste (non exhaustive) de pièges, un quatrième point se doit d'être soulevé. Lorsque delete array est appelé pour détruire un tableau, il va appeler le destructeur de chacune des entrées du tableau dans l'ordre inverse de l'appel des constructeurs (5.3.5 §6). Il va ainsi détruire le dernier élément du tableau, puis l'avant dernier, etc ... jusqu'à terminer par le premier. Il est donc interdit dans un destructeur de faire référence à d'autres éléments du tableau. A contrario, on notera que l'ordre de destruction des éléments dans un std::vector<> n'est pas spécifié (cette destruction est effectuée via un appel explicite au destructeur, de manière à ne pas désallouer la mémoire déja allouée).

Afin de remonter le moral des troupes, je me dois de vous donner quand même un bonne nouvelle, lié à une fonctionnalité souvent ignorée : selon 5.3.5 §2, la destruction d'un pointeur NULL n'a aucun effet. Plus besoin donc d'écrire if (pointer != NULL) delete pointer;, votre implémentation du standard C++ se charge de faire le test à votre place !

On le voit, malgré son apparente simplicité, l'utilisation de l'opérateur delete se doit d'être rigoureuse si l'on souhaite éviter ses nombreux pièges - les risques d'autodestruction du programme se faisant plus forte lorsqu'on ne prends pas certaines de ses caractéristiques en compte.

Notes

[1] on reviendra sur ce problème dans un billet ultérieur traitant de std::auto_ptr<>

[2] on notera que les fonctions du type delete_safe(T*&), qui mettent a NULL le paramètre après l'avoir détruit sont soumises aux mêmes problème: que se passe-t-il lorsqu'on passe en paramètre le retour d'une fonction ?

Trackbacks

Aucun trackback.

Les trackbacks pour ce billet sont fermés.

Commentaires

1. Le vendredi 28 juillet 2006 à 17:48, par Christophe MOUSTIER

Gravatar

pourrais-tu préciser le document d'où la référence [5.3.5 §2] et consors proviennent ?

2. Le dimanche 30 juillet 2006 à 13:32, par Emmanuel Deloget

Gravatar

Ah oui. Une telle information pourrait être utile...

Les références proviennent du standard ISO définissant le langage C++ (ISO/IEC 14882:1998, qui peut être commandé au "monde en tique" (librarie parisienne spécialisée) ici : www.lmet.fr/fiche.cgi?_IS... )

Ce standard a été modifié en 2003 et est de nouveau en cours de modification. Le dernier draft publié est disponible ici: www.open-std.org/jtc1/sc2... .

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.