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

4 5 6 7 8
Avatar
Gabriel Dos Reis
"kanze" writes:

[...]

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

Je driais exactement l'opposé.

-- Gaby
Avatar
Gabriel Dos Reis
"kanze" writes:

| 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 réalité, les deux sont orthogonales.

-- Gaby
Avatar
Fabien LE LEZ
On 17 Aug 2006 03:47:08 -0700, "kanze" :

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,


J'ai quelques classes où le choix entre retour d'un code d'erreur et
lancement d'exception se choisit en paramètre du constructeur.

Avatar
Fabien LE LEZ
On 17 Aug 2006 03:33:28 -0700, James Kanze:

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


Par contre, on peut bien aborter tout un "sous-programme" (i.e. une
hiérarchie d'appels de fonctions) en cas de faute de frappe.
C'est le principe des exceptions.

Le cas le plus extrême que j'aie pu rencontrer est celui d'un
programme d'installation.

Ça donnait à peu près ça :

bool fini= false;
while (!fini)
{
string nom_repertoire= DemanderLeNomDeRepertoire();
try
{
DecompresserDans (nom_repertoire);
fini= true;
}
catch (exception const& e)
{
Afficher ("Y'a une couille", e.what());
}
}

Et bien sûr, les fonctions appelées par "DecompresserDans" peuvent
lancer des exceptions pour plein de raisons (fichier corrompu,
problèmes de droits, problèmes matériels, etc.).

Pour résumer, j'ai l'impression que les exceptions conviennent bien
dans le cas où on a beaucoup de "throw" et très peu de "try...catch".


Tu as écrit par ailleurs :

[Les exceptions] nuisent, en revanche, en ce qu'elles
introduisent des flux de contrôle invisible, qui peuvent parfois
surprendre.


Certes. Mais les exceptions existent ; même si tu n'en lances jamais
toi-même, il y a toujours le risque que telle ou telle fonction en
lance une.

(En pratique, je considère qu'en général, seuls l'affectation de types
de base (et surtout de pointeurs), ainsi que les destructeurs, ont la
garantie "nothrow".)

Du coup, choisir de lancer ses propres exceptions ne change pas
grand-chose au problème.


Avatar
Sylvain Togni

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


La bonne performance du 3 viens du test effectué (i < 0)
par rapport aux autres tests (i%10 == 0 ou i%100000 == 0).

Si on remplace la fonction call() par quelque chose de plus
cohérent avec le reste :

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

On a chez moi (VC6) des résultats plus serrés, malgré le
paramètre supplémentaire que la solution à base de valeurs
de retour entraîne :

Time without errors : 2.64800 sec
Time with exceptions : 102.900 sec
Time with (no) exceptions : 2.75700 sec
Time with (few) exceptions : 2.75800 sec
Time with return values : 2.86700 sec
Time with (few) return values : 2.60200 sec


--
Sylvain Togni

Avatar
Pierre Barbier de Reuille
Sylvain Togni wrote:

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


La bonne performance du 3 viens du test effectué (i < 0)
par rapport aux autres tests (i%10 == 0 ou i%100000 == 0).

Si on remplace la fonction call() par quelque chose de plus
cohérent avec le reste :

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

On a chez moi (VC6) des résultats plus serrés, malgré le
paramètre supplémentaire que la solution à base de valeurs
de retour entraîne :

Time without errors : 2.64800 sec
Time with exceptions : 102.900 sec
Time with (no) exceptions : 2.75700 sec
Time with (few) exceptions : 2.75800 sec
Time with return values : 2.86700 sec
Time with (few) return values : 2.60200 sec




En effet, mais chez moi ça donne des résultats pour le moins bizarre ...

Time without errors : 2.04000 sec
Time with exceptions : 90.0000 sec
Time with (no) exceptions : 2.12000 sec <--
Time with (few) exceptions : 2.06000 sec <--
Time with return values : 2.34000 sec
Time with (few) return values : 2.35000 sec

Comment le fait de ne pas lever d'exception peut être plus lent ?
Bizarre ... bizarre ...

Si j'active l'optimisation maximale de g++ c'est encore pire :
Time without errors : 0.930000 sec
Time with exceptions : 90.0000 sec
Time with (no) exceptions : 0.930000 sec
Time with (few) exceptions : 0.930000 sec <-- !!!!!
Time with return values : 1.03000 sec
Time with (few) return values : 1.00000 sec

Cette fois, le fais de lever une exception est de temps en temps est
aussi rapide que de ne pas en lever ?

Bref, il faut regarder les résultats avec des pincettes et la seule
chose à retenir est :

1 - un bloc try-catch ne coûte rien (ou presque) tant qu'une exception
n'est pas levée
2 - le coût de lever d'une exception est élevé
3 - le coup d'une gestion d'erreur par valeur de retour est indépendant
du fait qu'il y ait une erreur ou non.

J'en conclu quand même que pour des *erreurs*, les exceptions sont plus
efficaces. Après, si c'est plus un retour à traiter différemment (i.e.
qqch qui arrive fréquemment), alors une valeur de retour s'impose.

Pierre


Avatar
Alain Gaillard

Je driais exactement l'opposé.


Moi itou

--
Alain

Avatar
Alain Gaillard

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.


Même pas au fait.
Les objets dans le tableau construits avant la levée de l'exception
seront détruits mais pas l'objet levant l'exception. Si l'exception est
levée dans le constructeur je veux dire. Dans ce cas l'objet en question
n'est pas construit du tout.

--
Alain

Avatar
kanze
Alain Gaillard wrote:

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.


Même pas au fait.
Les objets dans le tableau construits avant la levée de
l'exception seront détruits mais pas l'objet levant
l'exception. Si l'exception est levée dans le constructeur je
veux dire. Dans ce cas l'objet en question n'est pas construit
du tout.


J'ai l'impression qu'il y a un peu de flou dans le discours.
Pour être plus précis :

Un objet peut comporter des sous-objets -- une classe qui a une
classe de base ou des membres, les éléments d'un tableau, etc.
Un objet ou un sous-objet est considéré « construit », lorsque
son constructeur termine (non avant). Lors d'une exception dans
un constructeur, le compilateur appelle le destructeur de tous
les sous-objets déjà construits. Toujours, et quelque soit la
durée de vie normale de l'objet en question.

Note, en revanche, que si ta classe contient un T*, et que le
constructeur l'a initialisé avec un new, ce n'est que le
pointeur qui est sous-objet, et le destructeur d'un pointeur ne
fait rien. C'est donc à toi de s'assurer la libération de
l'objet alloué. Ce qu'on fait en général en encapsulant le
pointeur dans sa propre classe : c'est donc un sous-objet
complètement construit, dont le destructeur fait le delete.

Aussi, dans le cas d'une expression new, il y a deux actions.
D'abord, le compilateur appelle la fonction operator new, pour
obtenir la mémoire, et ensuite, il appelle le constructeur de
l'objet. Si le constructeur de l'objet sort par une exception,
le compilateur appelle l'operator delete qui correspond, s'il y
en a, pour libérer la mémoire. (Dans le cas d'un new normal, il
y a toujours un operator delete qui correspond. Dans le cas d'un
new de placement, on cherche un operator delete de placement qui
correspond. S'il y en a, on l'appelle ; s'il n'y en a pas, on
laisse tomber.)

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


J'ai l'impression qu'il y a un peu de flou dans le discours.
Pour être plus précis :


C'est une impression qui t'appartient.
Rien de flou dans le discours.


Un objet peut comporter des sous-objets -- une classe qui a une
classe de base ou des membres, les éléments d'un tableau, etc.
Un objet ou un sous-objet est considéré « construit », lorsque
son constructeur termine (non avant). Lors d'une exception dans
un constructeur, le compilateur appelle le destructeur de tous
les sous-objets déjà construits. Toujours, et quelque soit la
durée de vie normale de l'objet en question.


Certes. Quelqu'un a dit autre chose ? Pas moi en tout cas.


Note, en revanche, que si ta classe contient un T*, et que le
constructeur l'a initialisé avec un new, ce n'est que le
pointeur qui est sous-objet, et le destructeur d'un pointeur ne
fait rien. C'est donc à toi de s'assurer la libération de
l'objet alloué.


Certes. Quelqu'un a dit autre chose ? Pas moi en tout cas.

Aussi, dans le cas d'une expression new, il y a deux actions.
D'abord, le compilateur appelle la fonction operator new, pour
obtenir la mémoire, et ensuite, il appelle le constructeur de
l'objet. Si le constructeur de l'objet sort par une exception,
le compilateur appelle l'operator delete qui correspond, s'il y
en a, pour libérer la mémoire. (Dans le cas d'un new normal, il
y a toujours un operator delete qui correspond. Dans le cas d'un
new de placement, on cherche un operator delete de placement qui
correspond. S'il y en a, on l'appelle ; s'il n'y en a pas, on
laisse tomber.)



James, tout cela est incontestablement très intéressant. Mais quel
rapport avec les tous derniers échanges entre Pierre et moi même ?

Parce que tu n'as pas lu que le bout de code que j'avais donné quelques
posts plus haut résidait *dans* un constructeur, tu t'es mis en tête que
je ne sais pas du tout comment un objet est construit (je cite ton post
"Non, non et non" :) )
Tu ne crois pas que tu pousses un peu là quand même ?

--
Alain

4 5 6 7 8