En pratique avec C++, ce qui est RAII quels sont pointeurs intelligents Comment sont-ils mis en œuvre dans un programme et quels sont les avantages de l'utilisation de RAII avec des pointeurs intelligents ?
Réponses
Trop de publicités?Un exemple simple (et peut-être trop utilisé) de RAII est la classe File. Sans RAII, le code pourrait ressembler à quelque chose comme ceci :
File file("/path/to/file");
// Do stuff with file
file.close();
En d'autres termes, nous devons veiller à fermer le fichier une fois que nous en avons terminé avec lui. Cela présente deux inconvénients : tout d'abord, chaque fois que nous utilisons File, nous devons appeler File::close() - si nous oublions de le faire, nous conservons le fichier plus longtemps que nécessaire. Le second problème est le suivant : que se passe-t-il si une exception est levée avant la fermeture du fichier ?
Java résout le deuxième problème en utilisant une clause finale :
try {
File file = new File("/path/to/file");
// Do stuff with file
} finally {
file.close();
}
Le C++ résout les deux problèmes en utilisant le 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 qui devrait être le cas de toute façon), 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
La raison pour laquelle cela ne peut être fait en Java est que nous n'avons aucune garantie sur le moment où l'objet sera détruit, et ne pouvons donc pas garantir le moment où une ressource telle qu'un fichier sera libérée.
En ce qui concerne les pointeurs intelligents, la plupart du temps, nous créons simplement des objets sur la pile. Par exemple (et en volant un exemple d'une autre réponse) :
void foo() {
std::string str;
// Do cool things to or using str
}
Cela fonctionne bien, mais que faire si l'on veut renvoyer 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 avec ça ? Eh bien, le type de retour est std::string - ce qui signifie que nous retournons par valeur. Cela signifie que nous copions str et que nous retournons 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 vers str - mais str a été créé sur la pile, et sera donc supprimé dès que nous sortirons de foo(). En d'autres termes, au moment où l'appelant reçoit le pointeur, il est inutile (et sans doute pire qu'inutile puisque son utilisation pourrait provoquer toutes sortes d'erreurs bizarres).
Alors, quelle est la solution ? Nous pourrions créer str sur le tas en utilisant new - de cette façon, lorsque foo() est 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 que nous ne l'avons jamais supprimé. Cela peut ne pas être un problème dans un tout petit programme, mais en général, nous voulons être sûrs 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 pourrait se tromper, conduisant à une fuite de mémoire, c'est-à-dire ne pas supprimer 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 vraiment 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;
Il y a maintenant deux références à la même chaîne. Dès qu'il n'y aura plus de références à str, celle-ci sera supprimée. Ainsi, vous n'avez plus à vous soucier de la supprimer vous-même.
Modification rapide : comme certains commentaires l'ont souligné, cet exemple n'est pas parfait pour (au moins !) deux raisons. Premièrement, en raison de l'implémentation des chaînes de caractères, la copie d'une chaîne de caractères 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 puisque le compilateur peut faire preuve d'ingéniosité pour accélérer les choses.
Essayons donc un autre exemple en utilisant notre classe File.
Disons que nous voulons utiliser un fichier comme un journal. Cela signifie que nous voulons ouvrir notre fichier en mode "append only" :
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, utilisons 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 mal - le fichier sera fermé dès la fin de cette méthode, ce qui signifie que foo et bar ont maintenant un fichier journal invalide. Nous pourrions construire file sur le tas, et passer un pointeur vers file à 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 ne supprime le fichier, nous avons alors une fuite de mémoire et de ressources. Nous ne savons pas si foo ou bar aura fini avec le fichier en premier, donc nous ne pouvons pas nous attendre à ce qu'ils suppriment le fichier eux-mêmes. Par exemple, si foo supprime le fichier avant que bar n'ait fini de l'utiliser, bar a maintenant un pointeur invalide.
Donc, 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 ne doit s'inquiéter de la suppression du fichier - une fois que foo et bar ont terminé et n'ont plus de références au fichier (probablement parce que foo et bar ont été détruits), le fichier sera automatiquement supprimé.
RAII C'est un nom étrange pour un concept simple mais génial. Mieux, c'est le nom Gestion des ressources en fonction du champ d'application (SBRM). L'idée est qu'il arrive souvent d'allouer des ressources au début d'un bloc et de devoir les libérer à la sortie d'un bloc. La sortie du bloc peut se faire par un contrôle de flux normal, par un saut hors du bloc, et même par une exception. Pour couvrir tous ces cas, le code devient plus compliqué et redondant.
C'est juste un exemple de réalisation 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 y a de nombreuses façons de se faire écraser. L'idée est d'encapsuler la gestion des ressources dans une classe. L'initialisation de son objet acquiert la ressource ("Resource Acquisition Is Initialization"). Au moment où nous sortons du bloc (block scope), 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 propres qui n'ont pas pour seul but d'allouer/désallouer des ressources. L'allocation serait juste une préoccupation supplémentaire pour faire leur travail. Mais dès que vous voulez seulement 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, les 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 se trouvent juste à appeler delete
lorsque la ressource qu'ils possèdent sort de sa portée. Certains pointeurs intelligents, comme shared_ptr, vous permettent de leur indiquer ce qu'on appelle un deleter, qui est utilisé à la place de delete
. Cela vous permet, par exemple, de gérer les poignées de fenêtre, les ressources d'expression régulière et d'autres choses arbitraires, tant que vous indiquez à shared_ptr le bon suppresseur.
Il existe différents pointeurs intelligents pour différents usages :
unique_ptr
est un pointeur intelligent qui possède exclusivement un objet. Il n'est pas dans boost, mais il apparaîtra probablement dans la prochaine norme C++. C'est non copiable mais soutient transfert de propriété . Quelques exemples de code (prochain C++) :
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 streams et unique_ptr aussi.
scoped_ptr
est un pointeur intelligent boost qui n'est ni copiable ni déplaçable. C'est la chose parfaite à utiliser lorsque vous voulez vous assurer que les pointeurs sont supprimés lorsqu'ils sortent de la portée.
Code :
void do_something() {
scoped_ptr<pipe> sp(new pipe);
// do something here...
} // when going out of scope, sp will delete the pointer automatically.
partagé_ptr
est pour la propriété partagée. Il est donc à la fois copiable et déplaçable. Plusieurs instances de pointeurs intelligents peuvent posséder la même ressource. Dès que le dernier smart pointer propriétaire de la ressource sort du champ d'application, la ressource est libérée. Quelques exemples concrets d'un de mes projets :
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 du tracé (fonction fx) est partagée, mais chacun a une entrée séparée, sur laquelle nous définissons la couleur. Il existe une classe weak_ptr qui est utilisée lorsque le code a besoin de se référer à la ressource possédée par un pointeur intelligent, mais n'a pas besoin de posséder la ressource. Au lieu de passer un pointeur brut, vous devriez alors créer un weak_ptr. Il lèvera une exception lorsqu'il remarquera que vous essayez d'accéder à la ressource par un chemin d'accès de type weak_ptr, même s'il n'y a plus de shared_ptr propriétaire de la ressource.
La prémisse et les raisons sont simples, dans le concept.
RAII est le paradigme de conception qui permet de garantir que Les variables gèrent toute l'initialisation nécessaire dans leurs constructeurs et tout le nettoyage nécessaire dans leurs destructeurs. Cela réduit toute l'initialisation et le nettoyage à une seule étape.
Le C++ ne requiert pas de RAII, mais il est de plus en plus admis que l'utilisation des méthodes RAII produira un code plus robuste.
La raison pour laquelle le RAII est utile en C++ est que le 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 flux normal du code ou par le déroulement de la pile déclenché par une exception. C'est un avantage gratuit en C++.
En liant toute l'initialisation et le nettoyage à ces mécanismes, vous êtes assuré que le 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 lorsqu'il s'agit de nettoyage. Lors de la gestion de la mémoire allouée au tas acquise par malloc ou 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 s'assurer que les objets alloués au tas sont détruits chaque fois que la variable du pointeur est détruite.
Le pointeur intelligent est une variante du RAII. RAII signifie que l'acquisition de ressources est une initialisation. Le pointeur intelligent acquiert une ressource (mémoire) avant de l'utiliser, puis la jette automatiquement dans un destructeur. Deux choses se produisent :
- Nous allouons mémoire avant de l'utiliser, toujours, même quand on n'en a pas envie -- il est difficile de faire autrement avec un pointeur intelligent. Si ce n'était pas le cas, vous essayeriez d'accéder à la mémoire NULL, ce qui entraînerait un crash (très douloureux).
- Nous libérons mémoire même s'il y a une erreur. Aucune mémoire n'est laissée en suspens.
Par exemple, un autre exemple est la prise réseau RAII. Dans ce cas :
- Nous ouvrons prise réseau avant de l'utiliser, toujours, même quand on n'en a pas envie - c'est difficile de faire autrement avec RAII. Si vous essayez de le faire sans RAII, vous risquez d'ouvrir un socket vide pour, disons, une connexion MSN. Dans ce cas, un message comme "faisons-le ce soir" pourrait ne pas être transféré, les utilisateurs ne s'enverraient pas en l'air et vous risqueriez de vous faire virer.
- Nous fermons prise réseau même s'il y a une erreur. Aucune socket n'est laissée en suspens car cela pourrait empêcher le message de réponse "sure ill be on bottom" de revenir à l'expéditeur.
Maintenant, comme vous pouvez le voir, le RAII est un outil très utile dans la plupart des cas, car il aide les gens à s'envoyer en l'air.
Les sources C++ de pointeurs intelligents se comptent par millions sur le net, y compris les réponses ci-dessus.
Boost en possède un certain nombre, dont ceux de la rubrique "Autres". Boost.Interprocessus pour la mémoire partagée. Cela simplifie grandement la gestion de la mémoire, en particulier dans des situations qui donnent des maux de tête, comme lorsque vous avez 5 processus qui partagent la même structure de données : lorsque tout le monde a fini d'utiliser un morceau de mémoire, vous voulez qu'il soit automatiquement libéré & vous n'avez pas besoin de rester là à essayer de déterminer qui doit être responsable de l'appel de la mémoire partagée. delete
sur un morceau de mémoire, de peur de se retrouver avec une fuite de mémoire, ou un pointeur qui est libéré deux fois par erreur et peut corrompre le tas entier.