Le C++ supporte-t-il ' enfin Des blocs de béton ?
Quel est le idiome RAII ?
Quelle est la différence entre l'idiome RAII de C++ et L'instruction "using" de C# ?
Le C++ supporte-t-il ' enfin Des blocs de béton ?
Quel est le idiome RAII ?
Quelle est la différence entre l'idiome RAII de C++ et L'instruction "using" de C# ?
Non, le C++ ne prend pas en charge les blocs "finally". La raison en est que le C++ supporte plutôt le RAII : "Resource Acquisition Is Initialization" -- un bloc de type "finally". mauvais nom † pour un concept vraiment utile.
L'idée est que le destructeur d'un objet est responsable de la libération des ressources. Lorsque l'objet a une durée de stockage automatique, le destructeur de l'objet sera appelé lorsque le bloc dans lequel il a été créé sortira -- même si ce bloc est sorti en présence d'une exception. Voici un exemple L'explication de Bjarne Stroustrup du sujet.
Une utilisation courante de RAII est le verrouillage d'un mutex :
// A class with implements RAII
class lock
{
mutex &m_;
public:
lock(mutex &m)
: m_(m)
{
m.acquire();
}
~lock()
{
m_.release();
}
};
// A class which uses 'mutex' and 'lock' objects
class foo
{
mutex mutex_; // mutex for locking 'foo' object
public:
void bar()
{
lock scopeLock(mutex_); // lock object.
foobar(); // an operation which may throw an exception
// scopeLock will be destructed even if an exception
// occurs, which will release the mutex and allow
// other functions to lock the object and run.
}
};
RAII simplifie également l'utilisation d'objets comme membres d'autres classes. Lorsque la classe propriétaire est détruite, la ressource gérée par la classe RAII est libérée car le destructeur de la classe gérée par RAII est appelé en conséquence. Cela signifie que lorsque vous utilisez RAII pour tous les membres d'une classe qui gèrent des ressources, vous pouvez vous en sortir en utilisant un destructeur très simple, peut-être même par défaut, pour la classe propriétaire puisqu'elle n'a pas besoin de gérer manuellement la durée de vie de ses ressources membres. (Merci à Mike B pour l'avoir signalé).
Pour ceux qui sont familiers avec C# ou VB.NET, vous pouvez reconnaître que RAII est similaire à Destruction déterministe dans .NET à l'aide de IDisposable et des instructions "using". . En effet, les deux méthodes sont très similaires. La principale différence est que la RAII libère de manière déterministe tout type de ressource, y compris la mémoire. Lors de l'implémentation d'IDisposable dans .NET (même dans le langage .NET C++/CLI), les ressources seront libérées de manière déterministe, sauf la mémoire. En .NET, la mémoire n'est pas libérée de manière déterministe ; elle n'est libérée que pendant les cycles de collecte des déchets.
† Certaines personnes pensent que "La destruction est un abandon de ressources" est un nom plus exact pour l'idiome RAII.
"La destruction est une renonciation aux ressources" - DIRR... Non, ça ne marche pas pour moi. =P
Le RAII est bloqué - il n'y a pas moyen de le changer. Essayer de le faire serait insensé. Cependant, vous devez admettre que "Resource Acquisition Is Initialization" est toujours un nom assez pauvre.
En C++, la finalité est PAS nécessaire en raison du RAII.
RAII déplace la responsabilité de la sécurité des exceptions de l'utilisateur de l'objet vers le concepteur (et l'implémenteur) de l'objet. Je dirais que c'est le bon endroit car vous n'avez alors besoin d'assurer la sécurité des exceptions qu'une seule fois (dans la conception/implémentation). En utilisant finally, vous devez assurer la sécurité des exceptions à chaque fois que vous utilisez un objet.
De plus, le code de l'OMI est plus clair (voir ci-dessous).
Exemple :
Un objet de base de données. Pour s'assurer que la connexion à la base de données est utilisée, elle doit être ouverte et fermée. En utilisant RAII, cela peut être fait dans le constructeur/destructeur.
void someFunc()
{
DB db("DBDesciptionString");
// Use the db object.
} // db goes out of scope and destructor closes the connection.
// This happens even in the presence of exceptions.
L'utilisation de RAII rend très facile l'utilisation correcte d'un objet DB. L'objet DB se fermera correctement par l'utilisation d'un destructeur, quelle que soit la façon dont nous essayons d'en abuser.
void someFunc()
{
DB db = new DB("DBDesciptionString");
try
{
// Use the db object.
}
finally
{
// Can not rely on finaliser.
// So we must explicitly close the connection.
try
{
db.close();
}
catch(Throwable e)
{
/* Ignore */
// Make sure not to throw exception if one is already propagating.
}
}
}
Enfin, l'utilisation correcte de l'objet est déléguée à l'utilisateur de l'objet. c'est-à-dire Il incombe à l'utilisateur de l'objet de fermer correctement et explicitement la connexion à la base de données. Maintenant, vous pourriez argumenter que cela peut être fait dans le finisseur, mais les ressources peuvent avoir une disponibilité limitée ou d'autres contraintes et donc vous voulez généralement contrôler la libération de l'objet et ne pas compter sur le comportement non déterministe du garbage collector.
Il s'agit également d'un exemple simple.
Lorsque vous avez plusieurs ressources qui doivent être libérées, le code peut devenir compliqué.
Une analyse plus détaillée peut être trouvée ici : http://accu.org/index.php/journals/236
// Make sure not to throw exception if one is already propagating.
Il est important que les destructeurs C++ ne lèvent pas d'exceptions pour cette même raison.
@Cemafor : La raison pour laquelle le C++ ne lance pas d'exceptions en dehors du destructeur est différente de celle de Java. En Java, cela fonctionne (vous perdez juste l'exception originale). En C++, c'est vraiment mauvais. Mais l'intérêt du C++ est que vous n'avez à le faire qu'une seule fois (par le concepteur de la classe) lorsqu'il écrit le destructeur. En Java, vous devez le faire au moment de l'utilisation. C'est donc à l'utilisateur de la classe qu'il incombe d'écrire chaque fois le même texte passe-partout.
Si c'est une question d'être "nécessaire", vous n'avez pas besoin de RAII non plus. Débarrassons-nous-en ! :-) Blagues à part, RAII est très bien pour beaucoup de cas. Ce que RAII rend plus encombrant, c'est le cas où vous voulez exécuter du code (non lié aux ressources) même si le code ci-dessus est retourné prématurément. Pour cela, soit vous utilisez des gotos, soit vous le séparez en deux méthodes.
pourquoi même les langages gérés fournissent-ils un bloc final alors que les ressources sont de toute façon automatiquement désallouées par le ramasseur de déchets ?
En fait, les langages basés sur les collecteurs d'ordures ont davantage besoin de "finally". Un collecteur d'ordures ne détruit pas vos objets en temps voulu, on ne peut donc pas compter sur lui pour nettoyer correctement les problèmes non liés à la mémoire.
En ce qui concerne les données allouées dynamiquement, nombreux sont ceux qui soutiennent que vous devriez utiliser des pointeurs intelligents.
Cependant...
Le RAII transfère la responsabilité de la sécurité des exceptions de l'utilisateur de l'objet au concepteur.
Malheureusement, c'est sa propre perte. Les vieilles habitudes de programmation en C ont la vie dure. Si vous utilisez une bibliothèque écrite en C ou dans un style très C, le RAII n'aura pas été utilisé. À moins de réécrire l'ensemble de l'API frontale, c'est avec cela que vous devez travailler. Puis l'absence de "enfin" mord vraiment.
Exactement... RAII semble bien d'un point de vue idéal. Mais je dois travailler avec des API C conventionnelles tout le temps (comme les fonctions de style C dans l'API Win32...). Il est très fréquent d'acquérir une ressource qui renvoie une sorte de HANDLE, qui nécessite ensuite une fonction comme CloseHandle(HANDLE) pour être nettoyée. L'utilisation de try ... finally est une bonne façon de gérer les exceptions possibles. (Heureusement, il semble que shared_ptr avec les deleters personnalisés et les lambdas C++11 devraient fournir un soulagement basé sur RAII qui ne nécessite pas d'écrire des classes entières pour envelopper une API que je n'utilise qu'à un seul endroit).
@JamesJohnston, il est très facile d'écrire une classe enveloppante qui contient n'importe quel type de poignée et fournit les mécanismes RAII. ATL en fournit un grand nombre par exemple. Il semble que vous considériez que c'est trop difficile, mais je ne suis pas d'accord, elles sont très petites et faciles à écrire.
Simple oui, petit non. La taille dépend de la complexité de la bibliothèque avec laquelle vous travaillez.
En plus de faciliter le nettoyage des objets basés sur la pile, RAII est également utile car le même nettoyage "automatique" se produit lorsque l'objet est membre d'une autre classe. Lorsque la classe propriétaire est détruite, la ressource gérée par la classe RAII est nettoyée car le dtor de cette classe est appelé en conséquence.
Cela signifie que lorsque vous atteignez le nirvana du RAII et que tous les membres d'une classe utilisent le RAII (comme les pointeurs intelligents), vous pouvez vous en sortir avec un dtor très simple (peut-être même par défaut) pour la classe propriétaire puisqu'elle n'a pas besoin de gérer manuellement la durée de vie de ses ressources membres.
Désolé de déterrer un si vieux fil, mais il y a une erreur majeure dans le raisonnement suivant :
RAII déplace la responsabilité de la sécurité des exceptions de l'utilisateur de l'objet vers le concepteur (et l'implémenteur) de l'objet. Je dirais que c'est le bon endroit car vous n'avez alors besoin d'assurer la sécurité des exceptions qu'une seule fois (dans la conception/implémentation). En utilisant finally, vous devez assurer la sécurité des exceptions à chaque fois que vous utilisez un objet.
Le plus souvent, vous devez gérer des objets alloués dynamiquement, des nombres dynamiques d'objets, etc. Dans le bloc d'essai, un code peut créer de nombreux objets (dont le nombre est déterminé au moment de l'exécution) et stocker les pointeurs vers ces objets dans une liste. Ce n'est pas un scénario exotique, mais très courant. Dans ce cas, vous voudrez écrire des choses comme
void DoStuff(vector<string> input)
{
list<Foo*> myList;
try
{
for (int i = 0; i < input.size(); ++i)
{
Foo* tmp = new Foo(input[i]);
if (!tmp)
throw;
myList.push_back(tmp);
}
DoSomeStuff(myList);
}
finally
{
while (!myList.empty())
{
delete myList.back();
myList.pop_back();
}
}
}
Bien sûr, la liste elle-même sera détruite lorsqu'elle sortira de sa portée, mais cela ne nettoiera pas les objets temporaires que vous avez créés.
Au lieu de cela, vous devez emprunter la voie laide :
void DoStuff(vector<string> input)
{
list<Foo*> myList;
try
{
for (int i = 0; i < input.size(); ++i)
{
Foo* tmp = new Foo(input[i]);
if (!tmp)
throw;
myList.push_back(tmp);
}
DoSomeStuff(myList);
}
catch(...)
{
}
while (!myList.empty())
{
delete myList.back();
myList.pop_back();
}
}
De plus, pourquoi même les langues gérées fournissent-elles un bloc final alors que les ressources sont de toute façon automatiquement désallouées par le ramasseur de déchets ?
Conseil : il y a plus à faire avec "finally" que la simple désallocation de mémoire.
Les langages gérés ont besoin de finally-blocks précisément parce qu'un seul type de ressource est automatiquement géré : la mémoire. RAII signifie que toutes les ressources peuvent être gérées de la même manière, donc pas besoin de finally. Si vous utilisiez réellement RAII dans votre exemple (en utilisant des pointeurs intelligents dans votre liste au lieu de pointeurs nus), le code serait plus simple que votre exemple "finally". Et encore plus simple si vous ne vérifiez pas la valeur de retour de new - la vérifier est pratiquement inutile.
Vous soulevez une question importante, mais elle a deux réponses possibles. L'une est celle donnée par Myto -- utiliser des pointeurs intelligents pour toutes les allocations dynamiques. L'autre est d'utiliser des conteneurs standards, qui détruisent toujours leur contenu lors de la destruction. Dans tous les cas, chaque objet alloué est finalement possédé par un objet alloué statiquement qui le libère automatiquement lors de sa destruction. Il est vraiment dommage que ces meilleures solutions soient difficiles à découvrir pour les programmeurs en raison de la grande visibilité des pointeurs et des tableaux ordinaires.
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.
1 votes
Voir aussi les réponses dans : stackoverflow.com/questions/7779652/