Qu'entend-on par acquisition de ressources est initialisation (RAII)?


Réponses:


374

C'est un nom vraiment terrible pour un concept incroyablement puissant, et peut-être l'une des choses numéro 1 que les développeurs C ++ manquent lorsqu'ils passent à d'autres langages. Il y a eu un peu de mouvement pour essayer de renommer ce concept en tant que gestion des ressources liée à la portée , bien qu'il ne semble pas encore avoir pris le dessus.

Quand nous disons «ressource», nous ne parlons pas seulement de mémoire - il peut s'agir de descripteurs de fichiers, de sockets réseau, de descripteurs de base de données, d'objets GDI ... contrôler leur utilisation. L'aspect «lié à la portée» signifie que la durée de vie de l'objet est liée à la portée d'une variable, donc lorsque la variable sort de la portée, le destructeur libère la ressource. Une propriété très utile de ceci est qu'elle permet une plus grande sécurité d'exception. Par exemple, comparez ceci:

RawResourceHandle* handle=createNewResource();
handle->performInvalidOperation();  // Oops, throws exception
...
deleteResource(handle); // oh dear, never gets called so the resource leaks

Avec le RAII

class ManagedResourceHandle {
public:
   ManagedResourceHandle(RawResourceHandle* rawHandle_) : rawHandle(rawHandle_) {};
   ~ManagedResourceHandle() {delete rawHandle; }
   ... // omitted operator*, etc
private:
   RawResourceHandle* rawHandle;
};

ManagedResourceHandle handle(createNewResource());
handle->performInvalidOperation();

Dans ce dernier cas, lorsque l'exception est levée et que la pile est déroulée, les variables locales sont détruites, ce qui garantit que notre ressource est nettoyée et ne fuit pas.


2
@the_mandrill: J'ai essayé ideone.com/1Jjzuc ce programme. Mais il n'y a pas d'appel destructeur. Le tomdalling.com/blog/software-design/… indique que C ++ garantit que le destructeur d'objets sur la pile sera appelé, même si une exception est levée. Alors, pourquoi le destructeur n'a pas exécuté ici? Ma ressource a-t-elle été divulguée ou ne sera-t-elle jamais libérée ou libérée?
Destructor

8
Une exception est levée, mais vous ne l'attrapez pas, l'application se termine donc. Si vous encapsulez avec un essai {} catch () {} alors cela fonctionne comme prévu: ideone.com/xm2GR9
the_mandrill

2
Je ne sais pas trop si Scope-Boundc'est le meilleur choix de nom ici, car les spécificateurs de classe de stockage ainsi que la portée déterminent la durée de stockage d'une entité. Le fait de le restreindre à la portée est peut-être une simplification utile, mais il n'est pas précis à 100%
SebNag

125

Ceci est un idiome de programmation qui signifie brièvement que vous

  • encapsuler une ressource dans une classe (dont le constructeur acquiert généralement - mais pas nécessairement ** - la ressource, et son destructeur la libère toujours)
  • utiliser la ressource via une instance locale de la classe *
  • la ressource est automatiquement libérée lorsque l'objet devient hors de portée

Cela garantit que quoi qu'il arrive pendant que la ressource est en cours d'utilisation, elle sera finalement libérée (que ce soit en raison d'un retour normal, de la destruction de l'objet contenant ou d'une exception levée).

C'est une bonne pratique largement utilisée en C ++, car en plus d'être un moyen sûr de gérer les ressources, cela rend également votre code beaucoup plus propre car vous n'avez pas besoin de mélanger le code de gestion des erreurs avec la fonctionnalité principale.

* Mise à jour: "local" peut signifier une variable locale ou une variable membre non statique d'une classe. Dans ce dernier cas, la variable membre est initialisée et détruite avec son objet propriétaire.

** Update2: comme l'a souligné @sbi, la ressource - bien que souvent allouée à l'intérieur du constructeur - peut également être allouée à l'extérieur et passée en paramètre.


1
AFAIK, l'acronyme n'implique pas que l'objet doit se trouver sur une variable locale (pile). Il peut s'agir d'une variable membre d'un autre objet, donc lorsque l'objet 'holding' est détruit, l'objet membre est également détruit et la ressource est libérée. En fait, je pense que l'acronyme signifie spécifiquement qu'il n'y a pas de méthode open()/ close()pour initialiser et libérer la ressource, juste le constructeur et le destructeur, donc la `` conservation '' de la ressource n'est que la durée de vie de l'objet, peu importe si cette durée de vie est géré par le contexte (pile) ou explicitement (allocation dynamique)
Javier

1
En fait, rien ne dit que la ressource doit être acquise dans le constructeur. Les flux de fichiers, les chaînes et les autres conteneurs le font, mais la ressource pourrait tout aussi bien être transmise au constructeur, comme c'est généralement le cas avec les pointeurs intelligents. Puisque la vôtre est la réponse la plus votée, vous voudrez peut-être résoudre ce problème.
sbi

Ce n'est pas un acronyme, c'est une abréviation. IIRC la plupart des gens le prononcent "ar ey ay ay", donc il ne se qualifie pas vraiment pour un acronyme comme par exemple DARPA, qui est prononcé DARPA au lieu d'être orthographié. De plus, je dirais que RAII est un paradigme plutôt qu'un simple idiome.
dtech

@Peter Torok: J'ai essayé ideone.com/1Jjzuc ce programme. Mais il n'y a pas d'appel destructeur. Le tomdalling.com/blog/software-design/… indique que C ++ garantit que le destructeur d'objets sur la pile sera appelé, même si une exception est levée. Alors, pourquoi le destructeur n'a pas exécuté ici? Ma ressource a-t-elle été divulguée ou ne sera-t-elle jamais libérée ou libérée?
Destructor

50

"RAII" signifie "L'acquisition de ressources est l'initialisation" et est en fait tout à fait impropre, car il ne s'agit pas de l' acquisition de ressources (et de l'initialisation d'un objet), mais de libérer la ressource (au moyen de la destruction d'un objet). ).
Mais RAII est le nom que nous avons et ça colle.

En son cœur, l'idiome propose des ressources d'encapsulation (morceaux de mémoire, fichiers ouverts, mutex déverrouillés, nommez-le) dans des objets locaux et automatiques , et le destructeur de cet objet libérant la ressource lorsque l'objet est détruit au fin du périmètre auquel il appartient:

{
  raii obj(acquire_resource());
  // ...
} // obj's dtor will call release_resource()

Bien sûr, les objets ne sont pas toujours des objets locaux et automatiques. Ils pourraient aussi être membres d'une classe:

class something {
private:
  raii obj_;  // will live and die with instances of the class
  // ... 
};

Si de tels objets gèrent la mémoire, ils sont souvent appelés «pointeurs intelligents».

Il en existe de nombreuses variantes. Par exemple, dans les premiers extraits de code, la question se pose de savoir ce qui se passerait si quelqu'un voulait copier obj. La solution la plus simple serait de simplement interdire la copie. std::unique_ptr<>, un pointeur intelligent pour faire partie de la bibliothèque standard telle que présentée par la prochaine norme C ++, le fait.
Un autre pointeur intelligent de ce type std::shared_ptrprésente la «propriété partagée» de la ressource (un objet alloué dynamiquement) qu'elle détient. Autrement dit, il peut être copié librement et toutes les copies se réfèrent au même objet. Le pointeur intelligent garde une trace du nombre de copies faisant référence au même objet et le supprimera lorsque la dernière sera détruite.
Une troisième variante est présentée parstd::auto_ptr qui implémente une sorte de sémantique de déplacement: un objet n'appartient qu'à un seul pointeur, et tenter de copier un objet entraînera (par le biais d'un piratage syntaxique) le transfert de la propriété de l'objet à la cible de l'opération de copie.


4
std::auto_ptrest une version obsolète de std::unique_ptr. std::auto_ptrsorte de sémantique de mouvement simulé autant que possible en C ++ 98, std::unique_ptrutilise la nouvelle sémantique de mouvement de C ++ 11. Une nouvelle classe a été créée parce que la sémantique de déplacement de C ++ 11 est plus explicite (nécessite std::movesauf à partir de temporaire) alors qu'elle était par défaut pour toute copie de non-const dans std::auto_ptr.
Jan Hudec

@JiahaoCai: Une fois, il y a de nombreuses années (sur Usenet), Stroustrup lui-même l'a dit.
sbi

21

La durée de vie d'un objet est déterminée par sa portée. Cependant, nous avons parfois besoin, ou il est utile, de créer un objet qui vit indépendamment de la portée où il a été créé. En C ++, l'opérateur newest utilisé pour créer un tel objet. Et pour détruire l'objet, l'opérateur deletepeut être utilisé. Les objets créés par l'opérateur newsont alloués dynamiquement, c'est-à-dire alloués en mémoire dynamique (également appelée tas ou magasin libre ). Ainsi, un objet créé par newcontinuera d'exister jusqu'à ce qu'il soit explicitement détruit à l'aide de delete.

Certaines erreurs qui peuvent se produire lors de l' utilisation newet deletesont les suivants :

  • Objet fuité (ou mémoire): utilisation newpour allouer un objet et oublier à deletel'objet.
  • Suppression prématurée (ou référence suspendue ): tenir un autre pointeur sur un objet, deletel'objet, puis utiliser l'autre pointeur.
  • Double suppression : essayer deletedeux fois un objet.

Généralement, les variables de portée sont préférées. Cependant, RAII peut être utilisé comme alternative newet deletepour faire vivre un objet indépendamment de sa portée. Une telle technique consiste à prendre le pointeur sur l'objet qui a été alloué sur le tas et à le placer dans un objet handle / manager . Ce dernier dispose d'un destructeur qui se chargera de détruire l'objet. Cela garantira que l'objet est disponible pour toute fonction qui souhaite y accéder et que l'objet est détruit lorsque la durée de vie de l' objet handle se termine, sans avoir besoin d'un nettoyage explicite.

Les exemples de la bibliothèque standard C ++ qui utilisent RAII sont std::stringet std::vector.

Considérez ce morceau de code:

void fn(const std::string& str)
{
    std::vector<char> vec;
    for (auto c : str)
        vec.push_back(c);
    // do something
}

lorsque vous créez un vecteur et que vous y insérez des éléments, vous ne vous souciez pas d'allouer et de désallouer ces éléments. Le vecteur utilise newpour allouer de l'espace à ses éléments sur le tas et deletepour libérer cet espace. En tant qu'utilisateur de vector, vous ne vous souciez pas des détails de mise en œuvre et vous ferez confiance à vector pour ne pas fuir. Dans ce cas, le vecteur est l' objet poignée de ses éléments.

D' autres exemples de la bibliothèque standard RAII d'utilisation sont std::shared_ptr, std::unique_ptret std::lock_guard.

Un autre nom pour cette technique est SBRM , abréviation de Scope-Bound Resource Management .


1
"SBRM" a beaucoup plus de sens pour moi. Je suis venu à cette question parce que je pensais que je comprenais RAII mais le nom me décourageait, l'entendre décrit à la place comme "Scope-Bound Resource Management" m'a fait instantanément réaliser que j'avais bien compris le concept.
JShorthouse

Je ne sais pas pourquoi cela n'a pas été marqué comme réponse à la question. C'est une réponse très complète et bien écrite, merci @elmiomar
Abdelrahman Shoman

13

Le livre C ++ Programming with Design Patterns Revealed décrit RAII comme:

  1. Acquérir toutes les ressources
  2. Utilisation des ressources
  3. Libérer des ressources

  • Les ressources sont implémentées en tant que classes, et tous les pointeurs ont des wrappers de classe autour d'eux (ce qui en fait des pointeurs intelligents).

  • Les ressources sont acquises en invoquant leurs constructeurs et libérées implicitement (dans l'ordre inverse de l'acquisition) en invoquant leurs destructeurs.


1
@Brandin J'ai édité mon article afin que les lecteurs se concentrent sur le contenu qui compte, plutôt que de débattre de la zone grise du droit d'auteur de ce qui constitue une utilisation équitable.
Dennis

7

Une classe RAII comprend trois parties:

  1. La ressource est abandonnée dans le destructeur
  2. Les instances de la classe sont affectées à la pile
  3. La ressource est acquise dans le constructeur. Cette partie est facultative, mais courante.

RAII signifie «L'acquisition de ressources est l'initialisation». La partie "acquisition de ressources" de RAII est l'endroit où vous commencez quelque chose qui doit être terminé plus tard, comme:

  1. Ouverture d'un dossier
  2. Allouer de la mémoire
  3. Acquérir une serrure

La partie "est l'initialisation" signifie que l'acquisition se produit à l'intérieur du constructeur d'une classe.

https://www.tomdalling.com/blog/software-design/resource-acquisition-is-initialisation-raii-explained/


5

La gestion manuelle de la mémoire est un cauchemar que les programmeurs ont inventé des moyens d'éviter depuis l'invention du compilateur. Les langages de programmation avec garbage collector vous facilitent la vie, mais au détriment des performances. Dans cet article - Éliminer le garbage collector: The RAII Way , Peter Goodspeed-Niklaus, ingénieur de Toptal, nous donne un aperçu de l'histoire des ramasseurs de déchets et explique comment les notions de propriété et d'emprunt peuvent aider à éliminer les ramasseurs de déchets sans compromettre leurs garanties de sécurité.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.