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
Michael
En plus des aspects purement conception, il faut bien tenir
compte des contraints techniques. Si on ne peut pas se servir
d'un code de retour, et qu'on ne veut pas avorter le programme,
une exception est en général préferrable aux alternatifs, tels
qu'ignorer l'erreur, ou positionner une variable globale à la
errno. Dans le cas des constructeurs, elle convient
particulièrement, parce que grace à l'exception, il n'y a pas
l'objet ; sans l'exception, il faut traiter la possibilité d'un
objet invalid.


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.

Bug, ou bien c'est le comportement normal et c'est moi qui me trompe?

Avatar
Alain Gaillard

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;
}


--
Alain

Avatar
Pierre Barbier de Reuille
Fabien LE LEZ wrote:
On Wed, 16 Aug 2006 22:28:57 +0100, Pierre Barbier de Reuille
:

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.


OK jusque-là.
Note que si je ne m'abuse, on peut #inclure assert.h plusieurs fois
dans le même .cpp, avec des valeurs différentes. Ce qui permet
d'avoir, via des options de compilation, un contrôle assez fin sur les
erreurs qui arrêteront le programme.

Or, NDEBUG est la
macro qui est définit pour indiquer une compilation en mode debug.


C'est quoi le "mode debug" ?



le "mode debug" c'est le mode dans lequel un certain nombre de macro
sont définie et dans lequel le compilo génère des infos de débuggages.
Certes, ça n'est pas dans la norme :P Et puis en effet, comme précisé
plus bas dans la discussion, NDEBUG ne fait pas partie des macros
définies ou non pour la plupart des compilo :-/ Mais je dois dire que du
coup je suis bluffé ... j'ai toujours utilisé les assert en me disant
que ça serait virer pour le final ... me reste plus qu'à définir NDEBUG
pour la dernière compilation avant livraison ...

Pierre


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

Pour la hi?rarchie, je ferais trois classes abstraites
d?rivant directement de std::exception, une par type
d'exception, de fa?on ? permettre ? l'utilisateur de ta
lib (i.e. le programmeur) d'intercepter l'exceptions
pertinente o? il le veut (par exemple g?rer les exceptions
utilisateur de mani?re centralis?e pour avoir un affichage,
les exceptions qui lui sont destin?es aux endroits o? il
pourra y faire qqch, et les exceptions graves pourront
?ventuellement donner lieu ? un traitement entrainant la
fermeture du programme ...).


Donc le mieux c'est que tout type d'erreur passe par une
exception, puisque par exemple si dans la lib il est
impossible de créer un objet qui doit être utilisé, il est
inutile d'aller plus loin. Et autant avertir tout le monde
de ce qui a foiré.

Quant aux assert, je les utiliserai pour vérifier que les
paramètres entrés par l'utilisateur de la lib sont
corrects...


Voilà, pour moi la distinction est:

- Exception : erreur d'utilisation ou matérielle
- Assert : pour vérifier que *je* ne me suis pas trompé quelque part,
de toute façon ce sera supprimé dans la version finale (sans avoir rien
à faire, l'entête <cassert> définit la macro assert comme une fo nction
vide si la compilation n'est pas en debug ...)


Je vois trois catégories :

-- Code de rétour : erreur « attendue », donc, non
exceptionnelle. L'exemple type, c'est toute erreur dans les
entrées d'une utilisateur. (Utilisateur, ici, c'est le
bonhomme devant le clavier ; pour le code qui t'appelle, je
parlerais de client.) Pour la reste, c'est souvent une
question de jugement : d'une part, une connexion TCP qui
tombe, il faut s'y attendre de temps en temps, mais de
l'autre, si tu es sur un reseau local, et même en général
sur l'internet, c'est quand même assez exceptionnel.

En gros, je dirais que s'il y a des chances de pouvoir
continuer l'opération en cours, c'est le code de rétour qui
s'impose. (Donc, par exemple, on peut toujours démander que
l'utilisateur rentrer les informations erronées de nouveau,
sans avorter la session.)


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.

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.

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) ...
}
}

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.

[...]
-- 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.

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
Michael
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;
}


Le problème est que j'utilise intensivement boost::scoped_ptr pour gérer tout
ça bien comme il faut, et je ne peux pas intervenir sur la destruction de
l'objet...

Avatar
kanze
Pierre Barbier de Reuille wrote:
Michel Decima wrote:

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 ... 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).


Qu'est ce qui empeche de garder les assert en production ?


Le fait que la fonction assert est redéfinit en fonction vide
dès que le programme n'est plus compilé en debug ... (ce qui
en dit long sur l'usage prévu pour les assert)


La fonction assert n'est redéfinie en fonction vide que si le
symbole NDEBUG est défini dans le préprocesseur. Il n'est pas
défini par défaut. Si tu le définis dans la ligne de commande du
compilateur, c'est bien ta faute -- moi, je ne l'ai jamais
fait. Et je n'ai jamais rencontré un compilateur qui n'était
pas conforme à cet égard, avec les options de défaut ; si tu ne
donnes pas l'option -DNDEBUG (ou /DNDEBUG avec VC++), assert
marche.

La technique habituelle, évidemment, c'est d'utiliser ton propre
symbole du préprocesseur, du genre PRODUCTION. Et quand tu
tombes sur un cas où le profileur dit que les assertions font un
problème de performance, dans une fonction bien précise, tu
encadres la fonction avec quelque chose du genre :

#ifdef PRODUCTION
#define NDEBUG
#include <assert.h>
#endif

// fonction critique...

#undef NDEBUG
#include <assert.h>

Du coup, tu ne supprimes que les assertions qui posent le
problème, et non toutes.

--
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
Jean-Marc Bourguet
Alain Gaillard writes:

Même si on s'attend quand même à trouver NDEBUG là où il n'y a
pas de compilation en "mode debug" :)


Je ne m'attends pas à ce qu'on m'impose NDEBUG sans que je l'ai demandé
explicitement. Et je ne le demande généralement qu'après avoir mesuré que
ça fait une différence.

A+

--
Jean-Marc
FAQ de fclc++: http://www.cmla.ens-cachan.fr/~dosreis/C++/FAQ
C++ FAQ Lite en VF: http://www.ifrance.com/jlecomte/c++/c++-faq-lite/index.html
Site de usenet-fr: http://www.usenet-fr.news.eu.org

Avatar
Alain Gaillard


Le problème est que j'utilise intensivement boost::scoped_ptr


Ah ? Et ça se devine où dans la question initiale ?

pour gérer tout
ça bien comme il faut, et je ne peux pas intervenir sur la destruction de
l'objet...


Je n'ai pas encore utilisé boost (mais je compte m'y mettre sans tarder
:) ). Cependat à voir le nom "scoped_ptr" (plus ou moins l'équivalent de
auto_ptr ?) et si tout est géré "bien comme il faut" comment se fait-il
que ton scoped_ptr ne détruise pas l'objet en cas de levée d'exception.
Si ton scoped_ptr est un membre de la classe, c'est à priori aussi un
objet complètement construit au moment de la levée d'exception, donc il
sera détruit et détruira l'objet dont il a la charge. Enfin normalement.
C'est vrai que sans code on n'y voit pas bien clair. Peut être nous
cache tu encore quelques infos ? ;)

--
Alain

Avatar
Pierre Barbier de Reuille
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 ? Si une exception est levée, l'objet est censé être détruit par
le compilateur. C'est uniquement dans le cas de la construction d'un
tableau "automatique" (i.e. alloué dans la pile) que seul l'objet levant
l'exception est détruit.

Pierre


Avatar
kanze
Michael wrote:
En aucun cas. Le formattage appartient à l'application, ou
éventuellement (pour certains messages) au sous-système de
logging.


C'est bien ce que je craignais...

Est-ce que tu pourrais me donner un exemple de comment les
choses doivent être faites stp? Parce que je ne vois pas
comment faire...


C'est assez difficile à faire de façon générique, parce que
chaque erreur est unique en ce qui concerne les informations
supplémentaires qui lui concerne. Mais grosso modo, tu affectes
un mot clé au type d'erreur, et tu mets toutes les autres
informations qui pourraient être utiles à la suite.

Prenons par exemple le cas d'un échec lors de l'ouverture d'un
fichier (bien que la plupart du temps, un code de retour
convient mieux). J'imaginerais bien un « message » du genre :

"CANNOT_OPEN <errno> <nom_du_fichier>"
voire même :
"404 <errno> <nom_du_fichier>"

En fait, c'est assez utile que le message ait un format qui est
compréhensible aux développeurs. Lors du développement, avant la
mise en place du système de traitement, c'est tout ce qu'ont les
développeurs. Mais c'est aussi très important qu'il soit facile
à parser, parce que dans la production, on va le parser
(peut-être plusieurs fois) pour extraire les informations et les
communiquer à qui le droit. Donc, ici, par exemple, je me
servirais pas du errno dans la boîte de dialogue que je montre à
l'utilisateur. Dans l'email que j'envoie au support technique,
en revanche, si.

Est-ce à moi de proposer au sein de ma lib de quoi interpréter
les exceptions lancées?


Interpréter, je ne sais pas. J'aurais tendance à les rendre
assez facile à parser qu'un outil supplémentaire ne soit pas
nécessaire. Des messages qui correspondent aux diverses
informations que tu fournis, en revanche, serait en général bien
venu. Quelque chose du genre strerror, par exemple.

Alternativement, tu pourrais fournir quelque chose de plus
encapsuler encore. Disons une fonction du genre :

enum DetailLevel
{
simple,
medium,
complete
} ;
getMessage( DetailLevel level,
std::locale const& locale,
char const* what ) ;

On pourrait aussi incorporer un champs de séverité, avec une
fonction à part pour l'extraire.

Dans le catch, alors, le client pourrait écrire quelque chose du
genre :

try {
// ...
} catch ( SomeException const& error ) {
char const* msg = error.what() ;
popupDialogBox( getMessage( simple, std::locale(), msg ) ) ;
if ( getSeverity( msg ) > x ) {
sendMailToSupport( getMessage( complete, std::locale( "C"
), msg ) ;
}
// ...
}

Par exemple une fonction virtuelle retournant un entier pour
chaque type d'exception, avec à côté une doc expliquant à
quelle erreur correspond chaque valeur? Et le client encadre
tous les appels aux fonctions de ma lib avec un try { }
catch(const DShow::Exception & e) {} et récupère cette valeur
et formatte un texte en conséquence?


Attention. Si le client doit encadrer chaque appel avec un
try/catch, tu abuses des exceptions. Dans ce cas-là, c'est
plutôt un code de retour qu'il faut.

--
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