Pourquoi n'y a-t-il pas de construction "finale" en C ++?

57

La gestion des exceptions en C ++ est limitée à essayer / lancer / attraper. Contrairement à Object Pascal, Java, C # et Python, même en C ++ 11, la finallyconstruction n'a pas été implémentée.

J'ai vu énormément de littérature C ++ parler de "code sécurisé d'exception". Lippman écrit que le code sécurisé d'exception est un sujet important mais complexe et complexe, qui dépasse le cadre de son introduction, ce qui semble impliquer que le code sécurisé n'est pas fondamental pour C ++. Herb Sutter consacre 10 chapitres à ce sujet dans son exceptionnel C ++!

Pourtant, il me semble que bon nombre des problèmes rencontrés lors de la tentative d'écriture de "code sécurisé d'exception" pourraient être assez bien résolus si la finallyconstruction était implémentée, permettant ainsi au programmeur de s'assurer que même en cas d'exception, le programme puisse être restauré. à un état sûr, stable, sans fuite, proche du point d’allocation des ressources et du code potentiellement problématique. En tant que programmeur Delphi et C # très expérimenté, j'utilise try .. finally, mais la plupart des programmeurs de ces langages bloquent assez largement dans mon code.

Considérant tous les "cloches et sifflets" mis en œuvre dans C ++ 11, j'ai été étonné de constater que "finalement" n'était toujours pas là.

Alors, pourquoi la finallyconstruction n'a-t-elle jamais été implémentée en C ++? Ce n'est vraiment pas un concept très difficile ou avancé à comprendre et cela aide énormément le programmeur à écrire du «code protégé par exception».

Vecteur
la source
25
Pourquoi non finalement? Parce que vous libérez des objets dans le destructeur qui se déclenche automatiquement lorsque l'objet (ou le pointeur intelligent) quitte la portée. Les destructeurs sont supérieurs à finally {} car ils séparent le flux de travail de la logique de nettoyage. Tout comme vous ne voudriez pas que les appels soient gratuits (), encombrant votre flux de travail dans un langage malpropre.
mike30
8
Poser la question "Pourquoi n'y a-t-il pas finallyde langage C ++ et quelles techniques de gestion des exceptions sont utilisées à sa place?" est valide et sujet pour ce site. Les réponses existantes couvrent bien ceci, je pense. En transformant cela en une discussion sur "Les raisons pour lesquelles les concepteurs C ++ ont choisi de ne pas inclure la finallypeine de gagner?" et "Devrait- finallyon ajouter au C ++?" et poursuivre la discussion à travers les commentaires sur la question et chaque réponse ne correspond pas au modèle de ce site de questions-réponses.
Josh Kelley
2
Si vous avez enfin, vous avez déjà une séparation des problèmes: le bloc de code principal est ici, et le problème de nettoyage est pris en charge ici.
Kaz
2
@ Kaz. La différence est implicite vs nettoyage explicite. Un destructeur vous donne un nettoyage automatique similaire à celui utilisé pour nettoyer une ancienne primitive lorsqu'elle sort de la pile. Vous n'avez pas besoin de faire des appels de nettoyage explicites et pouvez vous concentrer sur votre logique principale. Imaginez à quel point il serait compliqué de nettoyer les primitives allouées à une pile dans un essai / finalement. Le nettoyage implicite est supérieur. La comparaison de la syntaxe de classe avec des fonctions anonymes n'est pas pertinente. Cependant, en passant des fonctions de première classe à une fonction qui libère une poignée, le nettoyage manuel peut être centralisé.
mike30

Réponses:

57

Quelques commentaires supplémentaires sur la réponse de @ Nemanja (qui, puisqu'il cite Stroustrup, est vraiment la meilleure des réponses que vous puissiez obtenir):

Il s’agit vraiment de comprendre la philosophie et les idiomes du C ++. Prenons l'exemple d'une opération qui ouvre une connexion de base de données sur une classe persistante et doit s'assurer qu'elle ferme cette connexion si une exception est levée. Ceci est une question de sécurité des exceptions et s'applique à tout langage avec des exceptions (C ++, C #, Delphi ...).

Dans une langue qui utilise try/ finally, le code pourrait ressembler à ceci:

database.Open();
try {
    database.DoRiskyOperation();
} finally {
    database.Close();
}

Simple et simple. Il y a cependant quelques inconvénients:

  • Si le langage n'a pas de destructeurs déterministes, je dois toujours écrire le finallybloc, sinon je perds des ressources.
  • Si DoRiskyOperationest plus qu'un appel de méthode - si j'ai un traitement à effectuer dans le trybloc - alors l' Closeopération peut finir par être un bit décent loin de l' Openopération. Je ne peux pas écrire mon nettoyage juste à côté de mon acquisition.
  • Si plusieurs ressources doivent être acquises, puis libérées de manière extrêmement sûre, je peux me retrouver avec plusieurs couches de try/ finallyblocs en profondeur .

L'approche C ++ ressemblerait à ceci:

ScopedDatabaseConnection scoped_connection(database);
database.DoRiskyOperation();

Cela résout complètement tous les inconvénients de l' finallyapproche. Il présente quelques inconvénients, mais ils sont relativement mineurs:

  • Vous avez de bonnes chances d’écrire ScopedDatabaseConnectionvous-même le cours. Cependant, c'est une implémentation très simple - seulement 4 ou 5 lignes de code.
  • Cela implique la création d'une variable locale supplémentaire - dont vous n'êtes apparemment pas fan - d'après votre commentaire sur "la création et la destruction constantes de classes sur lesquelles s'appuyer sur leurs destructeurs pour nettoyer votre désordre est très médiocre" - mais un bon compilateur optimisera aucun des travaux supplémentaires qu’une variable locale supplémentaire implique. Une bonne conception C ++ repose beaucoup sur ces optimisations.

Personnellement, compte tenu de ces avantages et inconvénients, je trouve que la RAII est une technique bien préférable finally. Votre kilométrage peut varier.

Enfin, RAII étant un langage si bien établi en C ++, et pour soulager les développeurs de l’écriture de nombreuses Scoped...classes, il existe des bibliothèques telles que ScopeGuard et Boost.ScopeExit qui facilitent ce type de nettoyage déterministe.

Josh Kelley
la source
8
C # a l' usinginstruction, qui nettoie automatiquement tout objet implémentant l' IDisposableinterface. Ainsi, s’il est possible de se tromper, il est assez facile de le faire correctement.
Robert Harvey
18
Devoir écrire une classe entièrement nouvelle pour gérer l'inversion temporaire de changement d'état, en utilisant un idiome de conception implémenté par le compilateur avec une try/finallyconstruction, car le compilateur n'expose pas de try/finallyconstruction et le seul moyen d'y accéder consiste à utiliser la classe idiome de conception, n’est pas un "avantage"; c'est la définition même d'une inversion d'abstraction.
Mason Wheeler
15
@ MasonWheeler - Euh, je n'ai pas dit que le fait d'écrire une nouvelle classe était un avantage. J'ai dit que c'est un inconvénient. Sur l'équilibre, cependant, je préfère RAII à avoir à utiliser finally. Comme je l'ai dit, votre kilométrage peut varier.
Josh Kelley
7
@ JoshKelley: "Une bonne conception C ++ s'appuie beaucoup sur ce type d'optimisation." Écrire des gobs de code étranger puis s’appuyer sur l’optimisation du compilateur est une bonne conception ? OMI c'est l'antithèse d'un bon design. Un code concis, facilement lisible, est l’un des principes fondamentaux d’un bon design. Moins de déboguer, moins de maintenir, etc. etc. Vous ne devriez PAS écrire des gobs de code et ensuite vous fier au compilateur pour tout faire disparaître - une OMI qui n'a aucun sens!
Vecteur
14
@Mikey: Donc, la duplication du code de nettoyage (ou le fait que le nettoyage doit avoir lieu) dans tout le code-base est "concise" et "facilement lisible"? Avec RAII, vous écrivez ce code une fois, et il est automatiquement appliqué partout.
Mankarse
55

De Pourquoi pas C ++ fournir une construction « enfin »? dans la FAQ sur le style et la technique C ++ de Bjarne Stroustrup :

Parce que C ++ prend en charge une alternative presque toujours meilleure: la technique "L’acquisition des ressources est une initialisation" (TC ++ PL3, section 14.4). L'idée de base est de représenter une ressource par un objet local, de sorte que le destructeur de l'objet local libère la ressource. De cette façon, le programmeur ne peut pas oublier de libérer la ressource.

Nemanja Trifunovic
la source
5
Mais il n'y a rien dans cette technique qui soit spécifique au C ++, n'est-ce pas? Vous pouvez créer des RAII dans n’importe quel langage avec des objets, des constructeurs et des destructeurs. C'est une technique géniale, mais RAII simplement existant ne signifie pas qu'une finallyconstruction est toujours inutile pour toujours, malgré ce que dit Strousup. Le simple fait que l'écriture de "code sécurisé d'exception" soit une grosse affaire en C ++ en est la preuve. Heck, C # a les deux destructeurs et finally, et ils s'habituent tous les deux .
Tacroy
28
@Tacroy: C ++ est l'un des très rares langages grand public à posséder des destructeurs déterministes . Les "destructeurs" en C # sont inutiles à cette fin et vous devez écrire manuellement les blocs "en utilisant" pour avoir le RAII.
Nemanja Trifunovic
15
@ Mikey vous avez la réponse de "Pourquoi le C ++ ne fournit-il pas une construction" enfin "? directement de Stroustrup lui-même là-bas. Que pourriez-vous demander de plus? C'est pourquoi.
5
@Mikey Si vous vous inquiétez de votre code bien se comporter, en particulier les ressources ne fuient pas, lorsque des exceptions sont lancées, vous êtes se soucier de la sécurité d'exception / essayant d'exception d'écriture du code en toute sécurité. Vous ne l'appelez tout simplement pas ainsi et, en raison des différents outils disponibles, vous le mettez en œuvre différemment. Mais c’est exactement ce dont parlent les gens C ++ quand ils discutent de la sécurité des exceptions.
19
@Kaz: Je dois seulement me rappeler de faire le nettoyage dans le destructeur une fois, et à partir de là, je n'utilise plus que l'objet. Je dois me rappeler de faire le nettoyage dans le bloc finally à chaque fois que j'utilise l'opération qui alloue.
deworde
19

La raison pour laquelle C ++ n'a pas, finallyc'est parce que cela n'est pas nécessaire en C ++. finallyest utilisé pour exécuter du code indépendamment du fait qu'une exception se soit produite ou non, ce qui est presque toujours une sorte de code de nettoyage. En C ++, ce code de nettoyage doit figurer dans le destructeur de la classe appropriée et ce destructeur sera toujours appelé, exactement comme un finallybloc. Le langage utilisé pour utiliser le destructeur lors de votre nettoyage s’appelle RAII .

Au sein de la communauté C ++, on parle peut-être davantage de code «sauf exception», mais il est presque tout aussi important dans les autres langages dotés d'exceptions. L'intérêt du code "exception sûre" est que vous réfléchissiez à l'état dans lequel se trouve votre code si une exception se produit dans l'une des fonctions / méthodes que vous appelez.
En C ++, le code 'exception safe' est légèrement plus important, car C ++ ne dispose pas d'un garbage collection qui prend en charge les objets laissés orphelins à la suite d'une exception.

La raison pour laquelle la sécurité des exceptions est davantage discutée dans la communauté C ++ provient probablement aussi du fait qu'en C ++, vous devez être plus conscient de ce qui peut mal tourner, car il existe moins de filets de sécurité par défaut dans le langage.

Bart van Ingen Schenau
la source
2
Remarque: Veuillez ne pas prétendre que C ++ a des destructeurs déterministes. Object Pascal / Delphi possède également des destructeurs déterministes, mais prend également en charge «Enfin», pour les très bonnes raisons que j'ai expliquées dans mes premiers commentaires ci-dessous.
Vecteur
13
@Mikey: Etant donné qu'il n'y a jamais eu de proposition pour ajouter finallyà la norme C ++, je pense qu'il est prudent de conclure que la communauté C ++ ne considère pas the absence of finallyun problème. La plupart des langages qui en sont finallydépourvus n'ont pas la destruction déterministe cohérente dont dispose le C ++. Je vois que Delphi les a tous les deux, mais je ne connais pas suffisamment son histoire pour savoir laquelle était la première.
Bart van Ingen Schenau
3
Dephi ne prend pas en charge les objets basés sur la pile, mais uniquement les objets basés sur le tas et les références d'objets sur la pile. Par conséquent, "finally" est nécessaire pour appeler explicitement les destructeurs, etc., le cas échéant.
Vecteur
2
Il y a beaucoup de choses cruelles en C ++ qui ne sont sans doute pas nécessaires, alors cela ne peut pas être la bonne réponse.
Kaz
15
Au cours des deux décennies écoulées, j’ai utilisé le langage et travaillé avec d’autres personnes qui l’utilisaient. Je n’ai jamais rencontré de programmeur C ++ en activité qui a dit "Je souhaite vraiment que le langage ait un finally". Je ne me souviendrais jamais d'une tâche que cela aurait facilitée si j'avais pu y accéder.
Gort le robot
12

D'autres ont évoqué la solution RAII. C'est une très bonne solution. Mais cela ne dit pas vraiment pourquoi ils n’ont pas ajouté finallyautant car c’est une chose largement souhaitée. La réponse à cette question est plus fondamentale pour la conception et le développement de C ++: tout au long du développement de C ++, les personnes impliquées ont fermement résisté à l'introduction de fonctionnalités de conception pouvant être obtenues à l'aide d'autres fonctionnalités sans grande difficulté, en particulier lorsque cela nécessite l'introduction. de nouveaux mots-clés qui pourraient rendre l'ancien code incompatible. Comme RAII fournit une alternative très fonctionnelle à finallyet que vous pouvez réellement rouler vous-même finallyen C ++ 11, il y avait peu d’appel à faire.

Tout ce que vous avez à faire est de créer une classe Finallyqui appelle la fonction transmise à son constructeur dans son destructeur. Ensuite, vous pouvez faire ceci:

try
{
    Finally atEnd([&] () { database.close(); });

    database.doRisky();
}

En général, la plupart des programmeurs C ++ natifs préfèrent cependant les objets RAII conçus avec soin.

Jack Aidley
la source
3
Il vous manque la capture de référence dans votre lambda. Devrait être Finally atEnd([&] () { database.close(); });aussi, j'imagine que ce qui suit est préférable: { Finally atEnd(...); try {...} catch(e) {...} }(J'ai sorti le finaliseur du bloc try afin qu'il s'exécute après les blocs catch.)
Thomas Eding
2

Vous pouvez utiliser un modèle "trap" - même si vous ne souhaitez pas utiliser le bloc try / catch.

Placez un objet simple dans la portée requise. Dans le destructeur de cet objet, mettez votre logique "finale". Quoi qu'il en soit, lorsque la pile sera déroulée, le destructeur de l'objet sera appelé et vous obtiendrez votre bonbon.

Arie R
la source
1
Cela ne répond pas à la question et prouve simplement que ce n'est finalement pas une si mauvaise idée après tout ...
Vector
2

Eh bien, vous pourriez trier par vous-même finally, en utilisant Lambdas, ce qui donnerait ce qui suit pour compiler correctement (en utilisant un exemple sans RAII bien sûr, pas le plus beau morceau de code):

{
    FILE *file = fopen("test","w");

    finally close_the_file([&]{
        cout << "We're closing the file in a pseudo-finally clause." << endl;
        fclose(file);
    });
}

Voir cet article .

einpoklum - réintègre Monica
la source
-2

Je ne suis pas sûr d’être d’accord avec les affirmations que RAII est un sur-ensemble de finally. Le talon d’Achille de RAII est simple: exceptions. La RAII est implémentée avec des destructeurs, et il est toujours faux en C ++ de se débarrasser d'un destructeur. Cela signifie que vous ne pouvez pas utiliser RAII lorsque vous avez besoin de votre code de nettoyage. En finallyrevanche, si elles étaient mises en œuvre, il n’y aurait aucune raison de penser qu’il serait illégal de lancer à partir d’un finallybloc.

Considérons un chemin de code comme ceci:

void foo() {
    try {
        ... stuff ...
        complex_cleanup();
    } catch (A& a) {
        handle_a(a);
        complex_cleanup();
        throw;
    } catch (B& b) {
        handle_b(b);
        complex_cleanup();
        throw;
    } catch (...) {
        handle_generic();
        complex_cleanup();
        throw;
    }
}

Si nous avions finallynous pourrions écrire:

void foo() {
    try {
        ... stuff ...
    } catch (A& a) {
        handle_a(a);
        throw;
    } catch (B& b) {
        handle_b(b);
        throw;
    } catch (...) {
        handle_generic();
        throw;
    } finally {
        complex_cleanup();
    }
}

Mais il n’ya aucun moyen, que je puisse trouver, d’obtenir un comportement équivalent en utilisant RAII.

Si quelqu'un sait comment faire cela en C ++, la réponse m'intéresse beaucoup. Je serais même satisfait de quelque chose qui s'appuie, par exemple, sur l'application de toutes les exceptions héritées d'une même classe avec des capacités spéciales ou autres.

Scientifique fou
la source
1
Dans votre deuxième exemple, si complex_cleanuppeut lancer, vous pouvez avoir un cas où deux exceptions non capturées volent en même temps, comme vous le feriez avec RAII / destructors, et que C ++ refuse de permettre cela. Si vous souhaitez que l'exception d'origine soit visible, vous devez complex_cleanupempêcher toute exception, comme ce serait le cas avec RAII / destructors. Si vous souhaitez que complex_cleanupl’exception soit vue, alors je pense que vous pouvez utiliser des blocs imbriqués try / catch - bien qu’il s’agisse d’une tangente et qu’il est difficile de s’intégrer dans un commentaire, elle mérite donc une question distincte.
Josh Kelley
Je veux utiliser RAII pour obtenir un comportement identique au premier exemple, avec plus de sécurité. Un lancer dans un finallybloc putatif fonctionnerait clairement de la même manière qu'un jet dans un catchbloc - exceptions en vol WRT - ne pas appeler std::terminate. La question est "pourquoi non finallyen C ++?" et toutes les réponses disent "vous n'en avez pas besoin ... RAII FTW!" Ce que je veux dire, c’est que oui, RAII convient pour des cas simples comme la gestion de la mémoire, mais jusqu’à ce que le problème des exceptions soit résolu, il faut trop de réflexion / de travail en profondeur / de préoccupation / de conception pour devenir une solution polyvalente.
MadScientist
3
Je comprends votre point de vue - il y a des problèmes légitimes avec les destructeurs qui pourraient causer - mais ceux-ci sont rares. Dire que les exceptions RAII + ont des problèmes non résolus ou que RAII n'est pas une solution polyvalente ne correspond tout simplement pas à l'expérience de la plupart des développeurs C ++.
Josh Kelley
1
Si vous vous trouvez confronté à la nécessité de générer des exceptions dans les destructeurs, vous faites quelque chose de mal: utilisez probablement des pointeurs ailleurs, quand ils ne sont pas nécessaires.
Vecteur
1
C'est trop compliqué pour les commentaires. Posez une question à ce sujet: comment géreriez-vous ce scénario en C ++ en utilisant le modèle RAII ... cela ne semble pas fonctionner ... Encore une fois, vous devriez diriger vos commentaires : tapez @ et le nom du membre dont vous parlez. au début de votre commentaire. Lorsque les commentaires sont sur votre propre message, vous êtes averti de tout, mais les autres ne le font pas, sauf si vous leur envoyez un commentaire.
Vecteur