• Aucun résultat trouvé

Chemins d’exécution « exceptionnels »

Dans le document Mieux programmer c++ (Page 80-86)

String EvaluerSalaireEtRenvoyerNom( Employe e ) ^4(bis)^ ^4^

4. L’argument e étant passé par valeur, le constructeur de copie Employe est appelé.

Cette opération est susceptible de générer une exception.

4bis. La fonction retournant une valeur, il est possible que le constructeur de copie de

String, invoqué pour copier le contenu de l’objet temporaire renvoyé vers le code appelant, échoue et génère une exception. On ne comptera pas ce cas comme un chemin d’exécution supplémentaire, vu qu’il peut être considéré comme externe à la fonction.

if( e.Fonction() == "PDG" || e.Salaire() > 100000 ) ^5^ ^7^ ^6^ ^11^ ^8^ ^10^ ^9^

5. L’appel à Fonction() peut générer une exception, provoquée soit par son implé-mentation interne, soit par l’échec de l’opération de copie de l’objet temporaire retourné.

6. La chaîne "PDG" est susceptible d’être converti en un objet temporaire (du même type que celui retourné par Fonction) afin de pouvoir être transmis en paramètre de l’opérateur == adéquat. Cette opération de construction peut échouer.

7. Si operator==() est une fonction redéfinie, elle est susceptible de générer une exception.

8. L’appel à Salaire() peut générer une exception, de manière tout à fait similaire à ce qui a été vu au (5).

9. La valeur 100000 peut faire l’objet d’une conversion, laquelle peut générer une exception, de manière similaire à ce qui a été vu au (6).

10. Si elle est redéfinie, la fonction operator>() peut générer une exception, de manière similaire à ce qui a été vu au (7).

11. Même remarque pour la fonction operator||().

cout<<e.Prenom()<<" "<<e.Nom()<<"est trop payé"<<endl;

^12^ ^17^ ^13^ ^14^ ^18^ ^15^ ^16^

12-16. Comme le spécifie la norme C++, chacun des cinq appels à operator<<()

peut être à l’origine d’une exception.

17-18. De manière similaire à (5), les fonctions Prenom() et Nom() peuvent générer une exception ou renvoyer un objet temporaire dont la copie peut provoquer une exception.

return e.Prenom() + " " + e.Nom();

^19^ ^22^ ^21^ ^23^ ^20^

19-20. Même remarque que 17-18.

21. Même remarque que 6.

22-23. Même remarque que pour 7, appliquée à l’opérateur +.

Nous avons pu voir dans ce problème dans quelle mesure il est parfois complexe de maîtriser parfaitement tous les chemins d’exécution possibles. Nous allons voir dans le problème suivant quel impact peut avoir cette complexité invisible sur la fiabi-lité et la facifiabi-lité de test d’un code.

La fonction ci-dessous, étudiée lors du problème précédent, est-elle robuste aux exceptions ? Autrement dit, se comporte-t-elle correctement en présence d’excep-tions et transmet-elle toutes les excepd’excep-tions au code appelant ?

String EvaluerSalaireEtRenvoyerNom( Employe e ) {

if( e.Fonction() == "PDG" || e.Salaire() > 100000 ) {

cout<<e.Prenom()<<" "<<e.Nom()<<"est trop payé"<<endl;

}

return e.Prenom() + " " + e.Nom();

}

Recommandation

Sachez où et quand des exceptions sont susceptibles d’être générées.

P

B N

° 19. C

OMPLEXITÉ DU CODE

(2

e PARTIE

) D

IFFICULTÉ

: 7

Dans ce problème, il s’agit de modifier le code du problème précédent de manière à le rendre robuste aux exceptions.

Argumentez-votre réponse. Si la fonction n’est pas robuste aux exceptions, peut-on au moins assurer qu’en cas d’exceptipeut-on, il ne se produira pas de fuite mémoire et/

ou que l’état du programme sera inchangé ? Peut-on, à l’inverse, assurer que cette fonction ne génèrera jamais d’exception ?

On considérera que toutes les fonctions appelées et les objets utilisés (incluant les objets temporaires) sont robustes aux exceptions (c’est-à-dire qu’ils peuvent générer des exceptions, mais se comportent correctement dans ce cas-là et, en particulier, ne laissent pas de ressources mémoires non désallouées).

Une remarque préliminaire au sujet des hypothèses de travail : nous avons considéré que toutes les fonctions appelées étaient robustes aux exceptions ; en prati-que, ce n’est pas toujours le cas pour les flux. Les fonctions operator<< des classes

« flux » peuvent en effet avoir des effets secondaires indésirables : il peut tout à fait arriver qu’elles traitent une partie de la chaîne qui leur est passée en paramètre, puis échouent sur une exception. Nous négligerons ce type de problèmes dans la suite.

Reprenons le code de la fonction EvaluerSalaireEtRenvoyerNom et voyons si elle est robuste aux exceptions :

String EvaluerSalaireEtRenvoyerNom( Employe e ) {

if( e.Fonction() == "PDG" || e.Salaire() > 100000 ) {

cout<<e.Prenom()<<" "<<e.Nom()<<"est trop payé"<<endl;

}

return e.Prenom() + " " + e.Nom();

}

Telle qu’elle est écrite, cette fonction garantit de ne pas provoquer de fuite mémoire en présence d’exceptions. En revanche, elle n’assure pas que l’état du pro-gramme sera inchangé si une exception se produit (autrement dit, si la fonction échoue sur une exception, elle pourra ne s’être exécutée que « partiellement »).

En effet :

Si une exception se produit entre le début et la fin de la transmission des informa-tions à cout (par exemple, si le quatrième opérateur << génère une exception), le message n’aura été que partiellement affiché1.

Si le message s’affiche correctement mais qu’une exception se produit lors de la suite de l’exécution de la fonction (lors de la préparation de la valeur de retour, par exemple), alors la fonction aura affiché un message alors qu’elle ne se sera pas exécutée entièrement.

S

OLUTION

1. Ce n’est pas dramatique dans le cas d’un message affiché à l’écran, mais pourrait l’être nettement plus, par exemple, dans le cas d’une transaction bancaire.

Enfin, la fonction ne garantit évidemment pas qu’elle ne laissera échapper aucune exception, au contraire : chacune des opérations effectuées en interne est susceptible de générer une exception et il n’y a aucun bloc try/catch !

Tentons maintenant de modifier la fonction de manière à assurer son caractère ato-mique (exécution complète ou sans effet, mais pas partielle).

Voici une première proposition :

// 1ère proposition : est-ce mieux ?

String EvaluerSalaireEtRenvoyerNom( Employe e ) {

String result = e.Prenom() + " " + e.Nom();

if( e.Fonction() == "PDG" || e.Salaire() > 100000 ) {

String message = result+"est trop payé\n";

cout << message;

}

return result;

}

Cette solution n’est pas mauvaise. Nous limitons totalement le risque d’affichage partiel, grâce à l’emploi de la variable message et du caractère « \n » au lieu de

« endl ». Il subsiste pourtant un problème, illustré dans le code client suivant :

// Un problème //

String leNom ;

leNom = EvaluerSalaireEtRenvoyerNom( Robert ) ;

La fonction renvoyant un objet par valeur, le constructeur de copie de String est appelé pour copier la valeur retournée dans l’objet « leNom ». Si cette copie échoue, la fonction se sera exécutée mais le résultat aura été irrémédiablement perdu ; on ne peut donc pas considérer qu’il s’agit là d’une fonction atomique.

Une deuxième solution serait d’utiliser une référence non constante passée en paramètre plutôt qu’une valeur de retour :

// 2ème proposition : est-ce la bonne ?

void EvaluerSalaireEtRenvoyerNom( Employe e, String& r ) {

String result = e.Prenom() + " " + e.Nom();

if( e.Fonction() == "PDG" || e.Salaire() > 100000 ) {

Recommandation

Connaissez et appliquez les principes généraux garantissant le bon comportement d’un programme en présence d’exceptions.

String message = result+"est trop payé\n";

cout << message;

}

r = result;

}

Cette solution peut paraître meilleure, du fait qu’elle évite l’utilisation d’une valeur de retour et élimine, de ce fait même, les risques liés à la copie. En réalité, elle pose exactement le même problème que la précédente, déplacé à un autre endroit : c’est maintenant l’opération d’affectation qui risque d’échouer sur une exception, ce qui aura également pour conséquence une exécution partielle de la fonction.

Pour finalement résoudre le problème, il faut avoir recours à un pointeur pointant vers un objet String alloué dynamiquement ou, mieux encore, à un pointeur automa-tique (auto_ptr) :

// 3ème proposition : la dernière et la bonne ! auto_ptr<String>

EvaluerSalaireEtRenvoyerNom( Employe e ) {

auto_ptr<String> result

= new String (e.Prenom() + " " + e.Nom());

if( e.Fonction() == "PDG" || e.Salaire() > 100000 ) {

String message = result+"est trop payé\n";

cout << message;

}

return result; // ne risque pas de lancer d’exceptions }

Cette dernière solution est la bonne. Il n’y a plus aucun risque de génération d’exception au moment de la transmission d’une valeur de retour ou au moment d’une affectation. Cette fonction satisfait tout à fait à la technique du « valider ou annuler » : si elle est interrompue par une exception, elle annule totalement les opérations en cours (aucun message affiché à l’écran, aucune valeur reçue par l’appelant). L’utilisa-tion d’un auto_ptr comme valeur de retour permet de gérer correctement la transmis-sion de la chaîne de caractère à l’appelant : si l’appelant récupère correctement la valeur de retour, il prendra le contrôle de la chaîne allouée et aura pour responsabilité de la désallouer ; si, au contraire, l’appelant ne récupère pas correctement la valeur de retour, la chaîne orpheline sera automatiquement détruite au moment de la destruction de la variable automatique result. Nous n’avons obtenu cette robustesse aux excep-tions qu’au prix d’une petite perte de performance due à l’allocation dynamique. Les bénéfices retirés au niveau de la qualité du code en valent largement la peine.

Nous avons finalement réussi, avec la troisième proposition, à obtenir une implé-mentation de la fonction se comportant de manière atomique1 (c’est-à-dire dont l’exé-1. Comme nous l’avons indiqué précédemment, nous négligeons ici le fait que les

opéra-teurs sur les flux peuvent générer des exceptions.

cution est soit totale, soit sans effet). Ceci a été possible car nous avons réussi à assurer que les deux actions externes (affichage d’une chaîne à l’écran, renvoi d’une valeur chaîne au code appelant) de la fonction soient exécutées toutes les deux en cas de réussite ou ne soient ni l’une ni l’autre exécutées en cas d’exception.

Il n’est pas toujours possible d’arriver à ce résultat, surtout avec des fonctions ayant des actions externes trop nombreuses ou trop découplées (par exemple, une fonction réalisant une écriture sur cout puis une écriture sur cerr poserait problème).

Dans ce type de cas, la seule solution viable est souvent la scission de la fonction à traiter en plusieurs fonctions. C’est donc, une nouvelle fois, l’occasion d’insister sur les bénéfices apportés par une bonne modularité du code.

En conclusion :

L’obtention d’une robustesse importante aux exceptions ne s’effectue générale-ment (mais pas toujours) qu’au prix d’un sacrifice en terme de performances.

Il est général difficile de rendre « atomique » une fonction ayant plusieurs actions extérieures. En revanche, le problème peut être en général être résolu par le découpage de la fonction à traiter en plusieurs fonctions.

Il n’est pas toujours indispensable de rendre une fonction parfaitement robuste aux exceptions. En l’occurrence, la première des trois propositions présentées plus haut sera a priori amplement suffisante pour la majorité des utilisateurs et permettra de plus d’éviter la légère perte de performance qu’impose la troisième proposition.

Dernière petite remarque : dans tout ce problème, nous avons négligé le fait que la fonction operator<<() de la classe ostream puisse ne pas se comporter correctement en présence d’exceptions (c’est d’ailleurs le cas pour toutes les classes de type

« flux »). Notre hypothèse initiale selon laquelle toutes les fonctions internes à Eva-luerSalaireEtRenvoyerNom étaient robustes aux exceptions n’était donc pas tout à fait pertinente. Pour être parfaite, notre implémentation devrait gérer par un bloc try/

catch les exceptions pouvant être générées par l’opérateur << puis les relancer vers l’appelant en ayant, au passage, réinitialisé le statut d’erreur de l’objet cout.

Recommandation

Efforcez-vous de toujours découper votre code de manière à ce que chaque unité d’exécu-tion (chaque module, chaque classe, chaque foncd’exécu-tion) ait une responsabilité unique et bien définie.

Dans le document Mieux programmer c++ (Page 80-86)