web-dev-qa-db-fra.com

Pourquoi n'y a-t-il pas de construction «enfin» en C ++?

La gestion des exceptions en C++ est limitée à try/throw/catch. Contrairement à Object Pascal, Java, C # et Python, même en C++ 11, la construction finally n'a pas été implémentée.

J'ai vu énormément de littérature C++ discutant du "code d'exception". Lippman écrit que le code sécurisé d'exception est un sujet important mais avancé et difficile, au-delà de la portée de son introduction - ce qui semble impliquer que le code sécurisé n'est pas fondamental pour C++. Herb Sutter consacre 10 chapitres au 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 de sécurité d'exception" pourraient être assez bien résolus si la construction finally était implémentée, permettant au programmeur de s'assurer que même en cas d'exception , le programme peut être restauré dans un état sûr, stable et sans fuite, à proximité 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 .. finalement bloque assez largement dans mon code, comme le font la plupart des programmeurs dans ces langages.

Compte tenu de 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 construction finally 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 va beaucoup pour aider le programmeur à écrire du "code d'exception".

60
Vector

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

Dans un langage qui utilise try/finally, le code pourrait ressembler à ceci:

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

Simple et direct. Il existe cependant quelques inconvénients:

  • Si le langage n'a pas de destructeurs déterministes, j'ai toujours pour écrire le bloc finally, sinon je perds des ressources.
  • Si DoRiskyOperation est plus qu'un appel de méthode unique - si j'ai du traitement à faire dans le bloc try - alors l'opération Close peut finir par être un peu décente l'opération Open. Je ne peux pas écrire mon nettoyage juste à côté de mon acquisition.
  • Si j'ai plusieurs ressources qui doivent être acquises puis libérées d'une manière sûre, je peux me retrouver avec plusieurs couches de blocs try/finally.

L'approche C++ ressemblerait à ceci:

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

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

  • Il y a de fortes chances que vous ayez besoin d'écrire la classe ScopedDatabaseConnection vous-même. Cependant, c'est une implémentation très simple - seulement 4 ou 5 lignes de code.
  • Cela implique de créer une variable locale supplémentaire - dont vous n'êtes apparemment pas fan, basée sur votre commentaire sur "créer et détruire constamment des classes pour s'appuyer sur leurs destructeurs pour nettoyer votre gâchis est très pauvre" - mais un bon compilateur optimisera sur tout le travail supplémentaire qu'une variable locale supplémentaire implique. Une bonne conception C++ repose beaucoup sur ce type d'optimisations.

Personnellement, compte tenu de ces avantages et inconvénients, je trouve RAII (L'acquisition de ressources est l'initialisation) une technique bien préférable à finally. Votre kilométrage peut varier.

Enfin, parce que RAII est un idiome bien établi en C++, et pour soulager les développeurs d'une partie du fardeau de l'écriture de nombreux Scoped... classes, il existe des bibliothèques comme ScopeGuard et Boost.ScopeExit qui facilitent ce type de nettoyage déterministe.

58
Josh Kelley

De ( Pourquoi C++ ne fournit-il pas une construction "finalement"? dans le style C++ de Bjarne Stroustrup et FAQ technique :

Parce que C++ prend en charge une alternative presque toujours meilleure: la technique "l'acquisition de ressources est l'initialisation" (TC++ PL3 section 14.4). L'idée de base est de représenter une ressource par un objet local, afin 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.

58

La raison pour laquelle C++ n'a pas finally est parce qu'il n'est pas nécessaire en C++. finally est utilisé pour exécuter du code, qu'une exception se soit produite ou non, qui est presque toujours une sorte de code de nettoyage. En C++, ce code de nettoyage devrait être dans le destructeur de la classe appropriée et le destructeur sera toujours appelé, tout comme un bloc finally. L'idiome de l'utilisation du destructeur pour votre nettoyage s'appelle RAII .

Au sein de la communauté C++, il peut être plus question de code "sans exception", mais il est presque tout aussi important dans d'autres langages qui ont des exceptions. L'intérêt du code "à l'exception des exceptions" est que vous pensez dans quel état votre code est laissé si une exception se produit dans l'une des fonctions/méthodes que vous appelez.
En C++, le code "sans exception" est légèrement plus important, car C++ n'a pas de récupération de place automatique qui prend en charge les objets qui restent orphelins en raison d'une exception.

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

D'autres ont discuté du RAII comme solution. C'est une très bonne solution. Mais cela ne résout pas vraiment pourquoi ils n'ont pas ajouté finally aussi, 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 fortement résisté à l'introduction de fonctionnalités de conception qui peuvent être obtenues en utilisant d'autres fonctionnalités sans énormément de bruit et surtout lorsque cela nécessite l'introduction. de nouveaux mots clés qui pourraient rendre l'ancien code incompatible. Étant donné que RAII fournit une alternative hautement fonctionnelle à finally et que vous pouvez réellement rouler votre propre finally en C++ 11 de toute façon, il n'y avait pas grand-chose à faire.

Il vous suffit de créer une classe Finally qui appelle la fonction passée à son constructeur dans son destructeur. Ensuite, vous pouvez le faire:

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

    database.doRisky();
}

Cependant, la plupart des programmeurs C++ natifs préfèrent en général les objets RAII bien conçus.

13
Jack Aidley

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

Mettez 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 est déroulée, le destructeur d'objet sera appelé et vous obtiendrez votre bonbon.

3
Aryéh Radlé

Eh bien, vous pouvez trier votre propre finally, en utilisant Lambdas, qui obtiendrait 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 .

2
einpoklum