OVH Cloud OVH Cloud

Définition d'une hiérarchie d'exceptions

80 réponses
Avatar
Michael
Bonsoir à tous,

achevant l'écriture de ma bibliothèque servant à utiliser l'API DirectShow,
j'en arrive au sujet un peu brulant pour moi que sont les exceptions...

Je ne sais pas comment les implémenter correctement.

J'ai distingué trois types d'exception qui peuvent être levées:

1) Les exceptions qui surgissent pendant l'initialisation des modules et
qui interdisent l'utilisation du module (initialisation d'un filtre qui
échoue par exemple, ou bien tentative de connecter deux filtres qui ne
peuvent pas être mis en relation ensemble)

2) Les exceptions qui surgissent lors de l'utilisation d'un module, mais
qui n'entravent pas son fonctionnement (impossibilité de se déplacer dans
une vidéo manuellement par exemple, impossibilité d'extraire une image
d'une vidéo)

3) Les exceptions qui sont levées suite à une action de l'utilisateur du
programme final (Problème dans le choix d'un fichier par exemple (un ZIP au
lieu d'un AVI))


Les exceptions de la première catégorie sont plutôt destinés au programmeur
qui utilise la bibliothèque et n'aident en rien l'utilisateur du programme
final, puisqu'il ne pourra rien modifier.


Les exceptions de la deuxième catégorie peuvent être utiles à l'utilisateur
de la librairie et à l'utilisateur du programme


Enfin les exceptions de la dernière catégorie sont exclusivement destinées
à l'utilisateur du programme, afin qu'il change de fichier qui doit être lu
par exemple.



Bref toujours est-il que je ne sais pas comment m'y prendre...

Est-ce que je dois encadrer les fonctions susceptibles de lever une
exception par un try catch()? Lesquelles?

Est-ce à l'utilisateur de la bibiliothèque de le faire?
L'utilisateur du programme?

Ou bien je laisse les exceptions se déclencher et c'est au programmeur de
se débrouiller comme il veut?

Un peu d'aide serait la bienvenue :)

Merci d'avance

Michael

10 réponses

Avatar
kanze
Alain Gaillard wrote:

A ce sujet une question sur le comportement de CodeGuard,
une extension de C++ Builder qui permet de tracker les
fuites mémoires...

Quand je construis un objet avec new et qu'une exception est
lancée dans le constructeur, à la sortie du programme,
CodeGuard m'indique une fuite mémoire parce que l'objet en
question n'a pas été détruit.



Normal. Il n'a pas été construit non plus. C'est bien à toi
d'assurer qu'il n'y a pas de fuites dans le constructeur quand
il sort par une exception.

Je crois bien me souvenir qu'il n'est pas besoin de faire un
delete dans ce cas.



De la mémoire allouée par le new, non. De toute façon, tu ne
pourrais pas, vu que tu n'en a pas le pointeur.

Si je comprend bien la question, C++Builder a raison.
C'est à toi de détruire l'objet par delete.


Non, non et non. Tu ne peux pas appeler delete sur l'objet,
parce que 1) il est illégal d'appeler delete sur un objet dont
le constructeur n'a pas terminé, et 2) tu n'en as pas l'adresse.

La façon de faire lorsque tu crains qu'une exception soit
levée dans ton constructeur est de mettre le code dans un bloc
try/catch. Lorsque tu captures l'exception tu effaces l'objet,
puis tu relances l'exception.

try
{
// du code
objet* = new.....
// du code qui lève une exception
}
catch(...)
{
delete objet;


Sauf que objet n'est pas visible ici.

La solution ici, c'est d'utiliser std::auto_ptr (ou
boost::scoped_ptr) pour le pointeur. Peut-être -- sans savoir
exactement pourquoi l'objet est alloué dynamiquement, on peut
pas dire.

Mais la question concernait le cas où c'est le constructeur de
l'objet qui a levé l'exception. Dans ce cas-là, tu n'as rien à
faire. Et tu ne pourrais rien faire, parce que tu n'as jamais eu
l'adresse renvoyée par la fonction operator new.

Ce qu'il faut, en revanche, c'est que ton constructeur soit
propre. Quelque chose comme :

Classe::Classe()
: pointerMember( new XXX )
{
if ( whatever ) {
throw SomeError ;
}
}

provoque bien une fuite de mémoire. Dans ce cas-ci
(ultra-simple), on pourrait bien insérer un « delete
pointerMember » avant le throw, mais dans le cas général, il
est conseillé d'utiliser des objets supplémentaires (pointeurs
intelligents, sous-classes, etc.) qui s'occuperont
automatiquement du nettoyage ; le compilateur appelle bien le
destructeur de tous les sous-objets dont le constructeur a fini.

--
James Kanze GABI Software
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34


Avatar
Alain Gaillard
Alain Gaillard wrote:



A ce sujet une question sur le comportement de CodeGuard, une
extension de C++ Builder qui permet de tracker les fuites mémoires...

Quand je construis un objet avec new et qu'une exception est lancée
dans le constructeur, à la sortie du programme, CodeGuard m'indique
une fuite mémoire parce que l'objet en question n'a pas été détruit.

Je crois bien me souvenir qu'il n'est pas besoin de faire un delete
dans ce cas.



Si je comprend bien la question, C++Builder a raison.
C'est à toi de détruire l'objet par delete.
La façon de faire lorsque tu crains qu'une exception soit levée dans ton
constructeur est de mettre le code dans un bloc try/catch. Lorsque tu
captures l'exception tu effaces l'objet, puis tu relances l'exception.

try
{
// du code
objet* = new.....
// du code qui lève une exception
}
catch(...)
{
delete objet;
throw;
}





Comment tu peux faire ça ? J'ai testé ça :


#include <cassert>
#include <exception>

struct error : public std::exception
{
};

struct Test
{
Test()
: i( 10 )
{
throw error();
};
int i;
};

struct Test1
{
Test1()
: i( 0 )
, t()
{
}
int i;
Test t;
};

int main()
{
Test1 *t = 0;
try
{
t = new Test1();
}
catch( error )
{
assert( t );
delete t;
}
return 0;
}


Et j'obtiens:

construct_leak: construct_leak.cc:40: int main(): Assertion `t' failed.

... donc comment libérer l'objet alors que je n'ai pas un pointeur
dessus ?


Je pense que nous ne nous comprenons pas, j'ai écris:

objet* = new.....
// du code qui lève une exception

et j'avais bien dit que le bout de code était dans un contructeur. Sans
doute le terme objet a-t-il prêté à confusion. Il s'agit d'un pointeur
sur un objet membre d'une classe donc nous discutino à propos du
constructeur

quoiqu'il en soit, le code qui lève l'exception est *derrière* la
création de objet.
J'ai un pointeur dessus, je peux le libérer.

Si c'est objet* = new..... qui lève lui même l'exception, alors
effectivement il n'y a pas de pointeur dessus et rien à libérer.


tableau "automatique" (i.e. alloué dans la pile) que seul l'objet levant
l'exception est détruit.


Je pense vraiment que nous ne parlons pas de la même chose.

--
Alain



Avatar
Alain Gaillard
Alain Gaillard wrote:



Normal. Il n'a pas été construit non plus. C'est bien à toi
d'assurer qu'il n'y a pas de fuites dans le constructeur quand
il sort par une exception.


Tout à fait.

De la mémoire allouée par le new, non. De toute façon, tu ne
pourrais pas, vu que tu n'en a pas le pointeur.
Non, non et non. Tu ne peux pas appeler delete sur l'objet,
parce que 1) il est illégal d'appeler delete sur un objet dont
le constructeur n'a pas terminé, et 2) tu n'en as pas l'adresse.


Décidément je me suis mal exprimé (ou bien je n'ai pas compris la
question initiale c'est bien possible :) )

Dans mon bout de code "objet" est un membre d'un objet en cours de
construction, pas l'objet lui même, bien évidement.


Mais la question concernait le cas où c'est le constructeur de
l'objet qui a levé l'exception. Dans ce cas-là, tu n'as rien à
faire.


Nous sommes d'accord.

Et tu ne pourrais rien faire, parce que tu n'as jamais eu
l'adresse renvoyée par la fonction operator new.


Nous sommes toujours d'accord.
J'avais donc mal compris la question. Je n'avais pas capté (pas osé
capter ? ;) ) que le posteur parlait de détruire par delete l'objet en
cours de construction.


Ce qu'il faut, en revanche, c'est que ton constructeur soit
propre. Quelque chose comme :

Classe::Classe()
: pointerMember( new XXX )
{
if ( whatever ) {
throw SomeError ;
}
}

provoque bien une fuite de mémoire. Dans ce cas-ci
(ultra-simple), on pourrait bien insérer un « delete
pointerMember » avant le throw, mais dans le cas général, il
est conseillé d'utiliser des objets supplémentaires (pointeurs
intelligents, sous-classes, etc.) qui s'occuperont
automatiquement du nettoyage ; le compilateur appelle bien le
destructeur de tous les sous-objets dont le constructeur a fini.



Nous sommes encore d'accord :)

--
Alain

Avatar
kanze
Jean-Marc Bourguet wrote:
Pierre Barbier de Reuille writes:
Non, le assert définit dans assert.h ou cassert par la norme
spécifie bien que si la macro NDEBUG n'est pas définit au
moment de l'inclusion du fichier d'entête, alors la macro ne
fait rien.


C'est l'inverse.

Or, NDEBUG est la macro qui est définit pour indiquer une
compilation en mode debug. Ca n'a rien de compilateur
spécifique ...


Aucun des 6 compilateurs que j'utilise plus ou moins
régulièrement (gcc, como, intel pour Linux, Sun CC pour
Solaris, HP aCC pour HP-UX, IBM xlC pour AIX) ne lie la
définition de NDEBUG ni à la présence des informations de
débuggage, ni au degré d'optimisation.


Tu peux y ajouter VC++, ainsi que des compilateurs plus anciens,
genre CFront ou Zortech.

Aussi, aucun de ces compilateurs ne définit NDEBUG
implicitement. Il faut que tu la définis explicitement pour
qu'il soit défini. Sans un #define NDEBUG quelque part, ou un
-DNDEBUG dans la ligne de commande, assert est actif.

--
James Kanze GABI Software
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34


Avatar
Pierre Barbier de Reuille
kanze wrote:
Pierre Barbier de Reuille wrote:
kanze wrote:
Pierre Barbier de Reuille wrote:
Michael wrote:
Pierre Barbier de Reuille wrote
in news:44e2590c $0$18265$:





[...]


Je dois dire que je ne suis pas d'accord (même si, comme tu le
dis plus loin, c'est une affaire de jugement). Mais je ne
pense pas qu'il soit bon de mélanger code de retour et
exceptions.


Certainement pas. Certains types d'erreur sont rapportés par
des exceptions, d'autres par des codes de retour. Il n'y a pas
de mélange. (Il y a aussi, bien que moins souvent, des cas
particuliers où d'autres solutions s'imposent. Genre iostream,
avec un état dans l'objet.)

Un des objectif des exceptions est de ne gérer les erreurs
qu'à l'endroit où tu peux y faire quelque chose ...


L'objectif des exceptions, c'est de pouvoir propager une erreur
assez loin sans que les niveaux intermédiaires aient à s'en
occuper (directement, en tout cas), avec un nettoyage approprié.
Si de par sa nature, l'erreur ne doit pas propager loin,
l'intérêt des exceptions se trouve grandement réduit. On en paie
le coût (en lourdeur, par exemple) pour rien.

En règle générale, on ne se sert d'une exception que quand on
est certain que l'erreur ne peut pas été traitée localement.
Dans la doute, on préfère le code de retour, parce que c'est
bien plus simple et plus léger, et c'est simple aussi pour le
client de le convertir en exception s'il ne peut pas le traiter
localement.


Moi je dirais le contraire :P
On ne se sert d'un code de retour uniquement quand il est strictement
nécessaire de traiter l'erreur tout de suite ... une exception est plus
légère à gérer AMA.


alors qu'avec les codes de retour, il faut propager les
erreurs à la main jusqu'à l'endroit qui peut gérer les
erreurs.


Justement. Il y a beaucoup d'erreurs qu'on traite localement, et
qu'on ne propage pas. Rien n'est gratuit, et c'est bien plus
pénible d'essayer à attrapper des exceptions que de simplement
tester un code de retour.

Et, typiquement, les erreurs de saisies nécessiteront une
re-saisie des données et l'erreur sera donc gérées au mieux au
niveau de l'interface graphique (pour pouvoir redemander la
saisie).


Exactement. Typiquement, la saisie se trouve dans une boucle,
avec rupture de boucle quand elle est bonne ou quand
l'utilisateur démande un abort. Et c'est bien plus simple de
gérer une boucle avec un code de retour qu'avec une exception.


Sauf qu'une erreur utilisateur peut se révéler dans une fonction interne
(par exemple parce que tester la validité des données serait aussi
coûteuse que le traitement lui-même ... ), il te faudra donc remonter
l'erreur fonction par fonction.


Si tu utilises un code de retour pour ça, ce sera très lourd à
gérer.


Je dirais le contraire. Si tu utilises une exception pour ça, ce
sera très lourd à gérer. (En Java, je n'avais pas toujours le
choix, et je t'assure, les exceptions, dans des cas comme
l'échec d'une ouverture de fichier, ça alourdit le code
énormement.)

Pour moi, le seul cas où un code de retour est utilisable dans
un système utilisant les exceptions, est s'il est acceptable
d'avoir une erreur "silencieuse".


Sauf que si tu ne testes pas le code de retour, il y a une
avorte, non ? Au moins, les codes de retour que j'ai utilisé
ont en général un drappeau « lu », et un assert qu'il soit
vrai dans le destructeur.

Par exemple, dans le cas d'un overflow numérique, le fait
d'avoir NaN ou inf à la place d'un nombre valide peut se
justifier, car il est probable que le client remplace ces
valeurs par autre chose par la suite ... mais c'est un des
rares exemples que je vois comme ça.


C'est un cas particulier où il est permis de différer le
traitement d'erreur. Les erreurs des iostream sont pareilles.
Dans les deux cas, il se trouve que l'objet (le virgule flottant
ou le flux) peut devenir invalid à la suite de prèsque toute
opération. Tester après chaque opération serait assez
fastidieux, et on a choisi l'option de propager l'invalidité à
travers toutes les opérations suivantes, pour pouvoir ne le
tester qu'une fois, à la fin.

Comme tu dis, de tels cas ne sont pas fréquents.

De plus, je ne vois vraiment pas en quoi le code de retour
permet, plus que les exceptions, de récupérer l'exécution (ni
plus ni moins d'ailleurs ...).


C'est beaucoup moins lourd :

if ( ! premierOperation() ) {
// gestion d'erreur...
} else if ( ! secondOperation() ) {
// gestion d'erreur (différente du précédante) ...
}

contre :

bool succeeded = false ;
try {
premierOperation() ;
succeeded = true ;
} catch ( ... ) {
// gestion d'erreur...
}
if ( succeeded ) {
try {
secondOperation() ;
} catch ( ... ) {
// gestion d'erreur (différente du précédante) ...
}
}


Oui, sauf qu'en utilisant les exceptions, ça permet justement d'écrire :

try
{
premierOperation();
secondOperation();
}
catch(...)
{
// Gestion d'erreur1
}
catch(...)
{
// Gestion d'erreur2
}

ce qui est, AMA, moins lourd et en plus "déportable" (oui c'est pas
français :P) dans un appel englobant, et ceci sans coût ni pour la
structure, ni pour le temps d'exécution.

Alors que la version en code de retour suppose en plus que soit :
1 - ton type est convertible en booléen avec une valeur fausse s'il
est invalide
2 - tu n'ais pas de valeur de retour utile
3 - tu utilises un des paramètres en valeur de retour

Disons que tu doivent retourner une valeur (relativement courant), ou un
objet qui a déjà une autre interprétation comme booléen, seule le cas 3
est applicable ... mais c'est pas le plus lisible.


ou :

while ( ! end && getData() ) {
// traitement des données ...
}
if ( ! end ) {
// traitement de l'erreur ...
}

contre :

while ( ! end ) {
try {
getData() ;
// traitement des données...
} catch( ... ) {
// traitement de l'erreur...
end = true ;
}
}

Il faut aussi savoir que la gestion de code de retour est
coûteuse en temps de calcul tout le temps alors que la gestion
des exceptions ne coûte que quand une exception est levée ...


Tu as des mesures ? Jusqu'ici, je n'ai jamais rencontré une
application où le choix aurait fait une différence mesurable.


Yep ! J'avoue que je pensais le contraire jusqu'il y a quelques jours,
quand j'ai lu ça : http://www.gotw.ca/gotw/008.htm (tout à la fin)
Partant du principe qu'une erreur arrive de façon exceptionnelle (sinon
c'est plus une *erreur*, ça devient un comportement *normal*), j'ai
aussi écrit ça (en deux fichiers pour éviter les optimisations trop
violentes du compilateur):

==8<==8<==8<== Fichier Implémentation ==8<==8<==8<=
#include <exception>

struct Error : public std::exception
{ };

int call_excep( int i )
{
if( i%10 == 0 )
{
throw Error();
}
return i+1;
}

int call_excep2( int i )
{
if( i%1000000 == 0 )
{
throw Error();
}
return i+1;
}

int call_error( int i, int& res )
{
if( i%10 == 0 )
return 0;
res = i+1;
return 1;
}

int call_error2( int i, int& res )
{
if( i%1000000 == 0 )
return 0;
res = i+1;
return 1;
}

int call( int i )
{
if( i < 0 )
{
throw Error();
}
return i+1;
}

==>8==>8==>8==>8==>8==>8==>8==>8==>8==>8==>8==>8=
==8<==8<==8<== Fichier Principal ==8<==8<==8<=
#include <iostream>
#include <sys/times.h>
#include <exception>
#include <unistd.h>

#define NB_ITER 1000000
#define RATIO 100

using namespace std;

struct Error : public std::exception
{ };

int call_excep( int );
int call_excep2( int );
int call_error( int, int& );
int call_error2( int, int& );
int call( int );

int fct_excep( int i )
{
try
{
return call_excep( i );
}
catch( ... )
{
return i;
}
}

int fct_excep2( int i )
{
try
{
return call( i );
}
catch( ... )
{
return i;
}
}

int fct_excep3( int i )
{
try
{
return call_excep2( i );
}
catch( ... )
{
return i;
}
}

int fct( int i )
{
return call( i );
}

int fct_error( int i )
{
int j;
if( call_error( i,j ) )
return j;
return i;
}

int fct_error2( int i )
{
int j;
if( call_error2( i,j ) )
return j;
return i;
}

int main()
{
tms begin, normal, with_excep, with_excep2, with_excep3, with_error,
with_error2;
float nb_ticks_per_second = sysconf(_SC_CLK_TCK);
float time_normal, time_excep, time_excep2, time_excep3, time_error,
time_error2;
times( &begin );
int f = 0;
for( int i = 0 ; i < RATIO*NB_ITER ; ++i )
{
f += fct( i );
}
times( &normal );
for( int i = 0 ; i < NB_ITER ; ++i )
{
fct_excep( i );
}
times( &with_excep );
for( int i = 0 ; i < RATIO*NB_ITER ; ++i )
{
f += fct_excep2( i );
}
times( &with_excep2 );
for( int i = 0 ; i < RATIO*NB_ITER ; ++i )
{
f += fct_excep3( i );
}
times( &with_excep3 );
for( int i = 0 ; i < RATIO*NB_ITER ; ++i )
{
f += fct_error( i );
}
times( &with_error );
for( int i = 0 ; i < RATIO*NB_ITER ; ++i )
{
f += fct_error2( i );
}
times( &with_error2 );
cout << f << endl;
time_normal = ( normal.tms_utime - begin.tms_utime ) /
nb_ticks_per_second;
time_excep = ( with_excep.tms_utime - normal.tms_utime ) /
nb_ticks_per_second;
time_excep2 = ( with_excep2.tms_utime - with_excep.tms_utime ) /
nb_ticks_per_second;
time_excep3 = ( with_excep3.tms_utime - with_excep2.tms_utime ) /
nb_ticks_per_second;
time_error = ( with_error.tms_utime - with_excep3.tms_utime ) /
nb_ticks_per_second;
time_error2 = ( with_error2.tms_utime - with_error.tms_utime ) /
nb_ticks_per_second;
cout << showpoint;
cout << "Time without errors : " << time_normal << " sec"
<< endl
<< "Time with exceptions : " << RATIO*time_excep << "
sec" << endl
<< "Time with (no) exceptions : " << time_excep2 << " sec"
<< endl
<< "Time with (few) exceptions : " << time_excep3 << " sec"
<< endl
<< "Time with return values : " << time_error << " sec"
<< endl
<< "Time with (few) return values : " << time_error2 << " sec"
<< endl;
return 0;
}


==>8==>8==>8==>8==>8==>8==>8==>8==>8==>8==>8==>8=
Dans ce cas je teste :
1 - exécution sans gestion d'erreur (Time without errors)
2 - exécution avec exception (chaque appel lance une exception ou presque)
3 - exécution avec gestion des exception mais sans levée d'exception
4 - avec exception, mais peu d'exceptions sont effectivement levées
5 - erreur par valeurs de retour (pratiquement tous les appels génères
des erreurs)
6 - erreur par valeurs de retour (peu d'erreurs relevées)

Voici les temps que j'obtiens chez moi (compilé avec g++ avec les
options par défaut):

Time without errors : 1.28000 sec
Time with exceptions : 89.0000 sec
Time with (no) exceptions : 1.31000 sec
Time with (few) exceptions : 2.03000 sec
Time with return values : 2.33000 sec
Time with (few) return values : 2.34000 sec

On peut remarque que :
1 - quand une exception est levée, c'est lent
2 - la gestion des exceptions quand il n'y a rien de levé ne coûte
quasiment rien
3 - en gros, la rapidité va dépendre du nombre d'exceptions levées
4 - le temps pris par la gestion des valeurs de retour est indépendant
du nombre d'erreurs

Donc, on peut en déduire que, si une erreur arrive rarement, une
exception est ce qui est le plus adapté. Donc elles portent bien leur
nom ...


[...]
-- Assert (avec avortement en cas d'erreur) : tout ce qui
relève de l'erreur logiciel (y compris dans le code client)
ou d'une panne matérielle qui empêcherait au programme de
fonctionner.


Je suis d'accord pour l'erreur logiciel mais pas pour l'erreur
matérielle ! Lors d'une erreur matériel, une exception est
appropriée, quitte à simplement avertir l'utilisateur et
quitter ...


Tout dépend de l'erreur (et de l'application). Si l'erreur
matérielle est sur une périphérique sécondaire, tu as
probablement raison. Si c'est une faute de parité mémoire, en
revanche, ton programme est corrompu, et tout ce que tu fais de
plus pourrait faire encore plus de dégats. (Mais j'aurais dû
être plus précis.


En effet ... en erreur matériel je pensais plutôt à une erreur de
périphérique.


Le problème de la gestion d'une panne matérielle
avec un assert et qu'il n'y aura plus aucune erreur de gérées
dès que le code est mis en production (i.e. donc plus en
debug).


Depuis quand ? Je n'ai jamais livré du code sans les assert ?
On *peut* supprimer les assert en production, si le profiler
montre qu'il pose un véritable problème de temps, mais c'est une
pratique exceptionnelle.

--
James Kanze GABI Software
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34







Avatar
kanze
Fabien LE LEZ wrote:
On 16 Aug 2006 06:14:33 -0700, "kanze" :

-- Exception : ce sont les erreurs auxquelles on ne s'attend
pas dans un fonctionnement normal, mais qui peuvent se
produire dans des cas limites,


Je serais moins manichéen que toi sur la distinction code de
retour / exception. J'ai plutôt tendance à choisir suivant la
lisibilité de code plutôt que d'après une règle fixe.


Il faut prendre ce que j'ai écrit comme des indications
générales (guidelines), et non des règles absolues. Il y a
toujours des exceptions, et il y a une large mesure
d'appréciation dans chaque cas.

En particulier, si réparer le problème (par exemple, demander
à l'utilisateur de saisir une nouvelle valeur) demande de
remonter plusieurs appels de fonctions, les exceptions sont
parfaitement adaptées.


Ça se peut. Quand les erreurs sont détectées au niveau de
l'application, on peut faire un choix beaucoup plus cerné, parce
qu'on sait exactement où l'erreur sera traité. Si tu écris une
bibliothèque plus générique, c'est plus difficile ; il faut
plus ou moins estîmer ce que sera le cas la plupart du temps.

Si, en plein milieu d'un calcul, on s'aperçoit que la valeur
saisie entraîne des résultats incorrects, j'aime pouvoir dire
"On arrête tout et on recommence avec une autre valeur", sans
avoir à mettre des dizaines de vérifications de valeurs de
retour qui alourdissent le code.


Certes. Il n'y a pas de règle absolue. (Sauf peut-être aux
extrèmes : on n'aborte pas le programme suite à une faute de
frappe de l'utilisateur.)

--
James Kanze GABI Software
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34


Avatar
Pierre Barbier de Reuille
Alain Gaillard wrote:
Alain Gaillard wrote:

[...]



... donc comment libérer l'objet alors que je n'ai pas un pointeur
dessus ?


Je pense que nous ne nous comprenons pas, j'ai écris:

objet* = new.....
// du code qui lève une exception

et j'avais bien dit que le bout de code était dans un contructeur. Sans
doute le terme objet a-t-il prêté à confusion. Il s'agit d'un pointeur
sur un objet membre d'une classe donc nous discutino à propos du
constructeur

quoiqu'il en soit, le code qui lève l'exception est *derrière* la
création de objet.
J'ai un pointeur dessus, je peux le libérer.

Si c'est objet* = new..... qui lève lui même l'exception, alors
effectivement il n'y a pas de pointeur dessus et rien à libérer.


tableau "automatique" (i.e. alloué dans la pile) que seul l'objet levant
l'exception est détruit.


Je pense vraiment que nous ne parlons pas de la même chose.



En effet, je n'avais pas compris ça ... mais il faut dire que j'ai
interprété la question de Michael comme faisant une fuite de mémoire
dans le genre de code que j'ai écrit ...

Pierre


Avatar
kanze
Gabriel Dos Reis wrote:
Fabien LE LEZ writes:

| On 16 Aug 2006 06:14:33 -0700, "kanze" :

| > -- Exception : ce sont les erreurs auxquelles on ne s'attend
| > pas dans un fonctionnement normal, mais qui peuvent se
| > produire dans des cas limites,

| Je serais moins manichéen que toi sur la distinction
| code de retour / exception. J'ai plutôt tendance à choisir suivant la
| lisibilité de code plutôt que d'après une règle fixe.

la correction d'un programme est-elle un corollaire de « lisibilité » ?


La lisibilité est une condition nécessaire mais non suffisante
de la correction.

En ce qui concerne les exceptions : les exceptions améliore la
lisibilité en ce qu'elles libèrent le code d'un tas de tests des
cas particulier, et ainsi rend l'algorithme de base plus
visible. Elles y nuisent, en revanche, en ce qu'elles
introduisent des flux de contrôle invisible, qui peuvent parfois
surprendre. Si on utilise des techniques de RAII assez
rigueureusement, la nuisance est limitée, et si on n'a pas à
traiter l'erreur localement, l'amélioration est importante. Si
on a affaire à une fonction bourrée de blocs de try/catch, en
revanche, je crois que l'effet est plutôt négatif.

--
James Kanze GABI Software
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34

Avatar
kanze
Fabien LE LEZ wrote:

- si on oublie de vérifier un code de retour, une erreur
d'exécution peut passer inaperçue ;


Si on oublie de vérifier un code de retour, on a l'échec d'une
assertion. Ça ne se passe pas inaperçu chez moi.

- si on oublie d'intercepter une exception, une erreur
d'exécution peut arrêter le programme.


Si les tests ne provoque pas l'erreur, et qu'on a oublie
d'intercepter l'exception, ça passe inaperçu. Avec le code de
retour, en revanche, il y a échec de l'assertion si on ne
vérifie pas, même dans le cas où il n'y a pas d'erreur.

En pratique, la simplicité du code peut permettre de choisir la
méthode d'indication d'une erreur :
- si tu écris plusieurs try...catch dans la même fonction, il y
a des chances pour qu'une indication d'erreur par code de retour soit
plus adaptée ;


Même si chaque appel de la fonction qui lève l'exception doit
être encadré dans un try/catch.

- si tu écris plusieurs constructions du style
if (f() != OK) return ERREUR;
il y a des chances pour qu'une exception soit plus adaptée.


Tout à fait.

La seule chose à ajouter, c'est que si tu écris une
bibliothèque, et tu ne sais pas trop où le client va traiter
l'erreur, il vaut mieux pécher du côté du code de retour.
Simplement parce que c'est beaucoup plus facile au client
d'écrire :
if ( f() != OK ) throw whatever ;
que d'écrire :

try {
f() ;
} catch ( Whatever& error ) {
// ...
}

--
James Kanze GABI Software
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34

Avatar
Pierre Barbier de Reuille
kanze wrote:
Fabien LE LEZ wrote:

- si on oublie de vérifier un code de retour, une erreur
d'exécution peut passer inaperçue ;


Si on oublie de vérifier un code de retour, on a l'échec d'une
assertion. Ça ne se passe pas inaperçu chez moi.

- si on oublie d'intercepter une exception, une erreur
d'exécution peut arrêter le programme.


Si les tests ne provoque pas l'erreur, et qu'on a oublie
d'intercepter l'exception, ça passe inaperçu. Avec le code de
retour, en revanche, il y a échec de l'assertion si on ne
vérifie pas, même dans le cas où il n'y a pas d'erreur.

En pratique, la simplicité du code peut permettre de choisir la
méthode d'indication d'une erreur :
- si tu écris plusieurs try...catch dans la même fonction, il y
a des chances pour qu'une indication d'erreur par code de retour soit
plus adaptée ;


Même si chaque appel de la fonction qui lève l'exception doit
être encadré dans un try/catch.

- si tu écris plusieurs constructions du style
if (f() != OK) return ERREUR;
il y a des chances pour qu'une exception soit plus adaptée.


Tout à fait.

La seule chose à ajouter, c'est que si tu écris une
bibliothèque, et tu ne sais pas trop où le client va traiter
l'erreur, il vaut mieux pécher du côté du code de retour.
Simplement parce que c'est beaucoup plus facile au client
d'écrire :
if ( f() != OK ) throw whatever ;


Sauf que du code correct serait :

type_t var = f();
if( var != OK)
{
throw Whatever(parameters);
}
type1_t var1 = f1();
if( var1 != OK1)
{
throw Whatever(parameters);
}
type2_t var2 = f2();
if( var2 != OK2)
{
throw Whatever(parameters);
}
type3_t var3 = f3();
if( var3 != OK3)
{
throw Whatever(parameters);
}
// ...

que d'écrire :

try {
f() ;
} catch ( Whatever& error ) {
// ...
}


Et là ce serait :

try
{
fct();
fct2();
fct3();
// ...
}
catch(Whatever& error)
{
// ...
}


--
James Kanze GABI Software
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34