Le traitement des exceptions (EH) semble être la norme actuelle, et en cherchant sur le Web, je ne peux trouver aucune idée ou méthode nouvelle qui tente de l’améliorer ou de la remplacer (enfin, il existe des variantes, mais rien de nouveau).
Bien que la plupart des gens semblent l’ignorer ou tout simplement l’accepter, EH présente d’énormes inconvénients: les exceptions sont invisibles pour le code et créent de nombreuses sorties possibles. points. Joel sur le logiciel a écrit un article à ce sujet . La comparaison avec goto
me convient parfaitement, cela m'a fait repenser à EH.
J'essaie d'éviter EH et n'utilise que des valeurs de retour, des callbacks ou tout ce qui convient à l'objectif. Mais lorsque vous devez écrire du code fiable, vous ne pouvez tout simplement pas ignorer EH ces jours-ci : cela commence par le new
, qui risque de une exception, au lieu de simplement renvoyer 0 (comme dans les vieux jours). Cela rend toute ligne de code C++ vulnérable à une exception. Et puis plus d'emplacements dans le code de base C++ génèrent des exceptions ... std lib le fait, et ainsi de suite.
Cela donne l'impression de marcher sur des terrains instables .. Alors, nous sommes maintenant obligés de nous occuper des exceptions!
Mais c'est dur, c'est vraiment dur. Vous devez apprendre à écrire le code sécurisé des exceptions, et même si vous avez une expérience de ce code, il sera toujours nécessaire de revérifier une seule ligne de code pour être sûr! Ou vous commencez à mettre des blocs try/catch partout, ce qui encombre le code jusqu'à ce qu'il atteigne un état d'illisibilité.
EH a remplacé l'ancienne approche déterministe propre (valeurs de retour ..), qui ne présentait que quelques inconvénients compréhensibles et faciles à résoudre par une approche qui crée de nombreux points de sortie possibles dans votre code, et si vous commencez à écrire du code qui capture les exceptions sont forcés de le faire à un moment donné), alors il crée même une multitude de chemins dans votre code (code dans les blocs catch, pensez à un programme serveur sur lequel vous avez besoin de fonctions de journalisation autres que std :: cerr ..). EH a des avantages, mais ce n'est pas la question.
Mes vraies questions:
Votre question fait une affirmation selon laquelle "écrire du code protégé contre les exceptions est très difficile". Je vais d'abord répondre à vos questions, puis à la question cachée qui se cache derrière elles.
Est-ce que vous écrivez vraiment le code d'exception?
Bien sur que oui.
C'est le raison Java a perdu beaucoup de son attrait pour moi en tant que programmeur C++ (manque de sémantique RAII), mais je m'éloigne du sujet: c'est une question C++.
C'est en fait nécessaire lorsque vous devez travailler avec du code STL ou Boost. Par exemple, les threads C++ (boost::thread
ou std::thread
) lèveront une exception pour quitter normalement.
Etes-vous sûr que votre dernier code "prêt pour la production" est protégé contre les exceptions?
Pouvez-vous même en être sûr?
Écrire du code protégé contre les exceptions revient à écrire du code exempt d’erreurs.
Vous ne pouvez pas être sûr à 100% que votre code est protégé contre les exceptions. Mais ensuite, vous vous y efforcez, en utilisant des modèles connus et en évitant les anti-modèles connus.
Connaissez-vous et/ou utilisez-vous réellement des alternatives qui fonctionnent?
Il n’existe aucune alternative viable en C++ (c’est-à-dire que vous devez revenir en C et éviter les bibliothèques C++, ainsi que les surprises externes telles que Windows SEH).
Pour écrire un code sécurisé d'exception, vous devez connaître en premier == le niveau de sécurité des exceptions que vous écrivez.
Par exemple, un new
peut générer une exception, mais l'attribution d'un paramètre intégré (par exemple, un int ou un pointeur) n'échoue pas. Un échange n'échouera jamais (n'écrivez jamais un échange), un std::list::Push_back
peut lancer ...
La première chose à comprendre est que vous devez pouvoir évaluer la garantie d'exception offerte par toutes vos fonctions:
Le code suivant semble être correct comme C++, mais en vérité, il offre la garantie "aucune", et par conséquent, il n’est pas correct:
void doSomething(T & t)
{
if(std::numeric_limits<int>::max() > t.integer) // 1. nothrow/nofail
t.integer += 1 ; // 1'. nothrow/nofail
X * x = new X() ; // 2. basic : can throw with new and X constructor
t.list.Push_back(x) ; // 3. strong : can throw
x->doSomethingThatCanThrow() ; // 4. basic : can throw
}
J'écris tout mon code avec ce type d'analyse en tête.
La garantie la plus basse offerte est basique, mais ensuite, l'ordre de chaque instruction rend la fonction entière "aucune", car si 3. lance, x va fuir.
La première chose à faire serait de rendre la fonction "basique", c'est-à-dire de mettre x dans un pointeur intelligent jusqu'à ce qu'il soit détenu en toute sécurité par la liste:
void doSomething(T & t)
{
if(std::numeric_limits<int>::max() > t.integer) // 1. nothrow/nofail
t.integer += 1 ; // 1'. nothrow/nofail
std::auto_ptr<X> x(new X()) ; // 2. basic : can throw with new and X constructor
X * px = x.get() ; // 2'. nothrow/nofail
t.list.Push_back(px) ; // 3. strong : can throw
x.release() ; // 3'. nothrow/nofail
px->doSomethingThatCanThrow() ; // 4. basic : can throw
}
Maintenant, notre code offre une garantie "de base". Rien ne coulera et tous les objets seront dans un état correct. Mais nous pourrions offrir plus, c’est-à-dire la garantie forte. C’est là que peut devenir coûteux, et c’est pourquoi pas tous == Le code C++ est puissant. Essayons:
void doSomething(T & t)
{
// we create "x"
std::auto_ptr<X> x(new X()) ; // 1. basic : can throw with new and X constructor
X * px = x.get() ; // 2. nothrow/nofail
px->doSomethingThatCanThrow() ; // 3. basic : can throw
// we copy the original container to avoid changing it
T t2(t) ; // 4. strong : can throw with T copy-constructor
// we put "x" in the copied container
t2.list.Push_back(px) ; // 5. strong : can throw
x.release() ; // 6. nothrow/nofail
if(std::numeric_limits<int>::max() > t2.integer) // 7. nothrow/nofail
t2.integer += 1 ; // 7'. nothrow/nofail
// we swap both containers
t.swap(t2) ; // 8. nothrow/nofail
}
Nous avons réorganisé les opérations, en créant d'abord et en mettant X
à sa valeur correcte. Si une opération échoue, alors t
n'est pas modifié. Les opérations 1 à 3 peuvent donc être considérées comme "fortes": si quelque chose se lève, t
n'est pas modifié et X
ne fuira pas. parce qu'il appartient au pointeur intelligent.
Ensuite, nous créons une copie t2
de t
et travaillons sur cette copie des opérations 4 à 7. Si quelque chose se produit, t2
est modifié, mais alors, t
est toujours l'original. Nous offrons toujours la garantie forte.
Ensuite, nous échangeons t
et t2
. Les opérations d'échange doivent être nothrow en C++, espérons donc que l'échange que vous avez écrit pour T
n'est pas notifié (si ce n'est pas le cas, réécrivez-le pour qu'il ne soit pas transmis).
Donc, si nous atteignons la fin de la fonction, tout réussit (pas besoin d'un type de retour) et t
a sa valeur exceptée. S'il échoue, alors t
a toujours sa valeur d'origine.
Maintenant, offrir la garantie forte peut coûter très cher, alors n'essayez pas d'offrir la garantie forte à tout votre code, mais si vous pouvez le faire sans coût (et l'optimisation en C++ et d'autres optimisations pourraient rendre tout le code ci-dessus sans coût) , alors fais-le. L'utilisateur de la fonction vous en remerciera.
Il faut un peu d’habitude pour écrire du code protégé contre les exceptions. Vous devrez évaluer la garantie offerte par chaque instruction que vous utiliserez, puis évaluer la garantie offerte par une liste d'instructions.
Bien sûr, le compilateur C++ ne sauvegardera pas la garantie (dans mon code, j'offre la garantie sous la forme d'une balise @warning doxygen), ce qui est un peu triste, mais cela ne devrait pas vous empêcher d'essayer d'écrire du code protégé contre les exceptions.
Comment un programmeur peut-il garantir qu'une fonction sans échec aura toujours du succès? Après tout, la fonction pourrait avoir un bogue.
C'est vrai. Les garanties d'exception sont supposées être offertes par un code sans bug. Mais alors, dans n'importe quelle langue, appeler une fonction suppose que celle-ci soit sans bug. Aucun code sain d'esprit ne se protège contre la possibilité qu'il ait un bogue. Écrivez le code du mieux que vous pouvez, puis offrez la garantie en supposant qu'il est exempt de bogues. Et s'il y a un bug, corrigez-le.
Les exceptions concernent les échecs de traitement exceptionnels, pas les bogues de code.
Maintenant, la question est "Est-ce que ça vaut le coup?".
Bien sûr que si. Avoir une fonction "nothrow/no-fail" sachant que la fonction ne va pas échouer est une aubaine. Il en va de même pour une fonction "forte", qui vous permet d'écrire du code avec une sémantique transactionnelle, telle que des bases de données, avec des fonctions de validation/annulation, la validation étant l'exécution normale du code, les exceptions étant l'annulation.
Ensuite, la "base" est la moindre garantie que vous devriez offrir. Le langage C++ est un langage très puissant, avec ses portées, qui vous permettent d’éviter toute fuite de ressources (un dépanneur aurait du mal à offrir des ressources pour la base de données, la connexion ou les descripteurs de fichiers).
Donc, autant que je sache, cela est en vaut la peine.
nobar a fait un commentaire qui, je crois, est tout à fait pertinent, car il fait partie de "comment rédigez-vous un code sécurisé d'exception":
swap()
. Il convient toutefois de noter que std::swap()
peut échouer en fonction des opérations qu'il utilise en interne.la valeur par défaut std::swap
fera des copies et des assignations qui, pour certains objets, peuvent produire. Ainsi, le swap par défaut pourrait lancer, soit utilisé pour vos classes, soit même pour les classes STL. En ce qui concerne la norme C++, l’opération d’échange pour vector
, deque
et list
ne lancera pas, alors qu’elle pourrait pour map
si le foncteur de comparaison peut jeter sur la copie construction (voir Le langage de programmation C++, édition spéciale, annexe E, E.4.3.Échange).
En regardant l'implémentation Visual C++ 2008 du swap de vecteur, le swap de vecteur ne sera pas lancé si les deux vecteurs ont le même allocateur (c'est-à-dire le cas normal), mais en effectuera des copies s'ils ont des allocateurs différents. Et donc, je suppose que cela pourrait jeter dans ce dernier cas.
Ainsi, le texte original est toujours valable: n'écrivez jamais un échange de lancement, mais rappelez-vous du commentaire de nobar: veillez à ce que les objets que vous échangez aient un échange sans lancement.
Dave Abrahams , qui nous a donné les garanties de base/fortes/nothrow , décrit dans un article son expérience concernant la sécurisation de l'exception STL:
http://www.boost.org/community/exception_safety.html
Regardez le septième point (Test automatisé pour la sécurité des exceptions), où il s’appuie sur des tests unitaires automatisés pour s’assurer que chaque cas est testé. Je suppose que cette partie est une excellente réponse à la question de l'auteur "Pouvez-vous même en être sûr?".
t.integer += 1;
n'a pas la garantie que le débordement ne se produira PAS, sauf exception, et peut en fait invoquer techniquement UB! (Le débordement signé est UB: C++ 11 5/4 "Si, lors de l'évaluation d'une expression, le résultat n'est pas défini mathématiquement ou s'il ne figure pas dans la plage de valeurs pouvant être représentées pour son type, le comportement est indéfini.") Notez que non signé les nombres entiers ne débordent pas, mais font leurs calculs dans une classe d’équivalence modulo 2 ^ # bits.
Dionadar se réfère à la ligne suivante, qui a en effet un comportement indéfini.
t.integer += 1 ; // 1. nothrow/nofail
La solution ici est de vérifier si le nombre entier est déjà à sa valeur maximale (en utilisant std::numeric_limits<T>::max()
) avant de faire l'addition.
Mon erreur irait dans la section "Échec normal contre bogue", c’est-à-dire un bogue. Cela n'invalide pas le raisonnement et ne signifie pas non plus que le code protégé contre les exceptions est inutile car impossible à atteindre. Vous ne pouvez pas vous protéger contre la mise hors tension de l'ordinateur, ni contre les bogues du compilateur, ni même vos bogues, ou d'autres erreurs. Vous ne pouvez pas atteindre la perfection, mais vous pouvez essayer de vous rapprocher le plus possible.
J'ai corrigé le code en gardant à l'esprit le commentaire de Dionadar.
Écrire du code protégé contre les exceptions en C++ ne consiste pas vraiment à utiliser beaucoup de blocs try {} catch {}. Il s'agit de documenter le type de garanties fournies par votre code.
Je recommande de lire la série Gourou de la semaine de Herb Sutter, en particulier les versements 59, 60 et 61.
Pour résumer, vous pouvez fournir trois niveaux de sécurité des exceptions:
Personnellement, j’ai découvert ces articles assez tard, une grande partie de mon code C++ n’est certainement pas à l'abri des exceptions.
Certains d'entre nous utilisent l'exception depuis plus de 20 ans. PL/I les a, par exemple. La prémisse qu’il s’agit d’une technologie nouvelle et dangereuse me semble discutable.
Tout d’abord (comme Neil l’a déclaré), SEH est la gestion des exceptions structurées de Microsoft. Il est similaire au traitement des exceptions en C++, mais pas identique. En fait, vous devez activer la gestion des exceptions C++ si vous le souhaitez dans Visual Studio - le comportement par défaut ne garantit pas que les objets locaux sont détruits dans tous les cas! Dans les deux cas, le traitement des exceptions n'est pas vraiment plus difficile , il est simplement différent .
Maintenant pour vos questions réelles.
Est-ce que vous écrivez vraiment le code d'exception?
Oui. Je m'efforce d'obtenir un code sécurisé d'exception dans tous les cas. J'évangélise en utilisant des techniques RAII pour un accès limité aux ressources (par exemple, boost::shared_ptr
pour la mémoire, boost::lock_guard
pour le verrouillage). En général, une utilisation cohérente des techniques RAII et surveillance de la portée rendra beaucoup plus facile la rédaction de code sécurisé d'exception. L'astuce consiste à apprendre ce qui existe et à l'appliquer.
Etes-vous sûr que votre dernier code "prêt pour la production" est protégé contre les exceptions?
Non, c'est aussi sûr que c'est. Je peux dire que je n’ai pas vu de défaillance de processus due à une exception au cours de plusieurs années d’activités 24h/24 et 7j/7. Je ne m'attends pas à un code parfait, juste un code bien écrit. En plus de fournir une sécurité exceptionnelle, les techniques ci-dessus garantissent une correction presque impossible à obtenir avec des blocs try
/catch
. Si vous attrapez tout dans votre périmètre de contrôle supérieur (thread, processus, etc.), vous pouvez être sûr de continuer à s'exécuter malgré des exceptions ( la plupart du temps ). Les mêmes techniques vous aideront également à continuer à exécuter correctement malgré les exceptions sans try
/catch
blocs. partout .
Pouvez-vous même être sûr que c'est?
Oui. Vous pouvez être sûr par un audit approfondi du code mais personne ne le fait vraiment? Les révisions régulières du code et les développeurs attentifs contribuent toutefois à atteindre cet objectif.
Connaissez-vous et/ou utilisez-vous réellement des alternatives qui fonctionnent?
J'ai essayé quelques variations au fil des ans, telles que le codage des états dans les bits supérieurs (ala HRESULT
s ) ou cet horrible setjmp() ... longjmp()
bidouille. Dans la pratique, ces deux phénomènes se décomposent de manière complètement différente.
En fin de compte, si vous prenez l'habitude d'appliquer quelques techniques et de bien réfléchir à l'endroit où vous pouvez réellement faire quelque chose en réponse à une exception, vous obtiendrez un code très lisible et sûr. Vous pouvez résumer ceci en suivant ces règles:
try
/catch
lorsque vous pouvez faire quelque chose à propos d'une exception spécifiquenew
ou delete
brut dans le codestd::sprintf
_, snprintf
et les tableaux en général - utilisez _std::ostringstream
_ pour formater et remplacez les tableaux par _std::vector
_ et _std::string
_Je ne peux que vous recommander d'apprendre à utiliser correctement les exceptions et à oublier les codes de résultat si vous envisagez d'écrire en C++. Si vous souhaitez éviter les exceptions, vous pouvez envisager d'écrire dans une autre langue qui soit ne les a pas ou les rend sûres . Si vous voulez vraiment apprendre à utiliser pleinement C++, lisez quelques livres de Herb Sutter , Nicolai Josuttis , et Scott Meyers .
Il n’est pas possible d’écrire du code protégé contre les exceptions en supposant que "n'importe quelle ligne peut lancer". La conception d'un code d'exception repose de manière critique sur certains contrats/garanties que vous êtes censé attendre, observer, suivre et implémenter dans votre code. Il est absolument nécessaire d’avoir un code qui est garanti jamais jeté. Il existe d'autres types de garanties d'exception.
En d’autres termes, la création d’un code protégé par les exceptions est dans une large mesure une question de programme conception pas simplement une question de simplicité codage.
Eh bien, j'ai bien l'intention de.
Je suis sûr que mes serveurs 24/7 construits à l'aide d'exceptions fonctionnent 24/7 et ne perdent pas de mémoire.
Il est très difficile d’être sûr que le code est correct. En règle générale, on ne peut aller que par résultats
Non. L'utilisation d'exceptions est plus propre et plus facile que toutes les solutions de programmation que j'ai utilisées au cours des 30 dernières années.
Laissant de côté la confusion entre les exceptions SEH et C++, vous devez savoir que des exceptions peuvent être levées à tout moment et écrire votre code dans cet esprit. Le besoin de sécurité des exceptions est en grande partie ce qui motive l'utilisation de RAII, des pointeurs intelligents et d'autres techniques C++ modernes.
Si vous suivez les modèles bien établis, l'écriture de code protégé contre les exceptions n'est pas particulièrement difficile et, en fait, plus facile que d'écrire du code qui gère correctement les retours d'erreur, dans tous les cas.
EH est bon, en général. Mais l’implémentation de C++ n’est pas très conviviale, car il est vraiment difficile de dire à quel point votre couverture d’exception est bonne. Java facilite, par exemple, le compilateur a tendance à échouer si vous ne gérez pas les exceptions possibles.
Je fais de mon mieux pour écrire du code protégé contre les exceptions, oui.
Cela signifie que je prend soin de garder un œil sur lequel lignes peuvent jeter. Tout le monde ne le peut pas et il est essentiel de garder cela à l'esprit. La clé est vraiment de penser et de concevoir votre code pour satisfaire les garanties d’exception définies dans la norme.
Cette opération peut-elle être écrite pour fournir la garantie d'exception forte? Dois-je me contenter de celui de base? Quelles lignes peuvent générer des exceptions et comment puis-je m'assurer que si elles le font, elles ne corrompent pas l'objet?
J'aime beaucoup travailler avec Eclipse et Java (pour la première fois en Java), car cela génère des erreurs dans l'éditeur si vous manquez un gestionnaire EH. Cela rend les choses beaucoup plus difficiles à oublier pour gérer une exception ...
De plus, avec les outils IDE, il ajoute automatiquement le bloc try/catch ou un autre bloc catch.
Certains d'entre nous préfèrent des langages comme Java qui nous obligent à déclarer toutes les exceptions levées par les méthodes, au lieu de les rendre invisibles comme en C++ et C #.
Lorsqu'elles sont correctement effectuées, les exceptions sont supérieures aux codes de retour d'erreur, si pour la seule raison que vous n'avez pas à propager manuellement les échecs dans la chaîne d'appels.
Cela étant dit, la programmation de bibliothèques d'API de bas niveau devrait probablement éviter la gestion des exceptions et s'en tenir aux codes de retour d'erreur.
D'après mon expérience, il est difficile d'écrire du code de gestion des exceptions en clair en C++. Je finis par utiliser new(nothrow)
beaucoup.
Est-ce que vous écrivez vraiment le code d'exception? [Il n'y a rien comme ça. Les exceptions constituent un bouclier papier contre les erreurs, sauf si vous avez un environnement géré. Ceci s'applique aux trois premières questions.]
Connaissez-vous et/ou utilisez-vous réellement des alternatives qui fonctionnent? [Alternative à quoi? Le problème ici est que les gens ne séparent pas les erreurs réelles du fonctionnement normal du programme. S'il s'agit d'une opération normale du programme (c'est-à-dire d'un fichier introuvable), il ne s'agit pas vraiment d'une gestion d'erreur. Si c'est une erreur réelle, il n'y a aucun moyen de la "gérer" ou ce n'est pas une erreur réelle. Votre objectif ici est de découvrir ce qui ne va pas, d'arrêter la feuille de calcul et de consigner une erreur, de redémarrer le pilote sur votre grille-pain ou de simplement prier pour que l'avion de combat puisse continuer à voler même s'il contient des logiciels défectueux et espère que tout ira pour le mieux.]
Beaucoup (je dirais même la plupart) font les gens.
Ce qui est vraiment important à propos des exceptions, c'est que si vous n'écrivez aucun code de traitement, le résultat est parfaitement sûr et bien tenu. Trop pressé de paniquer, mais en sécurité.
Vous devez activement faire des erreurs dans les gestionnaires pour obtenir quelque chose qui ne soit pas sécurisé, et seul le moyen (...) {} sera comparable à ignorer le code d'erreur.