186 votes

RAII et smart pointeurs en C++

Dans la pratique avec C++, ce qui est RAII, ce sont des pointeurs intelligents, comment sont-elles mises en œuvre dans un programme et quels sont les avantages de l'utilisation de RAII avec des pointeurs intelligents?

306voto

Michael Williamson Points 6210

Une simple (et peut-être galvaudé) exemple de RAII est un Fichier de classe. 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 nous assurer de fermer le fichier une fois que nous avons fini avec elle. Cela a deux inconvénients: d'abord, où nous utilisons Fichier, nous allons devoir nous appelle Fichier::close() - si l'on oublie de le faire, nous sommes maintenant sur le fichier de plus que nous en avons besoin. Le deuxième problème est que si une exception est levée avant de fermer le fichier?

Java permet de résoudre le deuxième problème à l'aide d'une clause finally:

try {
    File file = new File("/path/to/file");
    // Do stuff with file
} finally {
    file.close();
}

C++ résout ces deux problèmes en utilisant RAII - qui est, la fermeture du fichier dans le destructeur de Fichier. Tant que le Fichier objet est détruit au bon moment (ce qui devrait être de toute façon), la fermeture du fichier est pris en charge pour nous. Ainsi, notre code ressemble maintenant à:

File file("/path/to/file");
// Do stuff with file
// No need to close it - destructor will do that for us

La raison de ce qui ne peut pas être fait en Java, c'est que nous n'avons aucune garantie sur lorsque l'objet sera détruit, il ne peut donc pas garantie lorsqu'une ressource comme le fichier sera libéré.

Sur des pointeurs intelligents - beaucoup de temps, nous venons de créer des objets sur la pile. Par exemple (et de voler un exemple d'une autre réponse):

void foo() {
    std::string str;
    // Do cool things to or using str
}

Cela fonctionne très bien mais que faire 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, où est le mal? Ainsi, le type de retour est std::string - donc, cela signifie que nous sommes de retour par valeur. Cela signifie que l'on copie str et fait retour de la copie. Cela peut être coûteux, et on peut vouloir éviter le coût de la copie. Par conséquent, nous pourrions venir avec l'idée d'un retour 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 sommes de retour d'un pointeur vers str - mais str a été créé sur la pile, nous avons donc être supprimé une fois que nous avons sortie de foo(). En d'autres termes, au moment où l'appelant obtient le pointeur, c'est inutile (et sans doute pire qu'inutile depuis qu'il l'aide pourrait causer toutes sortes de funky erreurs)

Alors, quelle est la solution? Nous pourrions créer des str sur le tas à l'aide de nouvelles, de cette façon, lorsque foo() est terminée, 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 parfaite non plus. La raison en est que nous avons créé str, mais nous n'avons jamais supprimer. Cela pourrait ne pas être un problème dans un très petit programme, mais en général, nous voulons nous assurer que nous supprimer. Nous pourrions nous contenter de dire que l'appelant doit supprimer l'objet une fois qu'il en a fini avec elle. L'inconvénient est que l'appelant a à gérer la mémoire, ce qui ajoute de la complexité, et peut se tromper, conduisant à une fuite de mémoire c'est à dire ne pas la suppression de l'objet, même si elle n'est plus nécessaire.

C'est là que des pointeurs intelligents viennent. L'exemple suivant utilise shared_ptr - je vous suggère de regarder sur les différents types de pointeurs intelligents pour apprendre ce que vous voulez 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 compte le nombre de références à la 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 ne reste plus de références à la str, il sera supprimé. En tant que tel, vous n'avez plus à vous soucier de les supprimer vous-même.

Quick edit: comme certains 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 de caractères, la copie d'une chaîne a tendance à être peu coûteux. Deuxièmement, en raison de ce qui est connu sous le nom nommés valeur de retour est de l'optimisation, retour par valeur peut ne pas être cher car le compilateur pouvez faire un peu d'astuce pour accélérer les choses.

Alors, nous allons essayer un autre exemple de l'utilisation de notre Fichier de classe.

Disons que nous voulons utiliser un fichier comme un journal. Cela signifie que nous voulons ouvrir notre fichier à ajouter uniquement en mode:

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, nous allons mettre notre fichier journal pour un couple de d'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 fichier sera fermé dès que cette méthode se termine, ce qui signifie que foo et bar maintenant invalide le fichier de log. Nous avons pu construire un fichier sur le tas, et de passer un pointeur de fichier à 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 de fichiers? Si ni supprimer le fichier, puis nous avons à la fois une mémoire et des ressources de fuite. Nous ne savons pas si foo ou de la barre d'en finir avec tout d'abord le fichier, de sorte que nous ne pouvons pas attendre non plus de supprimer le fichier à eux-mêmes. Par exemple, si foo supprime le fichier avant de le bar a fini avec elle, le bar a désormais un pointeur non valide.

Donc, comme vous l'aurez 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 besoin de s'inquiéter à propos de la suppression de fichiers une fois que les deux foo et bar ont fini et n'ont plus aucune références du fichier (probablement en raison de foo et bar d'être détruit), le fichier sera automatiquement supprimé.

136voto

RAII C'est un nom étrange pour un simple mais génial concept. Mieux, c'est le nom du Champ d'application Lié à la Gestion des Ressources (SBRM). L'idée est que, souvent, il vous arrive d'allouer des ressources au début d'un bloc, et la nécessité de le sortir à la sortie d'un bloc. Sortir le bloc peut se passer par la procédure normale de contrôle de flux, en sautant hors de lui, et même par une exception. Pour couvrir tous ces cas, le code devient plus complexe et redondant.

Juste un exemple, 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 que nous pouvons obtenir pwned. L'idée est que nous encapsuler la gestion des ressources dans une classe. L'initialisation de l'objet acquiert la ressource ("l'Acquisition de Ressources Est d'Initialisation"). Au moment de nous quitter le bloc (le bloc de portée), la ressource est libéré à nouveau.

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 de leurs propres qui ne sont pas uniquement dans le but d'allocation/désallocation de ressources. L'attribution pourrait juste être une préoccupation supplémentaire pour faire leur travail. Mais dès que vous voulez juste d'allouer/de libérer des ressources, le ci-dessus devient unhandy. Vous devez écrire un emballage de classe pour chaque type de ressource que vous acquérir. 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 minces wrappers autour de new / delete qui vient de se produire à l'appel delete lorsque les ressources qu'ils possèdent est hors de portée. Certains pointeurs intelligents, comme shared_ptr vous permettre de leur dire un soi-disant deleter, qui est utilisé à la place de delete. Qui vous permet, par exemple, pour gérer les poignées de fenêtre, expression rationnelle des ressources et d'autres arbitraire des choses, aussi longtemps que vous le dites shared_ptr sur le droit de la deleter.

Il existe différents pointeurs intelligents à des fins différentes:

unique_ptr

est un pointeur intelligent qui possède un objet exclusivement. Il n'est pas dans le coup de pouce, mais elle apparaîtra dans la prochaine Norme C++. Il est non-copiable mais prend en charge le transfert de la propriété. Un exemple de code (à côté 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 mis dans un récipient, parce que les conteneurs seront en mesure de tenir non-copiable (mais mobiliers) types, comme les ruisseaux et les unique_ptr trop.

scoped_ptr

est un coup de pouce de pointeur intelligent, qui n'est pas copiable, ni meubles. C'est le truc parfait pour être utilisé quand vous voulez assurez-vous que les pointeurs sont supprimés lorsque vous sortez 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.

shared_ptr

est le partage de la propriété. À cet effet, il est à la fois copiable et mobiles. Plusieurs pointeur intelligent instances peuvent posséder la même ressource. Dès que le dernier pointeur intelligent propriétaire de la ressource est hors de portée, la ressource va être libéré. Certains l'exemple du monde réel de l'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, l'intrigue-source (fonction fx) est partagé, mais chacun a une entrée séparée, sur lequel nous avons mis de la couleur. Il y a un weak classe qui est utilisé lorsque le code a besoin de se référer à la ressource détenue par 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 weak. Il va lever une exception quand il détecte que vous essayez d'accéder à la ressource par un weak chemin d'accès, même si il n'y a pas de shared_ptr plus propriétaire de la ressource.

32voto

Drew Dormann Points 25025

Les fondements et les raisons sont simples, dans le concept.

RAII est le paradigme de conception pour s'assurer que les variables de gérer tous besoin d'initialisation dans leurs constructeurs et tout le nécessaire de nettoyage dans leurs destructeurs. Cela réduit l'initialisation et de nettoyage en une seule étape.

C++ ne nécessite pas de RAII, mais il est de plus en plus admis que l'utilisation de RAII méthodes de produire du code plus robuste.

La raison que le RAII est utile de C++ que le C++ intrinsèquement gère la création et la destruction de variables comme ils entrent et sortent de la portée, que ce soit par le biais des flux de code ou à travers le déroulement de pile déclenchée par une exception. C'est un cadeau en C++.

En liant tout d'initialisation et de nettoyage à ces mécanismes, vous êtes assuré que C++ va prendre soin de ce travail pour vous.

Parler RAII en C++ conduit généralement à la discussion des pointeurs intelligents, car les pointeurs sont particulièrement fragile quand il s'agit de nettoyage. Lors de la gestion de segment de mémoire allouée acquis de malloc ou nouveau, il est généralement de la responsabilité du programmeur de gratuit ou de supprimer de la mémoire avant de le pointeur est détruit. Pointeurs intelligents utilisera le RAII de la philosophie pour s'assurer que tas les objets alloués sont détruits à tout moment le pointeur de la variable est détruite.

8voto

mannicken Points 1384

Smart pointeur est une variante du RAII. RAII moyens d'acquisition des ressources est l'initialisation. Pointeur intelligent acquiert une ressource (mémoire) avant l'utilisation, et qu'on jette ensuite automatiquement dans un destructeur. Deux choses se produisent:

  1. Nous allouer de la mémoire avant de nous les utilisons, pour toujours, même quand nous ne nous sentons pas comme ça -- il est difficile de le faire d'une autre façon avec un pointeur intelligent. Si ce n'était pas le cas, vous essayez d'accéder à NULL mémoire, résultant dans un accident (très douloureux).
  2. Nous libérer de la mémoire , même quand il y a une erreur. Aucune mémoire n'est laissée en suspens.

Par exemple, un autre exemple est prise réseau RAII. Dans ce cas:

  1. Nous avons ouvert une prise réseau avant de nous les utilisons,pour toujours, même quand on ne se sent-il est difficile de le faire d'une autre façon avec RAII. Si vous essayez de faire cela sans RAII vous pouvez ouvrir support vide pour, disons, connexion MSN. Alors message du genre "permet de le faire ce soir" pourrait ne pas être transféré, les utilisateurs ne seront pas mises, et on risquerait de se faire licencier.
  2. Nous fermons une prise réseau , même quand il y a une erreur. Pas de prise est laissée en suspens, car cela pourrait empêcher le message de réponse "malade sûr que sur le fond" de frapper à l'expéditeur de retour.

Maintenant, comme vous pouvez le voir, le 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 à travers le net, y compris les réponses au-dessus de moi.

2voto

Jason S Points 58434

Boost a un certain nombre, y compris ceux de Boost.Interprocessus pour la mémoire partagée. Il simplifie grandement la gestion de la mémoire, en particulier dans les maux de tête induisant des situations comme lorsque vous avez 5 les processus de partage de la même structure de données: quand tout le monde est fait avec un morceau de la mémoire, vous souhaitez obtenir automatiquement libéré et ne pas avoir à s'asseoir à essayer de comprendre qui devrait être responsable de l'appel delete sur une partie de la 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 l'ensemble du tas.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by: