Pour moi, la gestion des exceptions peut être formidable si tout est conforme à RAII et que vous réservez des exceptions pour des chemins vraiment exceptionnels (ex: un fichier corrompu en cours de lecture) et que vous n'avez jamais besoin de dépasser les limites du module et que toute votre équipe est à leurs côtés ......
EH zéro coût
De nos jours, la plupart des compilateurs mettent en œuvre une gestion des exceptions à coût nul, ce qui peut le rendre encore moins cher que la ramification manuelle sur des conditions d'erreur dans les chemins d'exécution normaux, bien qu'en échange, cela rend les chemins exceptionnels extrêmement coûteux (il y a aussi un peu de code, mais probablement pas plus que si vous avez traité à fond toutes les erreurs possibles manuellement). Le fait que lancer soit extrêmement coûteux ne devrait pas avoir d'importance si les exceptions sont utilisées de la manière prévue en C ++ (pour des circonstances vraiment exceptionnelles qui ne devraient pas se produire dans des conditions normales).
Inversion des effets secondaires
Cela dit, rendre tout conforme au RAII est beaucoup plus facile à dire qu'à faire. Il est facile pour les ressources locales stockées dans une fonction de les encapsuler dans des objets C ++ avec des destructeurs qui se nettoient, mais il n'est pas si facile d'écrire une logique d'annulation / restauration pour chaque effet secondaire qui pourrait se produire dans l'ensemble du logiciel. Considérez simplement combien il est difficile d'écrire un conteneur comme celui std::vector
qui est la séquence d'accès aléatoire la plus simple pour être parfaitement protégé contre les exceptions. Multipliez maintenant cette difficulté à travers toutes les structures de données d'un logiciel à grande échelle.
À titre d'exemple de base, considérons une exception rencontrée lors du processus d'insertion d'éléments dans un graphique de scène. Pour récupérer correctement de cette exception, vous devrez peut-être annuler ces modifications dans le graphique de la scène et restaurer le système dans un état comme si l'opération ne s'était jamais produite. Cela signifie supprimer automatiquement les enfants insérés dans le graphique de la scène lors de la rencontre d'une exception, peut-être d'un garde de portée.
Faire cela à fond dans un logiciel qui provoque de nombreux effets secondaires et traite avec beaucoup d'état persistant est beaucoup plus facile à dire qu'à faire. Souvent, je pense que la solution pragmatique est de ne pas se soucier de cela dans l'intérêt de faire expédier les choses. Avec le bon type de base de code qui a une sorte de système d'annulation central et peut-être des structures de données persistantes et le nombre minimal de fonctions provoquant des effets secondaires, vous pourriez être en mesure d'obtenir une sécurité d'exception à tous les niveaux ... mais c'est beaucoup de des trucs dont vous avez besoin si tout ce que vous allez faire est d'essayer de rendre votre code protégé contre les exceptions partout.
Il y a quelque chose de mal là-dedans, car l'inversion conceptuelle des effets secondaires est un problème difficile, mais elle est rendue plus difficile en C ++. Il est en fait parfois plus facile d'inverser les effets secondaires en C en réponse à un code d'erreur que de le faire en réponse à une exception en C ++. L'une des raisons est que tout point donné d'une fonction pourrait potentiellement throw
en C ++. Si vous essayez d'écrire un conteneur générique fonctionnant avec type T
, vous ne pouvez même pas anticiper où se trouvent ces points de sortie, car tout ce qui T
pourrait impliquer pourrait être lancé. Même en comparant les uns T
aux autres pour l'égalité pourrait jeter. Votre code doit gérer toutes les possibilités , et le nombre de possibilités se multiplie exponentiellement en C ++ alors qu'en C, elles sont beaucoup moins nombreuses.
Absence de normes
Un autre problème de gestion des exceptions est le manque de normes centrales. Il y en a comme j'espère que tout le monde convient que les destructeurs ne devraient jamais lancer car les rollbacks ne devraient jamais échouer, mais cela ne fait que couvrir un cas pathologique qui ferait généralement planter le logiciel si vous violiez la règle.
Il devrait y avoir des normes plus sensées comme ne jamais lancer depuis un opérateur de comparaison (toutes les fonctions qui sont logiquement immuables ne devraient jamais lancer), ne jamais lancer depuis un ctor de déplacement, etc. De telles garanties faciliteraient tellement l'écriture de code sans exception mais nous n'avons pas de telles garanties. Nous devons en quelque sorte respecter la règle selon laquelle tout peut être jeté - sinon maintenant, peut-être à l'avenir.
Pire encore, des personnes d'horizons linguistiques différents voient les exceptions très différemment. En Python, ils ont en fait cette philosophie de "sauter avant de regarder" qui implique de déclencher des exceptions dans les chemins d'exécution normaux! Lorsque vous obtenez alors de tels développeurs écrivant du code C ++, vous pourriez regarder des exceptions levées à gauche et à droite pour des choses qui ne sont pas vraiment exceptionnelles, et gérer tout cela peut être un cauchemar si vous essayez d'écrire du code générique, par exemple
La gestion des erreurs
La gestion des erreurs présente l'inconvénient que vous devez vérifier chaque erreur possible qui pourrait se produire. Mais cela a un avantage: parfois, vous pouvez vérifier les erreurs un peu tard avec le recul, comme glGetError
réduire considérablement les points de sortie d'une fonction et les rendre explicites. Parfois, vous pouvez continuer à exécuter le code un peu plus longtemps avant de vérifier et de propager et de récupérer après une erreur, et parfois cela peut être vraiment plus facile que la gestion des exceptions (en particulier avec l'inversion des effets secondaires). Mais vous pourriez même ne pas trop vous soucier des erreurs dans un jeu, peut-être simplement arrêter le jeu avec un message si vous rencontrez des choses comme des fichiers corrompus, des erreurs de mémoire insuffisante, etc.
Comment cela se rapporte au développement d'une bibliothèque utilisée à travers plusieurs projets.
Une partie désagréable des exceptions dans les contextes DLL est que vous ne pouvez pas jeter en toute sécurité des exceptions d'un module à un autre à moins que vous ne puissiez garantir qu'elles sont générées par le même compilateur, utiliser les mêmes paramètres, etc. Donc, si vous écrivez comme une architecture de plugin destiné aux utilisateurs de toutes sortes de compilateurs et peut-être même de différents langages comme Lua, C, C #, Java, etc., les exceptions commencent souvent à devenir une nuisance majeure car vous devez avaler chaque exception et les traduire en erreur codes de toute façon partout.
Qu'est-ce que cela fait d'utiliser des bibliothèques tierces.
S'ils sont des dylibs, ils ne peuvent pas lancer d'exceptions en toute sécurité pour les raisons mentionnées ci-dessus. Ils devraient utiliser des codes d'erreur, ce qui signifie également que vous, en utilisant la bibliothèque, devriez constamment vérifier les codes d'erreur. Ils pourraient envelopper leur dylib avec une bibliothèque d'encapsulation C ++ liée statiquement que vous construisez vous-même, qui traduit les codes d'erreur du dylib en exceptions levées (essentiellement en jetant de votre binaire dans votre binaire). En général, je pense que la plupart des bibliothèques tierces ne devraient même pas déranger et se contenter de codes d'erreur si elles utilisent des bibliothèques dylibs / partagées.
Comment cela affecte-t-il les tests unitaires, les tests d'intégration, etc.?
Je ne rencontre généralement pas autant d'équipes testant des chemins exceptionnels / d'erreur de leur code. J'avais l'habitude de le faire et j'avais en fait du code qui pouvait récupérer correctement bad_alloc
parce que je voulais rendre mon code ultra-robuste, mais cela n'aide pas vraiment quand je suis le seul à le faire dans une équipe. À la fin, j'ai cessé de me déranger. J'imagine un logiciel essentiel à la mission que des équipes entières pourraient vérifier pour s'assurer que leur code récupère solidement des exceptions et des erreurs dans tous les cas possibles, mais c'est beaucoup de temps à investir. Le temps a du sens dans les logiciels critiques, mais peut-être pas dans un jeu.
Amour-haine
Les exceptions sont également mon type de fonctionnalité d'amour / haine du C ++ - l'une des fonctionnalités les plus profondément impactantes du langage, ce qui fait que RAII ressemble plus à une exigence qu'à une commodité, car cela change la façon dont vous devez écrire du code à l'envers, par exemple , C pour gérer le fait qu'une ligne de code donnée pourrait être un point de sortie implicite pour une fonction. Parfois, je souhaite presque que la langue n'ait pas d'exceptions. Mais il y a des moments où je trouve les exceptions vraiment utiles, mais elles sont trop d'un facteur dominant pour moi si je choisis d'écrire un morceau de code en C ou C ++.
Ils seraient exponentiellement plus utiles pour moi si le comité de normalisation se concentrait vraiment sur l'établissement de normes ABI qui permettaient de diffuser des exceptions en toute sécurité entre les modules, au moins du code C ++ au code C ++. Ce serait génial si vous pouviez lancer des langues en toute sécurité, bien que ce soit un rêve de pipe. L'autre chose qui rendrait les exceptions exponentiellement plus utiles pour moi est que les noexcept
fonctions provoquent réellement une erreur de compilation si elles tentent d'appeler une fonctionnalité qui pourrait être lancée au lieu d'appeler simplement std::terminate
à rencontrer une exception. C'est presque inutile (pourrait aussi bien appeler cela à la icantexcept
place de noexcept
). Il serait tellement plus facile de garantir que tous les destructeurs sont protégés contre les exceptions, par exemple, sinoexcept
provoquait en fait une erreur du compilateur si le destructeur faisait tout ce qui pouvait être lancé. Si cela coûte trop cher pour être complètement déterminé au moment de la compilation, alors faites en sorte que les noexcept
fonctions (que tous les destructeurs implicitement devraient être) ne soient autorisées à appeler de la même manière que les noexcept
fonctions / opérateurs, et en faire une erreur du compilateur à lancer à partir de l'un d'entre eux.