08 fév. 2007

RAII !

Un cri de guerre, dirait-on - tout comme on l'aurait dit de YAGNI. Bien évidemment, il n'en est rien, car sous ce vocable barbare se cache tout simplement un acronyme décrivant une technique de gestion des ressources adaptée aux langages objets ou orientés objets.

J'ai déjà brièvement décrit le principe de cette technique dans un billet antérieur. Ce billet est plus complet, et fait le point sur les forces et les caractéristiques de cet idiome.

Comment ça marche ?

RAII, ça veut simplement dire Resource Acquisition Is Initialization. L'idée est lumineuse : l'une des notions importante dans tous les langages objet est la notion de portée - la portée d'une variable reflétant sa disponibilité et sa validité. Lorsque le programme quitte la portée de la variable, celle-ci est détruite (ou récupérée par le ramasse-miette). Au moment de sa destruction, son destructeur est appelé si la variable est une instance d'une classe définissant un destructeur.

L'idée est la suivante: pourquoi ne pas s'assurer que les ressources utilisées pendant le traitement seront automatiquement libérée lorsque nous n'en auront plus besoin ? En définissant une variable dont le but est la gestion de ces ressources et en jouant avec sa portée, il est tout à fait possible d'automatiser leur libération en se servant du destructeur qui sera appelé.

Qu'est-ce qu'une ressource ?

Les ressources se présentent sous plusieurs aspect, dont le seul point commun est qu'elles sont en nombre limité, et que leur utilisation induit généralement des appels couteux au système d'exploitation (cela n'est pas toujours vrai : on peut tout a fait imaginer une ressource purement applicative). Cela peut être une zone mémoire, un fichier, ou (sous Windows) un handle sur un objet GDI.

Plus généralement, il s'agit d'une entité que le programme doit d'abord acquérir avant de s'en servir, puis qu'il doit libérer une fois que l'opération qu'il devait exécuter est terminée. Le non respect de cette règle aboutit à un gaspillage des ressources du système, voire une perte de ces ressources (resource leak). Dans le cas d'une perte de mémoire, on parlera de memory leak, mais ce n'est qu'un type particulier de resource leak.

Exemple

Un exemple simple est celui d'une classe gérant un tampon de données[1]. Lors de la création de l'objet, le tampon est créé. A la destruction, il est automatiquement libéré, sans travail additionnel du programmeur.

class buffer
{
  char *m_buffer;
  std::size_t m_size;
public: buffer(std::size_t size) : m_size(size) { m_buffer = new char[m_size]; } ~buffer() { delete [] m_buffer; } // some other methods... };

Un autre exemple, lui aussi lié à la gestion de la mémoire: la classe std::auto_ptr<>, qui a déja fait l'objet d'un billet dans ces pages. Dans le cas de std::auto_ptr<>, le pointeur encapsulé est acquis pendant l'initialisation de l'objet, au lieu d'être créé. Toutefois, le principe est le même : à la destruction de l'auto_ptr, le pointeur encapsulé est détruit et la mémoire qu'il occupait est libérée.

En fait, les exemples sont nombreux : prenez la libraire C++ standard, et les types std::string, std::vector, std::stream, etc. La plupart des classes de cette librairie suivent cet idiome.

Quel est l'intérêt ?

Le tout premier intérêt est la simplicité d'écriture accrue : le client n'a pas à se soucier des problèmes liés à la libération de la ressource qu'il a créé. Le code est moins lourd - car il comporte moins de lignes. Prenons par exemple le cas suivant: une fonction lit dans un fichier un ensemble de valeurs, puis effectue un traitement sur ces valeurs avant de retourner un résultat. Sans RAII, le code pourrait ressembler à ceci (j'ai volontairement simplifié certains appels de fonction) :

int some_function(const std::string& filename)
{
  int file_id = open(filename.c_str(), ...);
  if (file_id <= 0) return 0;
unsigned int file_size = get_file_size(file_id); char *buffer = new char[file_size];
if (has_error(read(file_id, buffer, file_size)) { delete [] buffer; close(file_id); }
int result = some_algorithm(buffer, file_size);
delete [] buffer; close(file_id); return result; }

En utilisant l'idiome RAII, le code est simplifié et ressemblerait à ceci :

int some_function(const std::string& filename)
{
  file_object file(filename,...);
if (file.is_open()) { buffer temp_buffer(file.get_size());
if (has_error(file.read(temp_buffer))) { return 0; } return some_algorithm(temp_buffer); } return 0; }

On ne peut pas le nier, ce code est plus simple, et évite un certain nombre de problèmes bien connus (comme par exemple oublier de libérer l'une des ressources utilisée en cas d'erreur). De manière générale, on dit souvent que plus le code est long, plus les risques d'erreur sont nombreux. La conclusion qui vient naturellement à l'esprit est que si on trouve un moyen de réduire la masse de code source, il est plus que probable qu'on va diminuer d'autant le risque de bogue.

Puisque le destructeur est appelé dès lors qu'on quitte la portée de la variable, le code réagit aussi très bien aux exceptions qui peuvent être générée. Dans l'exemple ci-dessus, que se passe-t-il si la fonction some_algorithm() génère une exception ? Dans le premier exemple, le buffer reste alloué, et le fichier reste ouvert. Dans le second exemple, les ressources sont correctement libérée - le code est résistant aux exceptions.

Un autre bienfait apporté par cet idiome : le code étant plus court, il devient plus simple à lire. Encore une fois, on peut comparer les deux fonctions précédentes pour répondre à la question : laquelle de ces deux versions est la plus simple à comprendre ? La réponse est triviale, tant le second exemple est clair.

Conclusion

En utilisant l'idiome RAII, on gagne sur deux tableaux : facilité d'écriture, et facilité de lecture. La facilité d'écrire nous permet de réduire les risques de bogue. La facilité de lecture nous permet de mieux comprendre le code déjà écrit, et donc de simplifier d'autant la maintenance de l'application - les bogues restant seront donc plus rapidement corrigés.

Notes

[1] dans la vie réelle, on utilisera std::vector<> plutôt qu'une classe spécialement créée pour l'occasion et assurant elle même la gestion de la mémoire (on peut aussi encapsuler std::vector<> de manière à simplifier son interface pour cet usage)

Commentaires

1. Le mardi, mars 13 2007, 15:46 par fabrizio

à noté que l'on peu simplement implémenté l'idiome RAII en C# en implémentant l'interface IDisposable et utiliser le mot clés using .

class Foo : IDisposable
{
public void Dispose() { ... }
}



using ( Foo foo = new Foo() )
{
foo.Method();
}

2. Le jeudi, mars 15 2007, 12:00 par Emmanuel Deloget

Je dois avouer que mes connaissances en C# ne m'auraient pas permis d'écrire ça (je sais, c'est un langage que je dois apprendre...)

3. Le lundi, décembre 15 2008, 18:44 par loufoque
 ~buffer()
 {
   delete[] m_buffer;
 }

Et non un simple delete.

4. Le dimanche, février 8 2009, 12:54 par Emmanuel Deloget

En fait, le [] y était, mais puisqu'il s'agit d'un caractère spécial pour l'écriture wiki dans doctclear, il avait simplement disparu a l'affichage? Merci quand même de l'avaoir remarqué :)

Ajouter un commentaire

Les commentaires peuvent être formatés en utilisant une syntaxe wiki simplifiée.

Fil des commentaires de ce billet