Qu'entend-on par acquisition de ressource, c'est l'initialisation (RAII)?
C'est un nom vraiment terrible pour un concept incroyablement puissant, et peut-être une des choses numéro 1 qui manque aux développeurs C++ lorsqu'ils passent à d'autres langages. Il y a eu un peu de mouvement pour essayer de renommer ce concept en Gestion des ressources liées à l'étendue, bien qu'il ne semble pas encore avoir compris.
Lorsque nous disons "ressource", nous ne parlons pas seulement de mémoire - il peut s'agir de descripteurs de fichier, de sockets réseau, de descripteurs de base de données, GDI objets ... En bref, les éléments dont nous disposons sont limités Nous devons pouvoir 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, de sorte que lorsque la variable sort de la portée, le destructeur publie le Une propriété très utile de ceci est qu'elle améliore la sécurité des exceptions. 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 un
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 coule pas.
Ceci est un langage de programmation qui signifie brièvement que vous
Cela garantit que quoi qu'il se passe pendant l'utilisation de la ressource, celle-ci sera éventuellement libérée (que ce soit en raison d'un retour normal, de la destruction de l'objet contenu 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, votre code est beaucoup plus propre, vous n'avez pas besoin de mélanger le code de traitement des erreurs avec la fonctionnalité principale.
*
pdate: "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.
**
pdate2: Comme @sbi l'a fait remarquer, la ressource - bien que souvent allouée à l'intérieur du constructeur - peut également être allouée à l'extérieur et transmise en tant que paramètre.
"RAII" signifie "L'acquisition d'une ressource est une initialisation" et est en fait assez impropre, car ce n'est pas une acquisition de ressource (et l'initialisation d'un objet ) elle concerne, mais libère la ressource (au moyen de destruction d'un objet).
Mais RAII est le nom que nous avons et ça colle.
En son cœur même, l’idiome contient des ressources encapsulantes (blocs de mémoire, fichiers ouverts, mutex déverrouillés, you-name-it) dans , objets automatiques locaux , et que le destructeur de cet objet libère la ressource lorsque l'objet est détruit à la fin de la portée à laquelle 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, 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 existe de nombreuses variantes de cela. Par exemple, dans les premiers extraits de code, la question qui se pose est de savoir ce qui se passerait si quelqu'un voulait copier obj
. La solution la plus simple serait tout simplement d’interdire la copie. std::unique_ptr<>
, un pointeur intelligent faisant partie de la bibliothèque standard décrite par le prochain standard C++, effectue cette opération.
Un autre pointeur intelligent, std::shared_ptr
présente la "propriété partagée" de la ressource (un objet alloué dynamiquement) qu’elle détient. Autrement dit, il peut être librement copié et toutes les copies se réfèrent au même objet. Le pointeur intelligent garde la trace du nombre de copies faisant référence au même objet et le supprime lorsque le dernier est en cours de destruction.
Une troisième variante est décrite par std::auto_ptr
qui implémente une sorte de sémantique de déplacement: un objet n’appartient qu’à un seul pointeur. Toute tentative de copie d’un objet entraîne (par le biais d’une hackery syntaxique) le transfert de la propriété de cet objet à la cible de la copie.
Le livre Programmation C++ avec les modèles de conception révélés décrit RAII comme:
Où
Les ressources sont implémentées en tant que classes et tous les pointeurs sont entourés d'un wrapper de classe (ce qui en fait des pointeurs intelligents).
Les ressources sont acquises en appelant leurs constructeurs et libérées implicitement (dans l'ordre inverse de l'acquisition) en appelant leurs destructeurs.
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 new
est utilisé pour créer un tel objet. Et pour détruire l'objet, l'opérateur delete
peut être utilisé. Les objets créés par l'opérateur new
sont alloués dynamiquement, c'est-à-dire alloués en mémoire dynamique (également appelés tas ou magasin gratuit ). Ainsi, un objet créé par new
continuera d'exister jusqu'à ce qu'il soit explicitement détruit à l'aide de delete
.
Certaines erreurs qui peuvent survenir lors de l'utilisation de new
et delete
sont les suivantes:
new
pour allouer un objet et oublier de delete
l'objet.delete
l'objet, puis utilisez l'autre pointeur.delete
un objet deux fois.Généralement, les variables de portée sont préférées. Cependant, RAII peut être utilisé comme alternative à new
et delete
pour faire vivre un objet indépendamment de sa portée. Une telle technique consiste à placer le pointeur sur l’objet alloué sur le tas et à le placer dans un objet handle/manager . Ce dernier a un destructeur qui se chargera de détruire l'objet. Cela garantira que l'objet est disponible pour toute fonction qui veut y accéder, et qu'il est détruit lorsque la durée de vie de l'objet handle se termine, sans la nécessité d'un nettoyage explicite.
Des exemples de la bibliothèque standard C++ qui utilisent RAII sont std::string
et std::vector
.
Considérons 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 de leur attribuer ou de les désallouer. Le vecteur utilise new
pour allouer de l'espace pour ses éléments sur le tas, et delete
pour libérer cet espace. En tant qu'utilisateur de Vector, vous ne vous souciez pas des détails de la mise en œuvre et vous ferez confiance à Vector pour ne pas fuir. Dans ce cas, le vecteur est l’objet handle de ses éléments.
Les autres exemples de la bibliothèque standard qui utilisent RAII sont std::shared_ptr
, std::unique_ptr
, et std::lock_guard
.
Un autre nom pour cette technique est SBRM , abréviation de Gestion des ressources liées à la portée .
La gestion manuelle de la mémoire est un cauchemar que les programmeurs inventent depuis l’invention du compilateur. Les langages de programmation avec les éboueurs facilitent la vie, mais au détriment des performances. Dans cet article - Éliminer le ramasse-miettes: la méthode RAII , Peter Goodspeed-Niklaus, ingénieur chez Toptal, nous raconte l’histoire des éboueurs et explique comment les notions de propriété et d'emprunt peuvent aider à éliminer les éboueurs sans compromettre les garanties de sécurité.
Une classe RAII comprend trois parties:
RAII signifie "L'acquisition des ressources est une initialisation". La partie "acquisition de ressources" de RAII est l'endroit où vous commencez quelque chose qui doit être terminé plus tard, tels que:
La partie "is initialization" signifie que l'acquisition a lieu à l'intérieur du constructeur d'une classe.