OVH Cloud OVH Cloud

methodes et mot clef throw()

16 réponses
Avatar
Brieuc Jeunhomme
Bonjour,

je souhaite créer des exceptions en les dérivant de std::exception, mais
je me fais agonir d'injures à l'édition de liens par g++ quant à leur
méthode what() et à leur destructeur, quand je n'utilise pas le mot clé
throw.

Par exemple:

using namespace std;

class foo: public exception {

string text_;

public:

foo( const char *txt ): text_( txt ) { }

~foo() throw() { }

foo & operator += ( string txt ) { text_ += txt; }

const char * what() const throw() { return text_.c_str(); }

};

Je croyais que le mot clé throw servait à déclarer quelles exceptions la
méthode concernée était susceptible de lever, je constate qu'il n'en
est rien, mais je ne vois vraiment pas ce que le throw utilisé dans ce
contexte peut signifier.

--
BBP

6 réponses

1 2
Avatar
Fabien LE LEZ
On 9 Jan 2006 00:48:11 -0800, "kanze" :

void f(); // throw (Machin)



Non. C'est plus de l'ordre de la documentation.


Non, c'est plus que de la documentation, parce que c'est enforcé
par le code généré par le compilateur.


Justement : ce que je veux mettre dans mon code, c'est de la
documentation.

Quand j'ai décidé d'utiliser des spécifications d'exceptions autres
que "nothrow", c'est en faisant du code de gestion de fichiers pour le
programme d'installation d'un logiciel.
Une fonction d'ouverture de fichier, par exemple, peut se terminer de
trois manières différentes :
- soit renvoyer un handle sur le fichier ;
- soit lancer une exception "impossible d'ouvrir le fichier"
(l'usage d'une exception ici se justifie par le nombre d'appels de
fonctions qu'il faut "remonter" avant de se retrouver dans l'interface
graphique, seule capable d'afficher un message d'erreur) ;
- soit un autre type d'erreur (un "new" qui lance une exception,
par exemple), et là j'avoue que je ne sais pas quel est le mieux :
sortir du programme par terminate(), ou remonter dans le main() et
afficher un message d'erreur guère plus explicite.

Donc, en pratique, dans ce cas, mettre une spécification d'exception
ou juste de la documentation, ça ne fait pas vraiment de différence.
Mais ce dont j'ai réellement besoin, c'est d'indiquer (si possible
dans le .h) les exceptions que la fonction peut lancer en
fonctionnement normal.




Avatar
kanze
Fabien LE LEZ wrote:
On 9 Jan 2006 00:48:11 -0800, "kanze" :

void f(); // throw (Machin)



Non. C'est plus de l'ordre de la documentation.


Non, c'est plus que de la documentation, parce que c'est
enforcé par le code généré par le compilateur.


Justement : ce que je veux mettre dans mon code, c'est de la
documentation.


Il en faut aussi. L'un n'exclut pas l'autre.

Quand j'ai décidé d'utiliser des spécifications d'exceptions
autres que "nothrow", c'est en faisant du code de gestion de
fichiers pour le programme d'installation d'un logiciel. Une
fonction d'ouverture de fichier, par exemple, peut se terminer
de trois manières différentes :
- soit renvoyer un handle sur le fichier ;
- soit lancer une exception "impossible d'ouvrir le fichier"
(l'usage d'une exception ici se justifie par le nombre
d'appels de fonctions qu'il faut "remonter" avant de se
retrouver dans l'interface graphique, seule capable d'afficher
un message d'erreur) ;
- soit un autre type d'erreur (un "new" qui lance une exception,
par exemple), et là j'avoue que je ne sais pas quel est le mieux :
sortir du programme par terminate(), ou remonter dans le main() et
afficher un message d'erreur guère plus explicite.


Et j'imagine que ce troisième type d'erreur est assez ouvert. Tu
n'as pas envie de le limiter par contrat. C-à-d que ton contrat
est :

-- [cas sans erreur...]
-- en cas X (et pas d'erreur plus grave), l'exception de type Y
sera lever.

et rien de plus, en ce qui concerne les exceptions. Il n'y a
donc aucune limitation contractuelle en ce qui concerne les
exceptions possibles, et donc, pas de spécification d'exception.

Évidemment, on aimerait aussi pouvoir spécifier les autres
garanties au moyen du langage, et qu'elles soient vérifiées
aussi. Dans le cas « si X, exception Y », malheureusement, je
n'en vois pas la possibilité. Ni dans le langage aujourd'hui, ni
dans les propositions d'extensions pour la programmation par
contrat, ni même dans une extension imaginiaire -- je ne vois
simplement pas comment implémenter une telle vérification. (À la
rigueur, tu pourrais vérifier, au moins partiellement, que si tu
n'as pas levé d'exception, tu renvoies un handle valid. Mais que
l'exception ait le type Y si, et seulement si, tu n'as pas pu
ouvrir le fichier ?)

Dans ce cas-ci donc, le contrat se résume à la documentation,
sans vérification. Ce n'est pas idéal, mais c'est toujours mieux
que pas de contrat du tout, ce qui se passe dans le cas
d'épuisement mémoire, par exemple. Éventuellement, tu voudras
donner plus de garanties sur l'état de l'objet dans ce cas-ci.
(Bien que je ne crois pas que ce soit le cas dans ce cas
précis.) C'est possible en C++, et c'est même possible de les
vérifier. Je ne suis pas sûr que ce soit une bonne idée, en
revanche. J'aurais tendance à penser que si on veut donner plus
qu'un minimum de garantie (qu'on peut appeler le destructeur sur
l'objet, mais rien de plus), le rétour par une exception n'est
pas la bonne solution. Mais qui sait. Je ne peux pas imaginer
tous les cas possibles.

Donc, en pratique, dans ce cas, mettre une spécification
d'exception ou juste de la documentation, ça ne fait pas
vraiment de différence.


Si. Avec une spécification, tu garantis qu'il n'y aura jamais
que les exceptions que tu spécifies. Ce qui n'est probablement
pas ce que tu veux.

Mais ce dont j'ai réellement besoin, c'est d'indiquer (si
possible dans le .h) les exceptions que la fonction peut
lancer en fonctionnement normal.


Auquel je répondrais qu'en fonctionnement normal, une fonction
ne doit pas lever d'exception:-). (Je suis un peu sceptique sur
l'utilisation d'une exception pour ne pas pouvoir ouvrir un
fichier. Mais tu connais ton application et ses contraints mieux
que moi.)

En fin de compte, ce que tu veux, ce n'est pas à dire : je ne
lève que..., mais de dire : si X, c'est garanti que je lève Y.
Je suis d'accord avec toi que c'est une information importante
pour l'utilisateur. Mais ce n'est pas la signification des
spécifications d'exception.

--
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
Brieuc Jeunhomme
J'ai beaucoup de mal à comprendre ce qu'elle apporte, puisque
le résultat, in fine, c'est qu'il ne suffit pas de lire le
prototype pour savoir quelles exceptions on risque de se
prendre sur la tête en appelant la fonction, alors que ça
semblait justement être le but (aha tout à fait louable) à la
base.


Comment ça ? Le prototype indique bien les exceptions possibles,
au moins s'il est bien écrit.


Oui, le jour où on l'écrit. Mais, en mettant ma casquette d'hérétique,
je dirais que le jour où on change la liste d'exceptions qui peuvent
être déclenchées par une méthode, le compilateur n'apportant aucune
aide pour vérifier qu'on n'a pas oublié d'ajouter l'exception dans la
clause throw() de l'ensemble des méthodes appelantes, l'indication
donnée par le prototype est vouée à devenir fausse, de même que du code
C est voué à devenir un plat de nouilles passé la 10000ième ligne de
code.

Dans la pratique, on constate qu'il y a peu d'intérêt à y aller
en détail.


Oui, à cause du problème ci-dessus, mais je suis tout d'accord avec
Fabien: un problème spécifique peut avoir son exception spécifique,
qu'on ne souhaite pas forcément traiter au même endroit que les autres.

Du moins est-ce le cas quand on utilise des librairies qui lèvent
joyeusement des exceptions dans des circonstances presque normales,
alors que l'erreur est rattrappable. Sinon, on se retrouve avec un
syndrome du python: le choix entre le catch-all et le pas de catch du
tout parce que de toute façon, on ne sait jamais quelles exceptions on
risque de lever en appelant une fonction.

Il est très important de savoir si la fonction peut
lever une exception ou non. Une fois qu'on sait qu'elle peut
lever une exception, le ou les types en est rélativement sans
intérêt.


Bah je ne trouve pas. Beaucoup d'erreurs sont rattrappables, mais il
faut se placer au bon layer pour les rattrapper ou implémenter des
retries, et pour ça, on a besoin de savoir qui peut lever quoi avec
certitude. Mais peut-être qu'on peut simplement résoudre ce problème
avec des exceptions hiérarchisées, je n'ai pas encore joué avec ça.

void f( int ) throw() ; // Ne peut pas lever une exception
void g( int ) ; // Peut lever une exception

Et comme j'ai déjà indiqué, le compilateur génère
automatiquement la vérification de cette post-condition de f().


Oui, mais en run time, non ?

--
BBP


Avatar
kanze
Brieuc Jeunhomme wrote:
J'ai beaucoup de mal à comprendre ce qu'elle apporte,
puisque le résultat, in fine, c'est qu'il ne suffit pas de
lire le prototype pour savoir quelles exceptions on risque
de se prendre sur la tête en appelant la fonction, alors
que ça semblait justement être le but (aha tout à fait
louable) à la base.


Comment ça ? Le prototype indique bien les exceptions
possibles, au moins s'il est bien écrit.


Oui, le jour où on l'écrit. Mais, en mettant ma casquette
d'hérétique, je dirais que le jour où on change la liste
d'exceptions qui peuvent être déclenchées par une méthode, le
compilateur n'apportant aucune aide pour vérifier qu'on n'a
pas oublié d'ajouter l'exception dans la clause throw() de
l'ensemble des méthodes appelantes, l'indication donnée par le
prototype est vouée à devenir fausse, de même que du code C
est voué à devenir un plat de nouilles passé la 10000ième
ligne de code.


C'est vrai que la vérification n'a lieu qu'à l'exécution. C'est
un peu dommage, mais par rapport à tellement de chose où il n'y
a pas de vérification de tout.

Dans la pratique, on constate qu'il y a peu d'intérêt à y
aller en détail.


Oui, à cause du problème ci-dessus,


Non. Il y a peu d'intérêt à y aller en détail parce que ça ne
change rien pour l'utilisateur, et que ça pose des problèmes
dans l'hiérarchie des appels. L'intérêt d'utiliser une
exception, c'est que le traitement de l'erreur se trouve à un
endroit bien éloigné de sa détection. Et que la seule chose qui
intéresse toutes ces fonctions intermédiaire, c'est est qu'il y
a un cheminement supplémentaire à prendre en compte, à cause
d'une exception possible, ou non. Si la fonction appelée peut
lever une exception, il faut que mon code soit « exception
safe ». Sinon, je peux prendre les libertés à cet égard. (Il y a
aussi le fait que pour rendre mon code exception safe, il faut
que je dispose de quelques opérations de base qui ne peut pas
lever une exception.)

Au plus bas niveau, il se peut qu'il y a des cas où on peut bien
dire qu'une fonction lève tel ou tel type d'exception, et rien
d'autre. Mais pour la plupart des fonctions, ce qu'on démande
c'est la neutralité vis-à-vis des exceptions -- elles appellent
d'autres fonctions pour faire le travail, et on s'attend à ce
qu'elle propage des exceptions de ces autres fonctions, sans
trop s'occuper de leur type. Le type de l'exception ne devient
intéressant qu'au niveau du bloc de catch.

mais je suis tout d'accord avec Fabien: un problème spécifique
peut avoir son exception spécifique, qu'on ne souhaite pas
forcément traiter au même endroit que les autres.


Par exemple ? Je ne suis pas sûr de comprendre ce que tu essaies
de dire. C'est évident qu'une manque de mémoire doit provoquer
un type d'exception différent qu'une erreur de lecture de
disque. Selon le cas, c'est même possible qu'on traite ces deux
types d'erreur à des niveaux différents. Mais pour toutes les
fonctions intermédiaires, où est la différence ?

Du moins est-ce le cas quand on utilise des librairies qui
lèvent joyeusement des exceptions dans des circonstances
presque normales, alors que l'erreur est rattrappable.


Si on abuse d'une facilité, c'est évident qu'on aurait des
problèmes supplémentaire:-).

Sinon, on se retrouve avec un syndrome du python: le choix
entre le catch-all et le pas de catch du tout parce que de
toute façon, on ne sait jamais quelles exceptions on risque de
lever en appelant une fonction.


Je crois que dans le cas de Fabien, il a un type bien précis
d'exception, pour un type d'erreur bien précis, qu'il peut
traiter à part, à un niveau plus bas. Il ne prétend pas que sa
fonction ne pourrait jamais lever d'autre types d'exception,
genre std::bad_alloc.

Ce qu'il a, ce n'est pas un contrat en ce qui concerne les types
d'exception possibles. Ce qu'il a, c'est un contrat en ce qui
concerne le comportement dans un cas bien précis. Si ce cas se
présente, ET il n'y a pas d'autre erreur, il y aurait une
exception d'un type défini par le contrat.

Il est très important de savoir si la fonction peut lever
une exception ou non. Une fois qu'on sait qu'elle peut lever
une exception, le ou les types en est rélativement sans
intérêt.


Bah je ne trouve pas. Beaucoup d'erreurs sont rattrappables,
mais il faut se placer au bon layer pour les rattrapper ou
implémenter des retries, et pour ça, on a besoin de savoir qui
peut lever quoi avec certitude.


Je crois que tu vois le problème à l'envers. Le problème que tu
sembles décrire, ce n'est pas si une fonction f peut lever une
exception X. Le problème est si le cas Y va provoquer une
exception X. Et le problème annex : est-ce que l'exception X ne
peut se produire QUE dans le cas Y ?

La spécification d'exception n'apporte rien ici. À vrai dire,
j'ai du mal à imaginer une construction du langage qui puisse y
apporter quelque chose.

Mais peut-être qu'on peut simplement résoudre ce problème avec
des exceptions hiérarchisées, je n'ai pas encore joué avec ça.

void f( int ) throw() ; // Ne peut pas lever une exception
void g( int ) ; // Peut lever une exception

Et comme j'ai déjà indiqué, le compilateur génère
automatiquement la vérification de cette post-condition de
f().


Oui, mais en run time, non ?


Oui, mais...

J'aurais préféré, moi-aussi, une vérifcation statique. Mais il
faut dire qu'il n'y a erreur que si l'exception est réelement
levée. Exiger une vérification statique aurait exiger ou bien
des moyens au delà de ce que savent faire les compilateurs
aujourd'hui, pour detecter tous les cas d'erreur, ou bien du
code supplémentaire (des try/catch en plus) écrit par le
programmeur quand il sait qu'aucune exception n'est possible.
Personnellement, le deuxième ne me gène pas trop (surtout, que
les cas où il serait nécessaire sont bien rare). Mais c'est
contraire à la philosophie de C++, et n'était pas acceptable au
comité.

--
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
Brieuc Jeunhomme
Dans la pratique, on constate qu'il y a peu d'intérêt à y
aller en détail.


Oui, à cause du problème ci-dessus,


Non. Il y a peu d'intérêt à y aller en détail parce que ça ne
change rien pour l'utilisateur, et que ça pose des problèmes
dans l'hiérarchie des appels.


À mon sens, ça change beaucoup de choses pour l'utilisateur. Prenons un
exemple. Disons que je suis en train de réaliser un logiciel, qui doit
échanger des informations avec des serveurs distants, et que tout ce
petit monde se cause en XML sur HTTP. J'envoie donc des requêtes en
POST contenant une description de ce que je veux en XML, et je reçois
des réponses en XML que je dois interpréter quand tout va bien.

Il y a bien sûr plusieurs découpages en couches possibles. L'un d'entre
eux est le suivant:

// niveau le plus haut
const parsed_reply_t * send_request( std::array < arg_t > * args );

// une couche en-dessous
const dom_tree_t * req::receive_and_parse_reply();
void req::encode_and_send_request( const dom_tree_t * );

// encore une couche en-dessous
const std::string http_req::receive_reply();
void http_req::send_request( const std::string content );

// encore en-dessous
void tcp_socket::send( const void * data, ssize_t length );
ssize_t tcp_socket::recv( const void * buffer, ssize_t max_length );

Si j'ai une erreur réseau, la classe tcp_socket va légitimement lever
une exception, par exemple une network_error. La classe http_req, qui
utilise les tcp_socket, risque donc dans ses méthodes de lever
network_error. Mais elle peut aussi lever d'autres exceptions, par
exemple http_parse_error si le serveur en face envoie n'importe quoi.
La méthode receive_and_parse_reply de la classe req, peut donc lever
ces deux exceptions-là, plus xml_parse_error si le HTTP est correct
mais le XML renvoyé n'a pas le bon format. Et bien sûr, quasiment tout
le monde peut lever std::bad_alloc.

Si j'implémente send_request, quand j'appelle une méthode de req, je ne
suis pas intéressé seulement par savoir si elle peut ou non lever des
exceptions. Je veux savoir lesquelles: si j'ai une bad_alloc, il est
probable, dans la plupart des applications, que je vais laisser tomber
et quitter l'appli. Par contre, si j'ai une network_error, je vais
peut-être vouloir essayer un autre serveur, ou ouvrir une fenêtre
disant qu'il faut vérifier que les connexions réseau sont ok. Si j'ai
une xml_parse_error, je vais peut-être vouloir envoyer un mail à
Bref, je vais exécuter des actions
différentes en fonction des exceptions qui arrivent, je ne vais pas me
contenter de faire du code exception safe. Mais pour ça, j'ai besoin de
connaître au moins les familles d'exceptions qui risquent d'être
déclenchées.

L'intérêt d'utiliser une
exception, c'est que le traitement de l'erreur se trouve à un
endroit bien éloigné de sa détection. Et que la seule chose qui
intéresse toutes ces fonctions intermédiaire, c'est est qu'il y
a un cheminement supplémentaire à prendre en compte, à cause
d'une exception possible, ou non.


Plusieurs cheminements, en fonction de la nature de l'exception : je
traite tel problème de telle manière, tel autre problème de telle autre
manière, ou je ne sais pas faire.

Par exemple ? Je ne suis pas sûr de comprendre ce que tu essaies
de dire. C'est évident qu'une manque de mémoire doit provoquer
un type d'exception différent qu'une erreur de lecture de
disque. Selon le cas, c'est même possible qu'on traite ces deux
types d'erreur à des niveaux différents. Mais pour toutes les
fonctions intermédiaires, où est la différence ?


Certaines fonctions intermédiaires peuvent avoir la responsabilité de
traiter une partie des erreurs, et pas les autres erreurs. Mais pour
ça, encore faut-il qu'elles sachents quelles erreurs elles sont
suceptibles de voir passer.

Du moins est-ce le cas quand on utilise des librairies qui
lèvent joyeusement des exceptions dans des circonstances
presque normales, alors que l'erreur est rattrappable.


Si on abuse d'une facilité, c'est évident qu'on aurait des
problèmes supplémentaire:-).


Oui, mais malheureusement, on ne choisit pas toujours :=(

Je crois que dans le cas de Fabien, il a un type bien précis
d'exception, pour un type d'erreur bien précis, qu'il peut
traiter à part, à un niveau plus bas. Il ne prétend pas que sa
fonction ne pourrait jamais lever d'autre types d'exception,
genre std::bad_alloc.

Ce qu'il a, ce n'est pas un contrat en ce qui concerne les types
d'exception possibles. Ce qu'il a, c'est un contrat en ce qui
concerne le comportement dans un cas bien précis. Si ce cas se
présente, ET il n'y a pas d'autre erreur, il y aurait une
exception d'un type défini par le contrat.


Tout à fait. Mais ce que nous déplorons, c'est qu'il n'y ait pas moyen
d'établir un contrat plus précis, couvrant toutes les erreurs de
manière exhaustive. Dans mon exemple ci-dessus, mon logiciel peut avoir
pour mission d'essayer tous les serveurs HTTP indiqués dans un fichier
de configuration jusqu'à épuisement, et par contre, de laisser
complètement tomber s'il se rend compte que le code déployé sur ces
serveurs génère du XML mal formé. Pour ce faire, il a donc besoin de
connaître exhaustivement les erreurs qui peuvent lui tomber sur la tête
en provenance des classes gérant la partie réseau et la partie HTTP :
il ne doit laisser passer que certaines erreurs venant de là, après les
avoir triées sur le volet.

Je crois que tu vois le problème à l'envers. Le problème que tu
sembles décrire, ce n'est pas si une fonction f peut lever une
exception X. Le problème est si le cas Y va provoquer une
exception X. Et le problème annex : est-ce que l'exception X ne
peut se produire QUE dans le cas Y ?


Pas d'accord. Mon souci n'est pas d'imaginer la liste de tous les
problèmes qui peuvent se produire et de demander quelle est l'exception
associée. Mon souci est de dire en appelant cette fonction, qu'est-ce
que je risque ? C'est à elle de déclarer l'ensemble des situations
d'erreur, pas à moi de chercher à les deviner. Et comme cet ensemble
évolue dans le temps au fil des versions, je trouve que se contenter de
le documenter est insuffisant, j'aimerais que le compilateur puisse
m'aider un peu et en me disant attention, il y a un nouveau cas
d'erreur que tu n'adresses pas, et tu n'as pas non plus déclaré que tu
ne l'adressais pas dans ton proto, bref tu es passé à côté.

La spécification d'exception n'apporte rien ici. À vrai dire,
j'ai du mal à imaginer une construction du langage qui puisse y
apporter quelque chose.


Je crois que ça existe en java, mais je ne connais pas les détails de la
chose. Évidemment, c'est plus facile avec ce langage où la moitié du
travail se fait à l'exécution. Je ne pense pas qu'on puisse avoir une
solution 100% garantie en langage compilé, mais il doit y avoir moyen
de traiter la plus grosse partie du problème.

J'aurais préféré, moi-aussi, une vérifcation statique. Mais il
faut dire qu'il n'y a erreur que si l'exception est réelement
levée. Exiger une vérification statique aurait exiger ou bien
des moyens au delà de ce que savent faire les compilateurs
aujourd'hui, pour detecter tous les cas d'erreur


Au niveau du seul compilateur, en effet. Mais avec un coup de main de
l'éditeur de liens, je pense qu'on doit pouvoir gérer beaucoup plus de
choses.

ou bien du
code supplémentaire (des try/catch en plus) écrit par le
programmeur quand il sait qu'aucune exception n'est possible.
Personnellement, le deuxième ne me gène pas trop (surtout, que
les cas où il serait nécessaire sont bien rare).


Tout à fait.

Mais c'est
contraire à la philosophie de C++, et n'était pas acceptable au
comité.


C'est bien triste :=) Je ne comprends pas très bien cette philosophie,
le choix fait pour les exceptions (ceinture mais pas bretelles) me
paraît incohérent par rapport au choix réalisé par rapport au mot clé
const (ceinture et bretelles). Pour les exceptions, on se contente de
la promesse de celui qui a écrit le prototype de la fonction, et au
pire, s'il ne la tient pas, on relève vaguement l'erreur lors de
l'exécution, mais on ne s'émeut pas plus que ça de le voir appeler des
sous-fonctions qui n'ont pas pris le même engagement que lui concernant
la liste des exceptions déclenchées.

Pour le traitement des objets constants, au contraire, si le programmeur
s'engage à ne pas écrire sur un objet, on le traîne sur le banc
d'infâmie s'il a le malheur d'appeler une sous-fonction qui ne prend
pas le même engagement. Certes, on a mis à sa disposition le const_cast
pour s'auto-amnistier, mais, au moins, on sait que quand on écrit un
const_cast, on prend la responsabilité de ce qui va se passer. Et je
trouve que c'est la meilleure option : on est libre de faire ce qu'on
veut, mais par défaut, on est protégé contre 90% des situations à
problèmes.

--
BBP



Avatar
kanze
Brieuc Jeunhomme wrote:

[Il s'agit ici de l'utilisation des spécifications
d'exception, et l'intérêt qu'il peut y avoir d'y détailler
exactement toutes les exceptions qu'il peut y avoir, par
rapport à ne faire une distinction entre les fonctions qui
peuvent lever une exception, et celles qui garantissent ne
jamais en lever une.]

Dans la pratique, on constate qu'il y a peu d'intérêt à y
aller en détail.


Oui, à cause du problème ci-dessus,


Non. Il y a peu d'intérêt à y aller en détail parce que ça
ne change rien pour l'utilisateur, et que ça pose des
problèmes dans l'hiérarchie des appels.


À mon sens, ça change beaucoup de choses pour l'utilisateur.

Prenons un exemple. Disons que je suis en train de réaliser un
logiciel, qui doit échanger des informations avec des serveurs
distants, et que tout ce petit monde se cause en XML sur HTTP.
J'envoie donc des requêtes en POST contenant une description
de ce que je veux en XML, et je reçois des réponses en XML que
je dois interpréter quand tout va bien.

Il y a bien sûr plusieurs découpages en couches possibles.
L'un d'entre eux est le suivant:

// niveau le plus haut
const parsed_reply_t * send_request( std::array < arg_t > * args );

// une couche en-dessous
const dom_tree_t * req::receive_and_parse_reply();
void req::encode_and_send_request( const dom_tree_t * );

// encore une couche en-dessous
const std::string http_req::receive_reply();
void http_req::send_request( const std::string content );

// encore en-dessous
void tcp_socket::send( const void * data, ssize_t length );
ssize_t tcp_socket::recv( const void * buffer, ssize_t max_length );

Si j'ai une erreur réseau, la classe tcp_socket va
légitimement lever une exception, par exemple une
network_error. La classe http_req, qui utilise les tcp_socket,
risque donc dans ses méthodes de lever network_error. Mais
elle peut aussi lever d'autres exceptions, par exemple
http_parse_error si le serveur en face envoie n'importe quoi.
La méthode receive_and_parse_reply de la classe req, peut donc
lever ces deux exceptions-là, plus xml_parse_error si le HTTP
est correct mais le XML renvoyé n'a pas le bon format. Et bien
sûr, quasiment tout le monde peut lever std::bad_alloc.

Si j'implémente send_request, quand j'appelle une méthode de
req, je ne suis pas intéressé seulement par savoir si elle
peut ou non lever des exceptions. Je veux savoir lesquelles:
si j'ai une bad_alloc, il est probable, dans la plupart des
applications, que je vais laisser tomber et quitter l'appli.
Par contre, si j'ai une network_error, je vais peut-être
vouloir essayer un autre serveur, ou ouvrir une fenêtre disant
qu'il faut vérifier que les connexions réseau sont ok. Si j'ai
une xml_parse_error, je vais peut-être vouloir envoyer un mail
à Bref, je vais exécuter des
actions différentes en fonction des exceptions qui arrivent,
je ne vais pas me contenter de faire du code exception safe.
Mais pour ça, j'ai besoin de connaître au moins les familles
d'exceptions qui risquent d'être déclenchées.


Au niveau que tu veux traiter une erreur, il est clair qu'il
faut que tu saches comment l'erreur est renseignée -- un
try/catch autour d'une fonction qui renseigne l'erreur au moyen
d'une valeur de rétour ne va pas marcher, de même qu'un catch
d'un type qui ne correspond pas au type de l'exception.

Je n'ai jamais essayé de nier ça. Ce que je ne comprends pas,
c'est ce que les spécifications d'exception viennent faire
là-dedans. La spécification de l'exception ne permet pas à dire
ça. Elle n'a jamais prétendu le permettre. Tout au plus, la
spécification d'exception permettrait à garantir que la fonction
ne lève pas d'autre exception que network_error. Mais ça ne nous
intéresse pas énormement. Si je m'intéresse aux erreurs du
reseau, je veux le contraire : une garantie que l'erreur sera
détectée (d'abord !), et ensuite, l'information sur comment elle
est renseignée. Et si ne m'intéresse pas à ce genre d'erreur (à
ce niveau, au moins), tout ce qui m'intéresse, c'est s'il faut
que je prends en compte les flux de programme supplémentaire dus
aux exceptions ou non.

Ce que j'essaie à faire comprendre, c'est que le message d'une
spécification d'exception est négatif. Il te dit ce qui ne peut
pas se passer, pas ce qui va se passer dans tel ou tel cas. Et
que le seul message négatif qui est intéressant, c'est que je
n'ai pas à me soucier d'une possibilité d'une exception,
quoiqu'elle soit.

Ça ne veut pas dire que parfois (je dirais même prèsque
toujours), il me faut aussi des informations positives. Et que
certaines de ces informations peuvent prendre la forme : si
condition X, alors exception de type Y. Mais note bien que cette
information n'est intéressant qu'avec le si. Ce que je veux
traiter, ce n'est pas une exception, mais une condition
(d'erreur ou d'autre). L'exception n'est intéressante que dans
le mesure qu'elle sert à renseigner la condition.

L'intérêt d'utiliser une exception, c'est que le traitement
de l'erreur se trouve à un endroit bien éloigné de sa
détection. Et que la seule chose qui intéresse toutes ces
fonctions intermédiaire, c'est est qu'il y a un cheminement
supplémentaire à prendre en compte, à cause d'une exception
possible, ou non.


Plusieurs cheminements, en fonction de la nature de
l'exception : je traite tel problème de telle manière, tel
autre problème de telle autre manière, ou je ne sais pas
faire.


D'abord, les problèmes que tu peux traiter localement ne doivent
pas être renseignés par des exceptions. Deuxièmement, qui a dit
le contraire ? Et quel rapport avec les spécifications
d'exception ?

Par exemple ? Je ne suis pas sûr de comprendre ce que tu
essaies de dire. C'est évident qu'une manque de mémoire doit
provoquer un type d'exception différent qu'une erreur de
lecture de disque. Selon le cas, c'est même possible qu'on
traite ces deux types d'erreur à des niveaux différents.
Mais pour toutes les fonctions intermédiaires, où est la
différence ?


Certaines fonctions intermédiaires peuvent avoir la
responsabilité de traiter une partie des erreurs, et pas les
autres erreurs. Mais pour ça, encore faut-il qu'elles sachents
quelles erreurs elles sont suceptibles de voir passer.


Quelles erreurs. Exactement. Et une spécification d'exception
est muette à cet égard.

Du moins est-ce le cas quand on utilise des librairies qui
lèvent joyeusement des exceptions dans des circonstances
presque normales, alors que l'erreur est rattrappable.


Si on abuse d'une facilité, c'est évident qu'on aurait des
problèmes supplémentaire:-).


Oui, mais malheureusement, on ne choisit pas toujours :=(

Je crois que dans le cas de Fabien, il a un type bien précis
d'exception, pour un type d'erreur bien précis, qu'il peut
traiter à part, à un niveau plus bas. Il ne prétend pas que
sa fonction ne pourrait jamais lever d'autre types
d'exception, genre std::bad_alloc.

Ce qu'il a, ce n'est pas un contrat en ce qui concerne les
types d'exception possibles. Ce qu'il a, c'est un contrat en
ce qui concerne le comportement dans un cas bien précis. Si
ce cas se présente, ET il n'y a pas d'autre erreur, il y
aurait une exception d'un type défini par le contrat.


Tout à fait. Mais ce que nous déplorons, c'est qu'il n'y ait
pas moyen d'établir un contrat plus précis, couvrant toutes
les erreurs de manière exhaustive.


Je suis ouvert aux propositions. Je serais curieux à savoir
comment ça pourrait se faire. Je ne connais pas de langage qui
le fait. Modula-3 s'y approche, mais en partie simplement en
ôtant la possibilité de traiter les erreurs les plus basses, du
genre manque de mémoire. Et encore, il exige que tous les
niveaux intermédiaire soient au courant -- je suis arrivé à
croire que ce n'est pas une si bonne idée que ça.

Dans mon exemple ci-dessus, mon logiciel peut avoir pour
mission d'essayer tous les serveurs HTTP indiqués dans un
fichier de configuration jusqu'à épuisement, et par contre, de
laisser complètement tomber s'il se rend compte que le code
déployé sur ces serveurs génère du XML mal formé. Pour ce
faire, il a donc besoin de connaître exhaustivement les
erreurs qui peuvent lui tomber sur la tête en provenance des
classes gérant la partie réseau et la partie HTTP : il ne doit
laisser passer que certaines erreurs venant de là, après les
avoir triées sur le volet.

Je crois que tu vois le problème à l'envers. Le problème que
tu sembles décrire, ce n'est pas si une fonction f peut
lever une exception X. Le problème est si le cas Y va
provoquer une exception X. Et le problème annex : est-ce que
l'exception X ne peut se produire QUE dans le cas Y ?


Pas d'accord. Mon souci n'est pas d'imaginer la liste de tous
les problèmes qui peuvent se produire et de demander quelle
est l'exception associée. Mon souci est de dire en appelant
cette fonction, qu'est-ce que je risque ? C'est à elle de
déclarer l'ensemble des situations d'erreur, pas à moi de
chercher à les deviner. Et comme cet ensemble évolue dans le
temps au fil des versions, je trouve que se contenter de le
documenter est insuffisant, j'aimerais que le compilateur
puisse m'aider un peu et en me disant attention, il y a un
nouveau cas d'erreur que tu n'adresses pas, et tu n'as pas non
plus déclaré que tu ne l'adressais pas dans ton proto, bref tu
es passé à côté.


Je l'aimerais aussi. Mais je ne vois pas comment c'est possible.
Il y a trop d'aspects sémantiques de l'application dedans.

La spécification d'exception n'apporte rien ici. À vrai
dire, j'ai du mal à imaginer une construction du langage qui
puisse y apporter quelque chose.


Je crois que ça existe en java, mais je ne connais pas les
détails de la chose.


Java a deux types d'exceptions : celles qu'on doit déclarer, et
celles qu'on n'a pas besoin de déclarer. C-à-d qu'il n'y a aucun
moyen à déclarer ce qui est le plus important, qu'une fonction
ne lève jamais d'exception, mais qu'on est souvent obligé à
déclarer des choses sans intérêt -- une partie des exceptions,
mais pas toutes.

En fait, c'est un peu mon expérience avec Java qui m'a convaincu
que cette approche ne marche pas. Idéalement, je comprends bien
ton point de vue.Idéalement, à partir de la déclaration de la
fonction, on verrait toutes les informations qu'elle pourrait
renseigner, et dans chaque fonction, il faudrait ou bien traiter
le cas, ou bien le propager plus haut, en déclarant qu'on peut
le renseigner à nos utilisateurs. Idéalement, parce que si on
l'essaie, on se heurte à deux problèmes de taille :

-- Les erreurs du plus bas niveau (du genre manque de mémoire)
propagent souvent très haut. Du coup, prèsque toutes les
fonctions dans l'application doivent les déclarer, ce qui
est plus que fastidieux pour le programmeur. C'est pour
contourner ce problème que Java a créé les deux types
d'exceptions ; malheureusement, leur solution fait qu'on ne
peut plus déclarer qu'il n'y a pas d'exception du tout, même
du plus bas niveau.

-- Toutes les exceptions (ou conditions particulières) ne sont
pas renseigées par des exceptions. Dans le cas de Java,
encore, ce que je constate, c'est que prèsque toutes les
exceptions « vérifiées » sont en fait des états qu'on
renseigne la plupart du temps avec des codes de rétour en
C++, parce que ce sont des erreurs qu'il faut typiquement
traiter tout de suite. Et que Java y utilise les exceptions
surtout parce qu'il n'a pas de paramètres par référence, et
donc pas de moyen d'implémenter des méthodes « out ».

Alors, il s'avère que pour des raisons historiques, en C++,
on peut ignorer un code de rétour (et encore plus,
évidemment, de tester un état de l'objet, comme avec
iostream). Et il s'avère que la plupart des erreurs qui nous
intéressent directement sont renseignées au moyen des codes
de rétour. Du coup, qu'est-ce que nous apportera une
vérification plus forte des spécifications d'exception, s'il
n'y a pas de vérification dans les cas les plus fréquents ?

Le tout pour dire que ce n'est pas un problème simple. Que je
comprends ce que tu veux, et que j'aimerais aussi l'avoir. Mais
que les spécifications d'exception joue un autre rôle,
également, sinon plus important. Et que je vois assez mal
comment on pourrait les faire évoluer pour remplir ce rôle.

Dans la contexte du C++. Je verrais bien un langage où on ne
pourrait pas ignorer une valeur de rétour (c'est le cas de Ada,
je crois), où la spécification d'exception fonctionnait comme en
Java, mais qu'il y avait en plus une spécification spéciale
(disons « nothrow ») pour dire qu'on ne lève pas la moindre
exception. Mais sans pouvoir ignorer un code de rétour, ce
langage ne serait pas C++. (Et ça ne résoud pas tous les
problèmes -- que faire des systèmes comme iostream ?)

Évidemment, c'est plus facile avec ce langage où la moitié du
travail se fait à l'exécution. Je ne pense pas qu'on puisse
avoir une solution 100% garantie en langage compilé, mais il
doit y avoir moyen de traiter la plus grosse partie du
problème.


D'autres langages compilés sont beaucoup plus stricts à cet
égard. Mais il s'agit d'un ensemble. Tu ne peux pas t'attaquer
aux spécifications d'exceptions sans t'attaquer à beaucoup
d'autres choses aussi. Je ne dis pas que ça serait mal, mais le
résultat ne serait pas C++.

J'aurais préféré, moi-aussi, une vérifcation statique. Mais
il faut dire qu'il n'y a erreur que si l'exception est
réelement levée. Exiger une vérification statique aurait
exiger ou bien des moyens au delà de ce que savent faire les
compilateurs aujourd'hui, pour detecter tous les cas
d'erreur


Au niveau du seul compilateur, en effet. Mais avec un coup de
main de l'éditeur de liens, je pense qu'on doit pouvoir gérer
beaucoup plus de choses.


J'y ai pensé un peu. Mais c'est loin d'être trival.

Note que le fait de répousser un certain nombre de chose à
l'édition de liens donne d'autres avantages aussi -- et en ce
qui concerne la detection des erreurs de programme, et en ce qui
concerne l'optimisation. À mon avis, on y arrivera ; déjà
certains compilateurs en font en partie. Mais on n'est pas
encore là.

ou bien du code supplémentaire (des try/catch en plus) écrit
par le programmeur quand il sait qu'aucune exception n'est
possible. Personnellement, le deuxième ne me gène pas trop
(surtout, que les cas où il serait nécessaire sont bien
rare).


Tout à fait.

Mais c'est contraire à la philosophie de C++, et n'était pas
acceptable au comité.


C'est bien triste :=) Je ne comprends pas très bien cette
philosophie,


C'est simple : il n'y a que des experts qui écrivent du code, il
ne font pas d'erreurs, et de toute façon, ce n'est pas grave si
le programme ne fonctionne pas bien dans les cas limites.

J'exagère énormement, évidemment. Mais le fait reste que la
réussite du C++ est en partie à cause de son attraction à
beaucoup de communités différentes, avec des priorités
différentes. Et qu'il y a au moins une partie de la communité
C++ qui vient du milieu C (où au départ ce que je viens de dire
n'était pas une exagération), et que souvent, c'est plus
difficile à faire évoluer les hommes que le logiciel.

le choix fait pour les exceptions (ceinture mais pas
bretelles) me paraît incohérent par rapport au choix réalisé
par rapport au mot clé const (ceinture et bretelles).


Je me rappelle un de mes premiers postings dans comp.lang.c++
(bien avant qu'il y avait comp.lang.c++.moderated ou
fr.comp.lang.c++) : suite à une discussion sur le const logique,
j'ai posté quelque chose du genre : « In sum, const means
exactly what the author of the class wants it to mean. » Andy
Koenig a répondu avec un seul mot : « Yes ».

Voilà pour const.

Pour les exceptions, on se contente de la promesse de celui
qui a écrit le prototype de la fonction, et au pire, s'il ne
la tient pas, on relève vaguement l'erreur lors de
l'exécution, mais on ne s'émeut pas plus que ça de le voir
appeler des sous-fonctions qui n'ont pas pris le même
engagement que lui concernant la liste des exceptions
déclenchées.


C'est le cas de toutes les autres post-conditions, non ?

Pour le traitement des objets constants, au contraire, si le
programmeur s'engage à ne pas écrire sur un objet, on le
traîne sur le banc d'infâmie s'il a le malheur d'appeler une
sous-fonction qui ne prend pas le même engagement.


Ça ne dépasse pas l'objet même. Au sens extrèmement limité de
« objet » qui sert dans les normes. Il n'y a rien dans le
langage qui empèchera vector<>::operator[] const de renvoyer une
référence non-const, par exemple.

Dans le cas des spécifications d'exception, on a aussi quelques
limites. On doit le respecter, par exemple, si on supplate la
fonction.

Certes, on a mis à sa disposition le const_cast pour
s'auto-amnistier, mais, au moins, on sait que quand on écrit
un const_cast, on prend la responsabilité de ce qui va se
passer. Et je trouve que c'est la meilleure option : on est
libre de faire ce qu'on veut, mais par défaut, on est protégé
contre 90% des situations à problèmes.


Je ne dis pas le contraire. Mais je constate que dans ce cas-ci,
les langages qui ont cherché à faire mieux ont ou bien perdu
d'autres choses plus importantes ou bien part d'une base
tellement différente de C++ qu'on ne peut pas vraiment comparer.
Les éléments dans un langage n'existe pas en isolation, et même
si j'aimerais que C++ choisisse plus souvent le défaut plus sûr,
j'ai bien l'impression que pour le faire, il faudrait réfondre
le langage complet. Et alors, ça ne serait plus le C++.

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




1 2