En pratique avec C ++, qu'est-ce que RAII , quels sont les pointeurs intelligents , comment sont-ils mis en œuvre dans un programme et quels sont les avantages d'utiliser RAII avec des pointeurs intelligents?
En pratique avec C ++, qu'est-ce que RAII , quels sont les pointeurs intelligents , comment sont-ils mis en œuvre dans un programme et quels sont les avantages d'utiliser RAII avec des pointeurs intelligents?
Réponses:
Un exemple simple (et peut-être surutilisé) de RAII est une classe File. Sans RAII, le code pourrait ressembler à ceci:
File file("/path/to/file");
// Do stuff with file
file.close();
En d'autres termes, nous devons nous assurer que nous fermons le fichier une fois que nous en avons fini avec lui. Cela a deux inconvénients - premièrement, partout où nous utilisons File, nous devrons appeler File :: close () - si nous oublions de le faire, nous conservons le fichier plus longtemps que nécessaire. Le deuxième problème est que si une exception est levée avant de fermer le fichier?
Java résout le deuxième problème en utilisant une clause finally:
try {
File file = new File("/path/to/file");
// Do stuff with file
} finally {
file.close();
}
ou depuis Java 7, une instruction try-with-resource:
try (File file = new File("/path/to/file")) {
// Do stuff with file
}
C ++ résout les deux problèmes en utilisant RAII - c'est-à-dire en fermant le fichier dans le destructeur de File. Tant que l'objet File est détruit au bon moment (ce qu'il devrait être quand même), la fermeture du fichier est prise en charge pour nous. Donc, notre code ressemble maintenant à quelque chose comme:
File file("/path/to/file");
// Do stuff with file
// No need to close it - destructor will do that for us
Cela ne peut pas être fait en Java car il n'y a aucune garantie quand l'objet sera détruit, donc nous ne pouvons pas garantir quand une ressource telle qu'un fichier sera libérée.
Sur les pointeurs intelligents - la plupart du temps, nous créons simplement des objets sur la pile. Par exemple (et voler un exemple d'une autre réponse):
void foo() {
std::string str;
// Do cool things to or using str
}
Cela fonctionne bien - mais que se passe-t-il si nous voulons retourner str? Nous pourrions écrire ceci:
std::string foo() {
std::string str;
// Do cool things to or using str
return str;
}
Alors, qu'est-ce qui ne va pas? Eh bien, le type de retour est std :: string - cela signifie donc que nous retournons par valeur. Cela signifie que nous copions str et renvoyons la copie. Cela peut être coûteux et nous pourrions vouloir éviter le coût de la copie. Par conséquent, nous pourrions avoir l'idée de retourner par référence ou par pointeur.
std::string* foo() {
std::string str;
// Do cool things to or using str
return &str;
}
Malheureusement, ce code ne fonctionne pas. Nous renvoyons un pointeur à str - mais str a été créé sur la pile, donc nous sommes supprimés une fois que nous quittons foo (). En d'autres termes, au moment où l'appelant obtient le pointeur, il est inutile (et sans doute pire qu'inutile car son utilisation pourrait provoquer toutes sortes d'erreurs géniales)
Alors, quelle est la solution? Nous pourrions créer str sur le tas en utilisant new - de cette façon, lorsque foo () sera terminé, str ne sera pas détruit.
std::string* foo() {
std::string* str = new std::string();
// Do cool things to or using str
return str;
}
Bien sûr, cette solution n'est pas non plus parfaite. La raison en est que nous avons créé str, mais nous ne le supprimons jamais. Ce n'est peut-être pas un problème dans un très petit programme, mais en général, nous voulons nous assurer de le supprimer. Nous pourrions simplement dire que l'appelant doit supprimer l'objet une fois qu'il en a fini avec lui. L'inconvénient est que l'appelant doit gérer la mémoire, ce qui ajoute une complexité supplémentaire et peut se tromper, conduisant à une fuite de mémoire, c'est-à-dire à la suppression de l'objet même s'il n'est plus nécessaire.
C'est là que les pointeurs intelligents entrent en jeu. L'exemple suivant utilise shared_ptr - je vous suggère de regarder les différents types de pointeurs intelligents pour savoir ce que vous voulez réellement utiliser.
shared_ptr<std::string> foo() {
shared_ptr<std::string> str = new std::string();
// Do cool things to or using str
return str;
}
Maintenant, shared_ptr comptera le nombre de références à str. Par exemple
shared_ptr<std::string> str = foo();
shared_ptr<std::string> str2 = str;
Maintenant, il y a deux références à la même chaîne. Une fois qu'il n'y aura plus de références à str, il sera supprimé. En tant que tel, vous n'avez plus à vous soucier de le supprimer vous-même.
Édition rapide: comme certains des commentaires l'ont souligné, cet exemple n'est pas parfait pour (au moins!) Deux raisons. Tout d'abord, en raison de la mise en œuvre de chaînes, la copie d'une chaîne a tendance à être peu coûteuse. Deuxièmement, en raison de ce que l'on appelle l'optimisation de la valeur de retour nommée, le retour par valeur peut ne pas être coûteux car le compilateur peut faire preuve d'intelligence pour accélérer les choses.
Essayons donc un autre exemple en utilisant notre classe File.
Disons que nous voulons utiliser un fichier comme journal. Cela signifie que nous voulons ouvrir notre fichier en mode ajout uniquement:
File file("/path/to/file", File::append);
// The exact semantics of this aren't really important,
// just that we've got a file to be used as a log
Maintenant, définissons notre fichier comme journal pour quelques autres objets:
void setLog(const Foo & foo, const Bar & bar) {
File file("/path/to/file", File::append);
foo.setLogFile(file);
bar.setLogFile(file);
}
Malheureusement, cet exemple se termine horriblement - le fichier sera fermé dès la fin de cette méthode, ce qui signifie que foo et bar ont maintenant un fichier journal non valide. Nous pourrions construire un fichier sur le tas et passer un pointeur sur file à la fois foo et bar:
void setLog(const Foo & foo, const Bar & bar) {
File* file = new File("/path/to/file", File::append);
foo.setLogFile(file);
bar.setLogFile(file);
}
Mais alors qui est responsable de la suppression du fichier? Si aucun des deux fichiers n'est supprimé, nous avons à la fois une fuite de mémoire et de ressources. Nous ne savons pas si foo ou bar finira avec le fichier en premier, nous ne pouvons donc pas nous attendre à supprimer le fichier lui-même. Par exemple, si foo supprime le fichier avant que la barre ne soit terminée, la barre a maintenant un pointeur non valide.
Ainsi, comme vous l'avez peut-être deviné, nous pourrions utiliser des pointeurs intelligents pour nous aider.
void setLog(const Foo & foo, const Bar & bar) {
shared_ptr<File> file = new File("/path/to/file", File::append);
foo.setLogFile(file);
bar.setLogFile(file);
}
Maintenant, personne n'a à s'inquiéter de la suppression du fichier - une fois que foo et bar sont terminés et n'ont plus de références au fichier (probablement en raison de la destruction de foo et bar), le fichier sera automatiquement supprimé.
RAII C'est un nom étrange pour un concept simple mais génial. Mieux vaut le nom Scope Bound Resource Management (SBRM). L'idée est que vous allouez souvent des ressources au début d'un bloc et que vous devez les libérer à la sortie d'un bloc. La sortie du bloc peut se produire par un contrôle de flux normal, en sautant et même par une exception. Pour couvrir tous ces cas, le code devient plus compliqué et redondant.
Juste un exemple de le faire sans SBRM:
void o_really() {
resource * r = allocate_resource();
try {
// something, which could throw. ...
} catch(...) {
deallocate_resource(r);
throw;
}
if(...) { return; } // oops, forgot to deallocate
deallocate_resource(r);
}
Comme vous le voyez, il existe de nombreuses façons de se faire pwned. L'idée est que nous encapsulons la gestion des ressources dans une classe. L'initialisation de son objet acquiert la ressource ("L'acquisition de ressources est une initialisation"). Au moment où nous quittons le bloc (portée du bloc), la ressource est à nouveau libérée.
struct resource_holder {
resource_holder() {
r = allocate_resource();
}
~resource_holder() {
deallocate_resource(r);
}
resource * r;
};
void o_really() {
resource_holder r;
// something, which could throw. ...
if(...) { return; }
}
C'est bien si vous avez des classes qui ne sont pas uniquement destinées à allouer / désallouer des ressources. L'allocation ne serait qu'une préoccupation supplémentaire pour faire leur travail. Mais dès que vous souhaitez simplement allouer / désallouer des ressources, ce qui précède devient peu pratique. Vous devez écrire une classe enveloppante pour chaque type de ressource que vous acquérez. Pour faciliter cela, des pointeurs intelligents vous permettent d'automatiser ce processus:
shared_ptr<Entry> create_entry(Parameters p) {
shared_ptr<Entry> e(Entry::createEntry(p), &Entry::freeEntry);
return e;
}
Normalement, les pointeurs intelligents sont de minces enveloppes autour de new / delete qui n'apparaissent que delete
lorsque la ressource qu'ils possèdent sort du domaine. Certains pointeurs intelligents, comme shared_ptr, vous permettent de leur dire un soi-disant délétère, qui est utilisé à la place de delete
. Cela vous permet, par exemple, de gérer les descripteurs de fenêtre, les ressources d'expression régulière et d'autres éléments arbitraires, tant que vous indiquez à shared_ptr le bon suppresseur.
Il existe différents pointeurs intelligents à des fins différentes:
Code:
unique_ptr<plot_src> p(new plot_src); // now, p owns
unique_ptr<plot_src> u(move(p)); // now, u owns, p owns nothing.
unique_ptr<plot_src> v(u); // error, trying to copy u
vector<unique_ptr<plot_src>> pv;
pv.emplace_back(new plot_src);
pv.emplace_back(new plot_src);
Contrairement à auto_ptr, unique_ptr peut être placé dans un conteneur, car les conteneurs pourront contenir des types non copiables (mais mobiles), comme les flux et unique_ptr également.
Code:
void do_something() {
scoped_ptr<pipe> sp(new pipe);
// do something here...
} // when going out of scope, sp will delete the pointer automatically.
Code:
shared_ptr<plot_src> p(new plot_src(&fx));
plot1->add(p)->setColor("#00FF00");
plot2->add(p)->setColor("#FF0000");
// if p now goes out of scope, the src won't be freed, as both plot1 and
// plot2 both still have references.
Comme vous le voyez, la source de tracé (fonction fx) est partagée, mais chacune a une entrée distincte, sur laquelle nous définissons la couleur. Il existe une classe faible_ptr qui est utilisée lorsque le code doit faire référence à la ressource appartenant à un pointeur intelligent, mais n'a pas besoin de posséder la ressource. Au lieu de passer un pointeur brut, vous devez ensuite créer un faiblesse_ptr. Il lèvera une exception lorsqu'il remarquera que vous essayez d'accéder à la ressource par un chemin d'accès faible_ptr, même s'il n'y a plus de shared_ptr propriétaire de la ressource.
unique_ptr
, et sort
seront également modifiés également.
RAII est le paradigme de conception pour garantir que les variables gèrent toutes les initialisations nécessaires dans leurs constructeurs et tous les nettoyages nécessaires dans leurs destructeurs. Cela réduit toute l'initialisation et le nettoyage à une seule étape.
C ++ ne nécessite pas RAII, mais il est de plus en plus admis que l'utilisation de méthodes RAII produira un code plus robuste.
La raison pour laquelle RAII est utile en C ++ est que C ++ gère intrinsèquement la création et la destruction des variables lorsqu'elles entrent et sortent de la portée, que ce soit par le biais du flux de code normal ou par le déroulement de pile déclenché par une exception. C'est un cadeau en C ++.
En liant toute l'initialisation et le nettoyage à ces mécanismes, vous êtes assuré que C ++ se chargera également de ce travail pour vous.
Parler de RAII en C ++ mène généralement à la discussion des pointeurs intelligents, car les pointeurs sont particulièrement fragiles en ce qui concerne le nettoyage. Lors de la gestion de la mémoire allouée en tas acquise à partir de malloc ou de new, il est généralement de la responsabilité du programmeur de libérer ou de supprimer cette mémoire avant que le pointeur ne soit détruit. Les pointeurs intelligents utiliseront la philosophie RAII pour garantir que les objets alloués en tas sont détruits à chaque fois que la variable de pointeur est détruite.
Le pointeur intelligent est une variante du RAII. RAII signifie que l'acquisition de ressources est l'initialisation. Le pointeur intelligent acquiert une ressource (mémoire) avant utilisation, puis la jette automatiquement dans un destructeur. Deux choses se produisent:
Par exemple, un autre exemple est le socket réseau RAII. Dans ce cas:
Maintenant, comme vous pouvez le voir, RAII est un outil très utile dans la plupart des cas car il aide les gens à se faire baiser.
Les sources C ++ de pointeurs intelligents sont des millions sur le net, y compris les réponses au-dessus de moi.
Boost en possède un certain nombre, y compris ceux de Boost.Interprocess pour la mémoire partagée. Il simplifie considérablement la gestion de la mémoire, en particulier dans les situations provoquant des maux de tête comme lorsque 5 processus partagent la même structure de données: lorsque tout le monde a fini avec un morceau de mémoire, vous voulez qu'il soit automatiquement libéré et ne pas avoir à rester là à essayer de comprendre qui devrait être responsable de l'appel delete
à un morceau de mémoire, de peur de vous retrouver avec une fuite de mémoire, ou un pointeur qui est libéré par erreur deux fois et peut corrompre le tas entier.
void foo () { std :: string bar; // // plus de code ici // }
Quoi qu'il arrive, la barre sera correctement supprimée une fois que la portée de la fonction foo () aura été abandonnée.
En interne, les implémentations std :: string utilisent souvent des pointeurs comptés par référence. La chaîne interne ne doit donc être copiée que lorsque l'une des copies des chaînes a changé. Par conséquent, un pointeur intelligent compté par référence permet de copier uniquement quelque chose lorsque cela est nécessaire.
De plus, le comptage des références internes permet que la mémoire soit correctement supprimée lorsque la copie de la chaîne interne n'est plus nécessaire.