web-dev-qa-db-fra.com

Comprendre la signification du terme et du concept - RAII (l'acquisition de ressources est l'initialisation)

Pourriez-vous, développeurs C++, nous donner une bonne description de ce qu'est le RAII, pourquoi il est important et s'il peut ou non avoir une pertinence pour d'autres langages?

Je fais sais un peu. Je crois que cela signifie "L'acquisition de ressources est l'initialisation". Cependant, ce nom ne correspond pas à ma compréhension (peut-être incorrecte) de ce qu'est RAII: j'ai l'impression que RAII est un moyen d'initialiser des objets sur la pile de telle sorte que, lorsque ces variables sortent du champ d'application, les destructeurs seront automatiquement être appelé provoquant le nettoyage des ressources.

Alors pourquoi cela ne s'appelle-t-il pas "utiliser la pile pour déclencher le nettoyage" (UTSTTC :)? Comment vous rendez-vous à "RAII"?

Et comment pouvez-vous faire quelque chose sur la pile qui provoquera le nettoyage de quelque chose qui vit sur le tas? Y a-t-il également des cas où vous ne pouvez pas utiliser RAII? Vous est-il déjà arrivé de souhaiter une collecte des ordures? Au moins un garbage collector que vous pourriez utiliser pour certains objets tout en laissant d'autres être gérés?

Merci.

109
Charlie Flowers

Alors pourquoi cela ne s'appelle-t-il pas "utiliser la pile pour déclencher le nettoyage" (UTSTTC :)?

RAII vous dit quoi faire: Acquérir votre ressource chez un constructeur! J'ajouterais: une ressource, un constructeur. UTSTTC n'est qu'une application de cela, RAII est bien plus.

La gestion des ressources craint. Ici, la ressource est tout ce qui doit être nettoyé après utilisation. Les études de projets sur de nombreuses plateformes montrent que la majorité des bogues sont liés à la gestion des ressources - et c'est particulièrement mauvais sous Windows (en raison des nombreux types d'objets et d'allocateurs).

En C++, la gestion des ressources est particulièrement compliquée en raison de la combinaison d'exceptions et de modèles (style C++). Pour un aperçu sous le capot, voir GOTW8 ).


C++ garantit que le destructeur est appelé si et seulement si le constructeur a réussi. En s'appuyant sur cela, RAII peut résoudre de nombreux problèmes désagréables dont le programmeur moyen pourrait ne même pas être au courant. Voici quelques exemples au-delà de "mes variables locales seront détruites à mon retour".

Commençons par une classe FileHandle trop simpliste utilisant RAII:

class FileHandle
{
    FILE* file;

public:

    explicit FileHandle(const char* name)
    {
        file = fopen(name);
        if (!file)
        {
            throw "MAYDAY! MAYDAY";
        }
    }

    ~FileHandle()
    {
        // The only reason we are checking the file pointer for validity
        // is because it might have been moved (see below).
        // It is NOT needed to check against a failed constructor,
        // because the destructor is NEVER executed when the constructor fails!
        if (file)
        {
            fclose(file);
        }
    }

    // The following technicalities can be skipped on the first read.
    // They are not crucial to understanding the basic idea of RAII.
    // However, if you plan to implement your own RAII classes,
    // it is absolutely essential that you read on :)



    // It does not make sense to copy a file handle,
    // hence we disallow the otherwise implicitly generated copy operations.

    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;



    // The following operations enable transfer of ownership
    // and require compiler support for rvalue references, a C++0x feature.
    // Essentially, a resource is "moved" from one object to another.

    FileHandle(FileHandle&& that)
    {
        file = that.file;
        that.file = 0;
    }

    FileHandle& operator=(FileHandle&& that)
    {
        file = that.file;
        that.file = 0;
        return *this;
    }
}

Si la construction échoue (à une exception près), aucune autre fonction membre - pas même le destructeur - n'est appelée.

RAII évite d'utiliser des objets dans un état invalide. il facilite déjà la vie avant même d'utiliser l'objet.

Voyons maintenant les objets temporaires:

void CopyFileData(FileHandle source, FileHandle dest);

void Foo()
{
    CopyFileData(FileHandle("C:\\source"), FileHandle("C:\\dest"));
}

Il y a trois cas d'erreur à gérer: aucun fichier ne peut être ouvert, un seul fichier peut être ouvert, les deux fichiers peuvent être ouverts mais la copie des fichiers a échoué. Dans une implémentation non RAII, Foo devrait gérer les trois cas explicitement.

RAII libère les ressources qui ont été acquises, même lorsque plusieurs ressources sont acquises dans une même instruction.

Maintenant, agrégons quelques objets:

class Logger
{
    FileHandle original, duplex;   // this logger can write to two files at once!

public:

    Logger(const char* filename1, const char* filename2)
    : original(filename1), duplex(filename2)
    {
        if (!filewrite_duplex(original, duplex, "New Session"))
            throw "Ugh damn!";
    }
}

Le constructeur de Logger échouera si le constructeur de original échoue (car filename1 N'a pas pu être ouvert), le constructeur de duplex échoue (parce que filename2 n'a pas pu être ouvert), ou l'écriture dans les fichiers à l'intérieur du corps du constructeur de Logger échoue. Dans tous ces cas, le destructeur de Logger sera pas appelé - nous ne pouvons donc pas compter sur le destructeur de Logger pour libérer les fichiers. Mais si original a été construit, son destructeur sera appelé lors du nettoyage du constructeur Logger.

RAII simplifie le nettoyage après une construction partielle.


Points négatifs:

Points négatifs? Tous les problèmes peuvent être résolus avec RAII et les pointeurs intelligents ;-)

RAII est parfois compliqué lorsque vous avez besoin d'une acquisition différée, poussant des objets agrégés sur le tas.
Imaginez que l'enregistreur ait besoin d'une SetTargetFile(const char* target). Dans ce cas, le handle, qui doit toujours être membre de Logger, doit résider sur le tas (par exemple dans un pointeur intelligent, pour déclencher la destruction du handle de manière appropriée.)

Je n'ai jamais vraiment souhaité de ramassage des ordures. Quand je fais du C #, je ressens parfois un moment de bonheur que je n'ai pas besoin de faire attention, mais bien plus encore, je manque tous les jouets sympas qui peuvent être créés par la destruction déterministe. (utiliser IDisposable ne suffit pas.)

J'ai eu une structure particulièrement complexe qui aurait pu bénéficier du GC, où des pointeurs intelligents "simples" provoqueraient des références circulaires sur plusieurs classes. Nous nous sommes débrouillés en équilibrant soigneusement les pointeurs forts et faibles, mais chaque fois que nous voulons changer quelque chose, nous devons étudier un grand tableau des relations. Le GC aurait pu être mieux, mais certains des composants contenaient des ressources qui devraient être publiées dès que possible.


Une note sur l'exemple FileHandle: Il n'était pas destiné à être complet, juste un exemple - mais s'est avéré incorrect. Merci à Johannes Schaub de l'avoir signalé et à FredOverflow de l'avoir transformé en une solution C++ 0x correcte. Au fil du temps, je me suis installé avec l'approche documentée ici .

131
peterchen

Il y a d'excellentes réponses, alors j'ajoute juste des choses oubliées.

0. RAII concerne les portées

RAII, c'est à la fois:

  1. acquérir une ressource (quelle que soit la ressource) dans le constructeur et la désacquérir dans le destructeur.
  2. avoir le constructeur exécuté lorsque la variable est déclarée et le destructeur exécuté automatiquement lorsque la variable sort du champ d'application.

D'autres ont déjà répondu à ce sujet, donc je ne développerai pas.

1. Lors du codage en Java ou C #, vous utilisez déjà RAII ...

MONSIEUR JOURDAIN: Quoi! Quand je dis: "Nicole, apporte-moi mes pantoufles et donne-moi mon bonnet de nuit", c'est de la prose?

MAÎTRE DE PHILOSOPHIE: Oui, monsieur.

MONSIEUR JOURDAIN: Depuis plus de quarante ans, je parle en prose sans rien en savoir, et je vous suis très reconnaissant de m'avoir appris cela.

- Molière: le gentleman de la classe moyenne, acte 2, scène 4

Comme M. Jourdain l'a fait avec la prose, C # et même Java les gens utilisent déjà RAII, mais de manière cachée. Par exemple, le code Java Java (qui est écrit de la même manière en C # en remplaçant synchronized par lock):

void foo()
{
   // etc.

   synchronized(someObject)
   {
      // if something throws here, the lock on someObject will
      // be unlocked
   }

   // etc.
}

... utilise déjà RAII: L'acquisition du mutex se fait dans le mot-clé (synchronized ou lock), et la désacquisition se fera à la sortie de la portée.

Il est si naturel dans sa notation qu'il ne nécessite presque aucune explication, même pour les personnes qui n'ont jamais entendu parler de RAII.

L'avantage du C++ par rapport à Java et C # ici, c'est que tout peut être créé en utilisant RAII. Par exemple, il n'y a pas d'équivalent intégré direct de synchronized ni lock en C++, mais nous pouvons toujours les avoir.

En C++, il s'écrirait:

void foo()
{
   // etc.

   {
      Lock lock(someObject) ; // lock is an object of type Lock whose
                              // constructor acquires a mutex on
                              // someObject and whose destructor will
                              // un-acquire it 

      // if something throws here, the lock on someObject will
      // be unlocked
   }

   // etc.
}

qui peut être facilement écrit de la manière Java/C # (en utilisant des macros C++):

void foo()
{
   // etc.

   LOCK(someObject)
   {
      // if something throws here, the lock on someObject will
      // be unlocked
   }

   // etc.
}

2. Le RAII a d'autres utilisations

LAPIN BLANC: [chantant] Je suis en retard/je suis en retard/Pour une date très importante./Pas le temps de dire "Bonjour"./Au revoir./Je suis en retard, je suis en retard, je suis en retard.

- Alice au pays des merveilles (version Disney, 1951)

Vous savez quand le constructeur sera appelé (à la déclaration d'objet), et vous savez quand son destructeur correspondant sera appelé (à la sortie de la portée), vous pouvez donc écrire du code presque magique avec mais une ligne. Bienvenue au pays des merveilles C++ (du moins, du point de vue d'un développeur C++).

Par exemple, vous pouvez écrire un objet compteur (je laisse cela comme un exercice) et l'utiliser simplement en déclarant sa variable, comme l'objet de verrouillage ci-dessus a été utilisé:

void foo()
{
   double timeElapsed = 0 ;

   {
      Counter counter(timeElapsed) ;
      // do something lengthy
   }
   // now, the timeElapsed variable contain the time elapsed
   // from the Counter's declaration till the scope exit
}

qui bien sûr, peut être écrit, encore une fois, la manière Java/C # en utilisant une macro:

void foo()
{
   double timeElapsed = 0 ;

   COUNTER(timeElapsed)
   {
      // do something lengthy
   }
   // now, the timeElapsed variable contain the time elapsed
   // from the Counter's declaration till the scope exit
}

3. Pourquoi C++ manque-t-il finally?

[CRIER] C'est le final compte à rebours!

- Europe: le compte à rebours final (désolé, j'étais hors devis, ici ... :-)

La clause finally est utilisée en C #/Java pour gérer l'élimination des ressources en cas de sortie de portée (soit via une return ou une exception levée).

Les lecteurs de spécifications astucieux auront remarqué que C++ n'a pas de clause finally. Et ce n'est pas une erreur, car C++ n'en a pas besoin, car RAII gère déjà l'élimination des ressources. (Et croyez-moi, il est plus facile d'écrire un destructeur C++ que d'écrire la bonne clause Java finally, ou même la méthode Dispose correcte d'un C #).

Pourtant, parfois, une clause finally serait cool. Pouvons-nous le faire en C++? Oui, nous pouvons! Et encore avec une utilisation alternative de RAII.

Conclusion: RAII est plus qu'une philosophie en C++: c'est C++

RAII? CECI IS C++ !!!

- Commentaire indigné du développeur C++, copié sans vergogne par un obscur roi de Sparte et ses 300 amis

Lorsque vous atteignez un certain niveau d'expérience en C++, vous commencez à penser en termes de RAII, en terme de exécution automatisée des constructeurs et des destructeurs.

Vous commencez à penser en termes de portées, et le { et } les caractères deviennent les plus importants de votre code.

Et presque tout convient parfaitement en termes de RAII: sécurité d'exception, mutex, connexions à la base de données, demandes de base de données, connexion au serveur, horloges, poignées de système d'exploitation, etc., et enfin, mais pas des moindres, la mémoire.

La partie base de données n'est pas négligeable, car si vous acceptez de payer le prix, vous pouvez même écrire un "programmation transactionnelle"style, en exécutant des lignes et des lignes de code jusqu'à ce que vous décidiez, à la fin, si vous souhaitez valider toutes les modifications, ou, si cela n'est pas possible, que toutes les modifications soient annulées (tant que chaque ligne satisfait au moins la garantie d'exception forte (voir la deuxième partie de cette article de Herb's Sutter pour la programmation transactionnelle).

Et comme un puzzle, tout rentre.

RAII fait tellement partie de C++, C++ ne pourrait pas être C++ sans lui.

Cela explique pourquoi les développeurs C++ expérimentés sont si amoureux de RAII, et pourquoi RAII est la première chose qu'ils recherchent lorsqu'ils essaient un autre langage.

Et cela explique pourquoi le Garbage Collector, tout en étant une magnifique technologie en soi, n'est pas si impressionnant du point de vue d'un développeur C++:

  • RAII gère déjà la plupart des cas traités par un GC
  • Un GC traite mieux que RAII avec des références circulaires sur des objets gérés purs (atténués par des utilisations intelligentes de pointeurs faibles)
  • Un GC est toujours limité à la mémoire, tandis que RAII peut gérer tout type de ressource.
  • Comme décrit ci-dessus, RAII peut faire beaucoup, beaucoup plus ...
42
paercebal
16
Mitch Wheat

RAII utilise la sémantique des destructeurs C++ pour gérer les ressources. Par exemple, considérons un pointeur intelligent. Vous avez un constructeur paramétré du pointeur qui initialise ce pointeur avec l'adresse de l'objet. Vous allouez un pointeur sur la pile:

SmartPointer pointer( new ObjectClass() );

Lorsque le pointeur intelligent sort de la portée, le destructeur de la classe du pointeur supprime l'objet connecté. Le pointeur est alloué par pile et l'objet - alloué par tas.

Il y a certains cas où RAII n'aide pas. Par exemple, si vous utilisez des pointeurs intelligents de comptage de références (comme boost :: shared_ptr) et créez une structure de type graphique avec un cycle, vous risquez de faire face à une fuite de mémoire car les objets d'un cycle vont empêcher la libération les uns des autres. La collecte des ordures aiderait contre cela.

10
sharptooth

Je voudrais l'exprimer un peu plus fortement que les réponses précédentes.

RAII, L'acquisition de ressources est une initialisation signifie que toutes les ressources acquises doivent être acquises dans le contexte de l'initialisation d'un objet. Cela interdit l'acquisition de ressources "nues". La raison en est que le nettoyage en C++ fonctionne sur la base des objets, et non sur la base des appels de fonction. Par conséquent, tout le nettoyage doit être effectué par des objets et non par des appels de fonction. En ce sens, C++ est plus orienté objet que par ex. Java. Java est basé sur les appels de fonction dans les clauses finally.

9
MSalters

Je suis d'accord avec cpitis. Mais j'aimerais ajouter que les ressources peuvent être autre chose que de la mémoire. La ressource peut être un fichier, une section critique, un thread ou une connexion à une base de données.

Il est appelé l'acquisition de ressources est l'initialisation car la ressource est acquise lorsque l'objet contrôlant la ressource est construit. Si le constructeur échoue (c'est-à-dire en raison d'une exception), la ressource n'est pas acquise. Ensuite, une fois l'objet hors de portée, la ressource est libérée. c ++ garantit que tous les objets de la pile qui ont été construits avec succès seront détruits (cela inclut les constructeurs des classes de base et des membres même si le constructeur de la super classe échoue).

La raison d'être de RAII est de sécuriser l'exception d'acquisition de ressources. Que toutes les ressources acquises soient correctement libérées, peu importe où une exception se produit. Cependant, cela dépend de la qualité de la classe qui acquiert la ressource (cela doit être sûr d'exception et c'est difficile).

8
iain

Le problème avec la collecte des ordures est que vous perdez la destruction déterministe qui est cruciale pour RAII. Une fois qu'une variable est hors de portée, c'est au garbage collector que l'objet sera récupéré. La ressource détenue par l'objet continuera à être conservée jusqu'à ce que le destructeur soit appelé.

7
Mark Ransom

RAII provient de l'allocation des ressources est l'initialisation. Fondamentalement, cela signifie que lorsqu'un constructeur termine l'exécution, l'objet construit est entièrement initialisé et prêt à l'emploi. Cela implique également que le destructeur libérera toutes les ressources (par exemple la mémoire, les ressources du système d'exploitation) appartenant à l'objet.

Comparé aux langages/technologies récupérés (par exemple Java, .NET), C++ permet un contrôle total de la vie d'un objet. Pour un objet alloué à la pile, vous saurez quand le destructeur de l'objet sera appelé (lorsque l'exécution sortira du cadre), chose qui n'est pas vraiment contrôlée en cas de garbage collection. Même en utilisant des pointeurs intelligents en C++ (par exemple boost :: shared_ptr), vous saurez que lorsqu'il n'y a aucune référence à l'objet pointé, le destructeur de cet objet sera appelé.

4
Cătălin Pitiș

Et comment pouvez-vous faire quelque chose sur la pile qui provoquera le nettoyage de quelque chose qui vit sur le tas?

class int_buffer
{
   size_t m_size;
   int *  m_buf;

   public:
   int_buffer( size_t size )
     : m_size( size ), m_buf( 0 )
   {
       if( m_size > 0 )
           m_buf = new int[m_size]; // will throw on failure by default
   }
   ~int_buffer()
   {
       delete[] m_buf;
   }
   /* ...rest of class implementation...*/

};


void foo() 
{
    int_buffer ib(20); // creates a buffer of 20 bytes
    std::cout << ib.size() << std::endl;
} // here the destructor is called automatically even if an exception is thrown and the memory ib held is freed.

Lorsqu'une instance de int_buffer vient à exister, elle doit avoir une taille et alloue la mémoire nécessaire. Quand il sort du domaine, son destructeur est appelé. Ceci est très utile pour des choses comme les objets de synchronisation. Considérer

class mutex
{
   // ...
   take();
   release();

   class mutex::sentry
   {
      mutex & mm;
      public:
      sentry( mutex & m ) : mm(m) 
      {
          mm.take();
      }
      ~sentry()
      {
          mm.release();
      }
   }; // mutex::sentry;
};
mutex m;

int getSomeValue()
{
    mutex::sentry ms( m ); // blocks here until the mutex is taken
    return 0;  
} // the mutex is released in the destructor call here.

Y a-t-il également des cas où vous ne pouvez pas utiliser RAII?

Non, pas vraiment.

Vous est-il déjà arrivé de souhaiter une collecte des ordures? Au moins un garbage collector que vous pourriez utiliser pour certains objets tout en laissant d'autres être gérés?

Jamais. La récupération de place ne résout qu'un très petit sous-ensemble de gestion dynamique des ressources.

3
Rob K

Il y a déjà beaucoup de bonnes réponses ici, mais je voudrais juste ajouter:
Une explication simple de RAII est qu'en C++, un objet alloué sur la pile est détruit à chaque fois qu'il sort du domaine. Cela signifie qu'un destructeur d'objets sera appelé et pourra effectuer tous les nettoyages nécessaires.
Cela signifie que si un objet est créé sans "nouveau", aucune "suppression" n'est requise. Et c'est aussi l'idée derrière les "pointeurs intelligents" - ils résident sur la pile et enveloppent essentiellement un objet basé sur le tas.

2
E Dominique

RAII est un acronyme pour Resource Acquisition Is Initialization.

Cette technique est très unique au C++ en raison de leur prise en charge à la fois des constructeurs et des destructeurs et presque automatiquement des constructeurs qui correspondent à ces arguments passés ou dans le pire des cas le constructeur par défaut est appelé & destructeurs si l'explicité fournie est appelée sinon celle par défaut qui est ajouté par le compilateur C++ est appelé si vous n'avez pas écrit explicitement un destructeur pour une classe C++. Cela se produit uniquement pour les objets C++ qui sont auto-gérés - ce qui signifie qu'ils n'utilisent pas le magasin gratuit (mémoire allouée/désallouée à l'aide des opérateurs C++ nouveaux, nouveaux []/supprimer, supprimer []).

La technique RAII utilise cette fonctionnalité d'objet géré automatiquement pour gérer les objets qui sont créés sur le tas/la librairie en demandant explicitement plus de mémoire en utilisant new/new [], qui devrait être explicitement détruit en appelant delete/delete [] . La classe de l'objet géré automatiquement encapsulera cet autre objet qui est créé sur la mémoire de tas/magasin libre. Par conséquent, lorsque le constructeur de l'objet autogéré est exécuté, l'objet encapsulé est créé sur la mémoire du tas/magasin libre et lorsque le descripteur de l'objet autogéré sort de la portée, le destructeur de cet objet autogéré est appelé automatiquement dans lequel l'encapsulé l'objet est détruit à l'aide de la suppression. Avec les concepts OOP, si vous encapsulez de tels objets dans une autre classe dans une portée privée, vous n'auriez pas accès aux membres et aux méthodes des classes encapsulées et c'est la raison pour laquelle les pointeurs intelligents (aka gérer les classes ) sont conçus pour. Ces pointeurs intelligents exposent l'objet enveloppé en tant qu'objet tapé au monde extérieur et là-bas en permettant d'appeler tous les membres/méthodes dont l'objet mémoire exposé est composé. Notez que les pointeurs intelligents ont différentes saveurs en fonction de différents besoins. Vous devriez vous référer à la programmation Modern C++ par Andrei Alexandrescu ou à la mise en œuvre de la bibliothèque shared_ptr.hpp de la bibliothèque boost (www.boostorg) pour en savoir plus. J'espère que cela vous aidera à comprendre RAII.

1
techcraver