I. Article original▲
Cet article est la traduction du billet Return-code vs. Exception handling par Aaron Lahman.
II. Introduction▲
C'est une des guerres de religion dans la théorie des langages de programmation : faut-il choisir de retourner un code d'erreur (cette méthode sera appelée par la suite RCH, pour Return Code Handling) ou utiliser une exception (appelée par la suite EH, pour Exception Handling) ? Premièrement, mon point de vue est biaisé. Deuxièmement, j'essaierai de rester le plus objectif possible, cloisonnant mes observations sur du code utilisant ces mécanismes en fonction de ce que je pense de leur utilité. En général, je préfère utiliser les exceptions et je suis l'adage disant que « les exceptions sont à limiter aux choses exceptionnelles ».
Cela étant dit…
Raymond Chen a écrit, il y a quelque temps, un article sur le sujet. Il y indique avec exactitude quelques faits sur la revue de code suivant l'une ou l'autre approche, concluant qu'il est généralement plus difficile de déterminer la fiabilité d'un code utilisant les retours de fonctions, que celle d'un code utilisant les exceptions pour propager les erreurs(1).
Il a raison. Et il a tort. Son article est incomplet. Commençons par définir le terrain de jeu.
III. Retours de fonction▲
Ci-dessous, nous avons deux versions d'un code utilisant la valeur de retour de fonction. Toutes les erreurs sont communiquées grâce à un code de retour. La version de gauche est un exemple erroné, les chemins d'erreurs ne sont pas pris en compte. La version de droite corrige cette faute.
Sélectionnez
|
Sélectionnez
|
Le code de gauche semble très propre mais ne vérifie pas les retours de fonction. Donc, selon la méthode de Chen, il est indubitablement incorrect. Toujours selon Chen, le code de gauche est « très facile » à écrire et il est « très facile » de remarquer qu'il est incorrect. L'absence de if toutes les deux lignes est un signal d'alarme. En gros, vous devez tester tous les chemins(2) que ce soit de réussite ou d'échec, même les cas non intéressants.
Passons au code d'exemple utilisant les exceptions du billet de Chen.
IV. Exceptions (ancienne approche)▲
Ci-dessous se trouve une adaptation du code de son article en utilisant les exceptions. Le code de droite est une correction possible du code de gauche.
Sélectionnez
|
Sélectionnez
|
Je suis en accord avec n'importe quelle critique disant que le code ci-dessus est difficile à diagnostiquer. Si l'une des fonctions set_text, set_icon ou set_visible peut lancer une exception, le code de gauche est totalement fichu. Le code sur la droite est délicat… en déplaçant GetInfo() sur une nouvelle ligne et set_icon à la fin, l'objet Icon ne peut pas provoquer de fuites (en supposant que set_icon soit responsable de la libération de Icon en cas d'erreur). Le try-catch supprimera NotifyIcon et propagera l'exception, mais tout cela rend le programme pénible à déboguer : nous allons voir deux exceptions au lieu d'une seule.
Encore pire, demain, ce code pourrait devenir incorrect. Si le constructeur de copie de Info envoie une exception, le code de droite est toujours faux. Je parie que vous n'y aviez pas pensé. Vous croyez être un expert en cela ? Essayez le point numéro 18 du livre Exceptional C++. Je pense que la première fois, j'ai trouvé 16 parcours d'exécution possibles dans trois lignes de code, me faisant entrer dans la catégorie 'Guru', mais cela m'a pris quinze minutes(3). Normalement, vous n'avez que cinq secondes dans une revue de code et vous ne trouverez donc pas les vingt-trois possibilités d'exécution. Vous avez mieux à faire que d'analyser toutes les possibilités.
Si on applique les critères de Raymond Chen, avoir un code correct est 'très dur' et j'espère que mon auditoire est d'accord avec moi. C'est compliqué. N'importe quelle ligne de code, n'importe quelle sous-expression peut lancer une exception et nous devons surveiller toutes les possibilités. La méthode utilisant les retours de fonction semble meilleure. Bien sûr, nous aurons quelque chose comme neuf if pour l'exemple ci-dessus, mais au moins, on verrait que toutes les possibilités d'exécution sont couvertes. Cependant, nous ne pouvons appliquer aucune règle pour voir si le code est trivialement incorrect. Dans la méthode utilisant les retours de fonction, une ligne sans branchement conditionnel est suspecte. Pour la méthode utilisant les exceptions (ancienne méthode), une ligne sans try-catch après une acquisition de ressources est suspecte.
J'appelle cela « approche ancienne » car cela ressemble beaucoup à la façon de procéder en C# ou en C++ des années 1990, sans le support de bibliothèques. Stephan T. Lavavej distingue le C++ 'archaïque' et le C++ 'moderne' et Raymond Chen note à la fin : « Oui, il y a des modèles de programmation comme le RAII et les transactions, mais vous voyez rarement un code d'exemple les utilisant. ». Corrigeons cela.
V. Exceptions (approche moderne)▲
Sur la gauche, nous avons un mauvais code utilisant les exceptions et le RAII pour reprendre l'exemple de Chen. Sur la droite, la version corrigée :
Sélectionnez
|
Sélectionnez
|
Je change les règles. Je ne me soucie plus du nombre de branchements, que ce soit lors d'échecs ou de réussites. Par contre, je me préoccupe énormément du fait qu'à chaque instant, il n'y a qu'un seul propriétaire pour chaque ressource et qu'en cas d'échec, ce propriétaire gérera la ressource.
C'est un changement fondamental. L'approche moderne utilisant le RAII n'a pas toujours un code explicite pour les conditions d'échecs. Les échecs courants sont toujours gérés non localement. La ressource locale est immédiatement prise en charge par des objets automatiques, grâce au RAII(4), c'est-à-dire qui seront détruits à la sortie du bloc. N'importe quelle ressource brute (comme l'allocation d'un nouvel objet) doit toujours être déléguée à un objet RAII.
Détaillons ce que j'ai trouvé de mauvais dans l'exemple de gauche :
- à la première ligne, nous avons un pointeur nu, mauvais. Règle du RAII : pointeur nu = code suspect ;
- à la deuxième ligne, set_text. Aucune ressource brute, bien ;
- à la troisième ligne, shared_ptr est mélangé avec un autre appel, mauvais. À déplacer sur une autre ligne ;
- à la dernière ligne, set_visible, également correct.
Comment savons-nous que nous avons fini ? Lorsqu'il n'y a que deux sortes de lignes : les initialisations d'objets RAII et le code utile qui ne crée pas de ressources non encapsulées. Maintenant, il est vrai qu'il est dur d'utiliser correctement des classes RAII dès le début, mais cela doit être fait seulement une fois par classe au lieu d'une fois par utilisation.
Bien qu'il soit compliqué de voir toutes les possibilités d'exécution, il devient vraiment facile de vérifier si le développeur a terminé avec l'utilisation des exceptions. N'importe quelle allocation de ressource sans RAII dira que le développeur n'a pas terminé. Bien sûr, un mauvais code utilisant les retours de fonction apparaît comme une rose dans un jardin de mauvaises herbes. Mais avec le RAII, vous pouvez normalement éviter d'écrire du mauvais code dès le départ : passez directement aux shared_ptr<T> ou shared_factory<HANDLE, close_handle>::type ou score_guard<T>, etc. Vous pouvez ainsi éviter d'écrire du mauvais code et gérer toutes les erreurs avec le RAII(5).
VI. Pensées finales▲
Nous avons donc un nouveau tableau pour reconnaître le code :
Différences entre bon et mauvais code utilisant RCH |
- Y a-t-il un if pour chaque appel de fonction qui peut échouer ? |
Différences entre bon et mauvais code utilisant EH (ancienne approche) |
- Quelles lignes peuvent lancer une exception ? |
Différences entre bon et mauvais code utilisant EH (approche méthode) |
- Est-ce que les objets natifs sont directement stockés dans des objets RAII ? |
Enfin, voici mon avis sur comment une utilisation moderne des exceptions et du RAII rentre dans le tableau de Raymond Chen :
Domaine |
Facile |
Difficile |
Très difficile |
Faire du mauvais code avec RCH ? |
X |
||
Différenciation bon code mauvais code ? |
X |
||
Faire du bon code avec RCH ? |
X |
||
Faire du mauvais code avec les EH (ancienne approche) |
X |
||
Différenciation bon code mauvais code ? |
X |
||
Faire du bon code avec EH (ancienne approche) ? |
X |
||
Faire du mauvais code avec EH (approche moderne) |
X |
||
Différenciation bon code mauvais code ? |
X |
||
Faire du bon code avec EH (approche moderne) ? |
X |
Finalement, voici mon code moderne utilisant les retours de fonction ou les exceptions.
Sélectionnez
|
Sélectionnez
|
VII. Divers▲
Merci à gbdivers et Luc Hermitte pour leur aide lors de la traduction de cet article. Merci à Gurdil le nain, jacques_jean, FirePrawn et ClaudeLELOUP pour leur relecture orthographique !