Twitter iPhone pliant OnePlus 11 PS5 Disney+ Orange Livebox Windows 11

classes de gestion des exceptions

6 réponses
Avatar
Aurélien Barbier-Accary
Bonjour,

je débute dans l'utilisation des assert et autres try-catch et je bloque sur
l'affichage du fichier et de la ligne de l'erreur car __FILE__ et __LINE__ ne
sont pas évaluées là où je le souhaiterai.
Comme je pense qu'un exemple sera plus parlant que du blabla, voila un petit
bout de code qui me pose pb :

//Ma classe d'erreur (olm_error.hpp) :
class Olm_Error : public exception
{
private:
string msg;

public:
explicit Olm_Error(const char* errmsg, const char* file = NULL, int line =
-1) throw();
virtual ~Olm_Error(void) throw() {}

virtual const char* what(void) const throw()
{ return msg.c_str(); }
};


//Une classe qui utilise la gestion d'erreurs (olm_scalar.hpp) :
#include "olm_error.hpp"
class Olm_Scalar : public Olm_Object
{
//...
double x;
//...

Olm_Scalar& operator/=(const Olm_Scalar& s) // en inline !!!
try{ if (s.x==0.0) throw Olm_Error("division by 0",__FILE__,__LINE__);
x/=s.x; return *this; }
catch(Olm_Error& e) { cerr << e.what() << endl; }

//...
};


// Exemple de programme déficient (main.cpp) :
#include "olm_scalar.hpp"
int main(int argc, char* argv[])
{
Olm_Scalar x(3.14);
x/=0;
return EXIT_SUCCESS;
}

-> le message d'erreur que j'obtiens est
olm_scalar.hpp - 44: division by 0
alors que je souhaiterai avoir
main.cpp - 5: division by 0

En fait le fait de définir la fonction inline ne produit pas l'effet escompté.
Comment faire ?

Merci d'avance.

Aurélien.

6 réponses

Avatar
Philippe
Bonjour,

Aurélien Barbier-Accary wrote:
Bonjour,

je débute dans l'utilisation des assert et autres try-catch et je bloque
sur l'affichage du fichier et de la ligne de l'erreur car __FILE__ et
__LINE__ ne sont pas évaluées là où je le souhaiterai.
Olm_Scalar& operator/=(const Olm_Scalar& s) // en inline !!!
try{ if (s.x==0.0) throw Olm_Error("division by
0",__FILE__,__LINE__); x/=s.x; return *this; }


Une solution est de definir une macro dans ton olm_error.hpp:

#define OLM_ERROR(text) Olm_Error(text, __FILE__, __LINE__)

et de l'utiliser pour lancer l'exception:

throw OLM_ERROR("division by 0");

HTH,
Philippe

Avatar
Marc Boyer
Aurélien Barbier-Accary a écrit :
je débute dans l'utilisation des assert et autres try-catch et je bloque sur
l'affichage du fichier et de la ligne de l'erreur car __FILE__ et __LINE__ ne
sont pas évaluées là où je le souhaiterai.


Comme l'a présenté Philippe, pour un appel de fonction, la solution
classique est d'utiliser une macro enveloppe.
Mais pour les opérateurs comme tu le fais, je ne vois pas de solution.

Marc Boyer
--
À vélo, prendre une rue à contre-sens est moins dangeureux
que prendre un boulevard dans le sens légal. À qui la faute ?

Avatar
kanze
Philippe wrote:

Aurélien Barbier-Accary wrote:

je débute dans l'utilisation des assert et autres try-catch
et je bloque sur l'affichage du fichier et de la ligne de
l'erreur car __FILE__ et __LINE__ ne sont pas évaluées là où
je le souhaiterai. Olm_Scalar& operator/=(const Olm_Scalar&
s) // en inline !!! try{ if (s.x==0.0) throw
Olm_Error("division by 0",__FILE__,__LINE__); x/=s.x; return
*this; }


Une solution est de definir une macro dans ton olm_error.hpp:

#define OLM_ERROR(text) Olm_Error(text, __FILE__, __LINE__)

et de l'utiliser pour lancer l'exception:

throw OLM_ERROR("division by 0");


Mais si j'ai bien compris, son problème n'est pas l'insertion
automatique de __FILE__ et de __LINE__ -- il a l'air de se
contenter de les insérer à la main. Son problème, c'est qu'il
detecte le problème dans une fonction, et le nom de fichier et
le numéro de la ligne qui l'intéresse, ce sont ceux de l'appel
de la fonction.

On pourrait poser la question si les exceptions sont le
mechanisme qui convient ici, mais le problème se pose de la même
façon pour les logs ou les messages en cas d'erreur fatale. Et
en fait, autant que je sache, il n'y a que deux solutions
possibles : faire passer l'information par l'appelant, ou
remonter la pile pour trouver les adresses de rétour, puis à
partir de ces adresses, trouver où on était.

Or, la première solution ne peut pas réelement servir dans le
cas des opérateurs, et même dans les autres cas, introduit des
contraints au niveau de l'utilisateur (paramètre supplémentaire
qui est en fait un macro, etc.). Et la deuxième a besoin du code
qui dépend de l'implémentation. J'ai une classe qui remonte bien
la pile, mais le code de l'implémentation n'est pas le même
selon l'architecture, et même parfois le compilateur, la version
de l'OS, etc. En plus, il ne sort que les adresses
hexadécimales.

--
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
Aurélien Barbier-Accary
Une solution est de definir une macro dans ton olm_error.hpp:

#define OLM_ERROR(text) Olm_Error(text, __FILE__, __LINE__)

et de l'utiliser pour lancer l'exception:

throw OLM_ERROR("division by 0");

HTH,
Philippe


oui j'y avais bien pensé mais je cherchais une solution plus "élégante" et
surtout qui ne nécessite pas de définir une macro pour le constructeur de
chacune de mes classes d'erreurs.
Je me dis que ça doit bien exister, je vais continuer à chercher.

En tous cas merci pour ta réponse.

Aurélien.

Avatar
Aurélien Barbier-Accary
... Son problème, c'est qu'il
detecte le problème dans une fonction, et le nom de fichier et
le numéro de la ligne qui l'intéresse, ce sont ceux de l'appel
de la fonction.

tout à fait


On pourrait poser la question si les exceptions sont le
mechanisme qui convient ici, mais le problème se pose de la même
façon pour les logs ou les messages en cas d'erreur fatale. Et
en fait, autant que je sache, il n'y a que deux solutions
possibles : faire passer l'information par l'appelant, ou
remonter la pile pour trouver les adresses de rétour, puis à
partir de ces adresses, trouver où on était.

Or, la première solution ne peut pas réelement servir dans le
cas des opérateurs, et même dans les autres cas, introduit des
contraints au niveau de l'utilisateur (paramètre supplémentaire
qui est en fait un macro, etc.). Et la deuxième a besoin du code
qui dépend de l'implémentation. J'ai une classe qui remonte bien
la pile, mais le code de l'implémentation n'est pas le même
selon l'architecture, et même parfois le compilateur, la version
de l'OS, etc. En plus, il ne sort que les adresses
hexadécimales.



Tout d'abord merci pour cette réponse détaillée.

Jusqu'ici je gérais les erreurs "à la main" par des bêtes
if (pb)
{
std::cerr << "PB" << std::endl;
exit(1);
}
mais je déplorais le fait d'être obligé de recourir à gdb ou autre en cas de pb
puisque cela impose de travailler au sein d'un environnement particulier et en
mode debug.
Là je cherche à définir des libs capables de garantir la robustesse des calculs
et en cas de mauvaise utilisation de prévenir l'utilisateur et de l'informer sur
la source effective de son erreur.
Donc ma question est: "quel est alors l'intérêt des exceptions" ?
On pourrait répondre que ça permet une lecture sémantique plus agréable du code
ou encore que ça autorise une gestion internationalisée des messages d'erreur
mais je crois pour ma part qu'un message d'erreur du genre "division par 0 ->
fin" est totalement inutile si on n'a pas plus d'information sur la source du
problème.
Je suis perplexe...

Pour revenir à tes remarques, effectivement il n'est pas envisageable de faire
passer ces informations par l'appelant.
Pour ce qui est de remonter la pile, une seule étape serait déjà une bonne chose
et je pensais que les exceptions "intégraient" ce mécanisme en gérant l'erreur à
partir de l'appelant et non pas dans la fonction appelée (au moins pour les
opérateurs !) :-(

Pour info, en quelques mots comment fonctionne ta classe qui remonte la pile ?
Dois-tu passer l'adresse de l'appelant à chaque fonction ? Si oui comment
gères-tu les opérateurs ?

Avatar
kanze
Aurélien Barbier-Accary wrote:
... Son problème, c'est qu'il detecte le problème dans une
fonction, et le nom de fichier et le numéro de la ligne qui
l'intéresse, ce sont ceux de l'appel de la fonction.


tout à fait

On pourrait poser la question si les exceptions sont le
mechanisme qui convient ici, mais le problème se pose de la
même façon pour les logs ou les messages en cas d'erreur
fatale. Et en fait, autant que je sache, il n'y a que deux
solutions possibles : faire passer l'information par
l'appelant, ou remonter la pile pour trouver les adresses de
rétour, puis à partir de ces adresses, trouver où on était.

Or, la première solution ne peut pas réelement servir dans
le cas des opérateurs, et même dans les autres cas,
introduit des contraints au niveau de l'utilisateur
(paramètre supplémentaire qui est en fait un macro, etc.).
Et la deuxième a besoin du code qui dépend de
l'implémentation. J'ai une classe qui remonte bien la pile,
mais le code de l'implémentation n'est pas le même selon
l'architecture, et même parfois le compilateur, la version
de l'OS, etc. En plus, il ne sort que les adresses
hexadécimales.


Tout d'abord merci pour cette réponse détaillée.

Jusqu'ici je gérais les erreurs "à la main" par des bêtes
if (pb)
{
std::cerr << "PB" << std::endl;
exit(1);
}

mais je déplorais le fait d'être obligé de recourir à gdb ou
autre en cas de pb puisque cela impose de travailler au sein
d'un environnement particulier et en mode debug.

Là je cherche à définir des libs capables de garantir la
robustesse des calculs et en cas de mauvaise utilisation de
prévenir l'utilisateur et de l'informer sur la source
effective de son erreur.

Donc ma question est: "quel est alors l'intérêt des
exceptions" ?


S'il s'agit réelement des erreurs d'utilisation -- ce que la
norme appelle « undefined behavior », quand il s'agit de la
bibliothèque standard -- je dirais qu'un assert convient plus
qu'une exception. Dans la plupart des cas, en tout cas.

On pourrait répondre que ça permet une lecture sémantique plus
agréable du code ou encore que ça autorise une gestion
internationalisée des messages d'erreur mais je crois pour ma
part qu'un message d'erreur du genre "division par 0 -> fin"
est totalement inutile si on n'a pas plus d'information sur la
source du problème.


Dans la pratique, ces messages-là seront en anglais (dans un
projet qui sert au delà qu'uniquement en France), et on ne s'en
occupe pas trop de leur internationalisation. Mais si on veut...
on peut internationaliser des messages en provenance d'un assert
privé aussi bien que ceux transmis par une exception.

Je suis perplexe...

Pour revenir à tes remarques, effectivement il n'est pas
envisageable de faire passer ces informations par l'appelant.

Pour ce qui est de remonter la pile, une seule étape serait
déjà une bonne chose et je pensais que les exceptions
"intégraient" ce mécanisme en gérant l'erreur à partir de
l'appelant et non pas dans la fonction appelée (au moins pour
les opérateurs !) :-(

Pour info, en quelques mots comment fonctionne ta classe qui
remonte la pile ?


D'une façon différente selon le hardware:-).

Sérieusement, actuellement, je ne travaille que sur Sparc (sous
Solaris) et PC (sous Linux). Et ces deux plateformes ont en
commun que la pile croît vers le bas, et qu'elle est
parfaitement linéaire au niveau de l'adressage. Alors, il suffit
de prendre l'adresse d'une variable locale (ou de récupérer des
informations d'un setjmp) pour avoir un point de départ.
Ensuite, c'est très dépendant de la machine : on ajoute une
valeur « magique » à l'adresse pour se retrouver au niveau de la
frame, qui contient l'adresse de rétour ET l'adresse du frame
précédant. (Dans le cas du Sparc, il faut prendre des
précautions en plus pour s'assurer que la pile est réelement en
mémoire.)

Pour Linux sur PC (Intel), ça donne quelque chose du genre :

class StackFrameIter
{
public:
StackFrameIter() ;

bool isDone() const ;
void next() ;
unsigned current() const ;

private:
struct Frame
{
Frame* next ;
unsigned returnAddr ;
} ;
Frame* myCurrent ;
} ;

inline
StackFrameIter::StackFrameIter()
{
void* fp = &fp + 1 ;
myCurrent = static_cast< Frame* >( fp )->next ;
}

inline bool
StackFrameIter::isDone() const
{
return myCurrent == NULL ;
}

inline void
StackFrameIter::next()
{
if ( ! isDone() ) {
myCurrent = myCurrent->next ;
}
}

inline unsigned
StackFrameIter::current() const
{
return myCurrent->returnAddr ;
}

(En régardant, je me pose des questions sur l'inline.
Normalement, une fonction inline n'a pas de frame. Mais ce code
est conçu pour être appelé dans un seul endroit ; il marche là,
et ça me suffit.)

Dois-tu passer l'adresse de l'appelant à chaque fonction ? Si
oui comment gères-tu les opérateurs ?


Je ne passe rien. Je remonte la pile, uniquement quand j'en ai
besoin.

Actuellement, je ne sais sortir que des adresses hexadécimales.
Pour moi (qui vient du hardware et de l'assembleur), ça suffit ;
je sais générer et exploiter un map de link. Sinon, il faut
exploiter les informations de debug dans l'exécutable pour
pouvoir afficher l'adresse symbolique. Ce qui est loin d'être
trivial. (Pour commencer, il n'y a pas de manière standard pour
trouver le fichier que tu es en train d'exécuter.) Il y a un
programme addr2line chez GNU (dans binutils), qui fonctionne
bien avec g++ sous Linux (ou Solaris) ; même si ça ne marche pas
chez toi, ça pourrait te donner des idées.

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