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

Classe pour la verification de conditions

16 réponses
Avatar
Aurélien REGAT-BARREL
Bonjour à tous,
Si vous avez lu le thread "[TORDU] Exception dans un destructeur, oui
mais..." vous savez que je suis en train de réfléchir à une meilleur gestion
des erreurs dans mon programme. Le critère numéro un est la simplicité
d'utilisation, pour encourager le programmeur à l"utiliser.
Le but est de simplifier au maximum un tel bloc de vérification:

int value = ...;
if ( value >= 10 )
{
std::ostringstream oss;
oss << "La valeur n'est pas un chiffre : " << value;
throw std::runtime_error( oss.str() );
}

j'étais parti dans un truc de ce genre:

verify( value < 10 ) << "La valeur n'est pas un chiffre : " << value;

Mais l'implémentation n'est pas top (exception lancée dans un destructeur),
et finalement je trouve ça moyennement explicite.

Après réflexion, je me suis dit que ce qui était le plus important en fin de
compte c'était de garder une trace des valeurs erronées. Le reste, à partir
du moment où on a l'emplacement dans le fichier on retrouve tout.
Alors voici ma nouvelle idée:

// la valeur doit être un chiffre
verify( value ) < 10;

verify() construit et renvoie un Verifier et c'est l'opérateur == de ce
dernier qui fait le test et lève ou non l'exception.

template <typename T>
class Verifier
{
public:
Verifier( const T & p1 ) : p1_( p1 ) {}

void operator == ( const T & p2 )
{
if ( ! ( this->p1_ == p2 ) )
{
std::ostringstream oss;
oss << "Verification failed with "
<< this->p1_ << " == " << p2;
throw std::runtime_error( oss.str() );
}
}

private:
T p1_;
};

template<typename T>
Verifier<T> verify( const T & t )
{
return Verifier<T>( t );
}

Je sais que boost a un truc pour simplifier la déclaration des opérateurs.
Ne connaissant pas la bête, je me suis contenté d'une macro pour simplifier
la tâche:

template <typename T>
class Verifier
{
public:
Verifier( const T & p1 ) : p1_( p1 ) {}

#define OPERATOR(op) \
void operator op ( const T & p2 ) \
{\
if ( ! ( this->p1_ op p2 ) )\
{\
std::ostringstream oss;\
oss << "Verification failed with "\
<< this->p1_ << " " #op " " << p2;\
throw std::runtime_error( oss.str() );\
}\
}

OPERATOR( == )
OPERATOR( < )
OPERATOR( > )
OPERATOR( != )
OPERATOR( >= )
OPERATOR( <= )

#undef OPERATOR

private:
T p1_;
};

les opérateurs renvoient void, c'est pas innocent c'est pour empêcher de les
utiliser dans un autre contexte, je sais pas trop ce que ça ferait.

Jusque là j'aimerais avoir votre avis.

Mais attention j'ai l'esprit un peu tordu et ça va plus loin. Allons-y pour
l'usine à gaz. Moyennant une petite macro verify() qui permet de connaitre
le fichier & la ligne où on se situe :

#define verify(x) Verify( x, __FILE__, __LINE__ )

on peut imaginer que Verify initialiserait l'objet Verifier afin que ce
dernier localise l'emplacement de l'erreur. Comme assert() en fait. Sauf
que, j'ai parlé d'usine à gaz....

- Imaginons qu'en cas d'échec Verifier construise un code d'erreur qui
permette d'identifier le fichier et la ligne de l'échec.
- Imaginons qu'un stupide logiciel / script d'extraction de commentaires
génère un gros fichier qui contienne tous les commentaires situés sur la
même ligne ou une ligne au dessus d'un appel à verify()
- et imaginons enfin que ce fichier est structuré de manière à facilement
retrouver le commentaire "associé" à ce couple erreur (fichier - ligne)

eh ben dans un tel cas on pourrai obtenir en message d'erreur le commentaire
associé au verify() qui a échoué, et écrire des commentaires ferait partie
de la procédure de gestion des erreurs. Ces commentaires servent aussi à
documenter le projet...
Et on peut imaginer que le fichier résultat puisse être traduit par la suite
afin d'offrir des messages d'erreurs dans la langue que l'on veut.
Que pensez-vous de cette approche ?

--
Aurélien REGAT-BARREL

10 réponses

1 2
Avatar
Loïc Joly
Aurélien REGAT-BARREL wrote:

Moyennant une petite macro verify() qui permet de connaitre
le fichier & la ligne où on se situe :

#define verify(x) Verify( x, __FILE__, __LINE__ )


Je croyais que tu étais opposé à une solution à base de macro ?

--
Loïc

Avatar
drkm
Loïc Joly writes:

Aurélien REGAT-BARREL wrote:

Moyennant une petite macro verify() qui permet de connaitre
le fichier & la ligne où on se situe :
#define verify(x) Verify( x, __FILE__, __LINE__ )


Je croyais que tu étais opposé à une solution à base de macro ?


S'il veut utiliser __LINE__ et __FILE__, il n'a pas beaucoup de
choix.

--drkm


Avatar
Aurelien REGAT-BARREL
#define verify(x) Verify( x, __FILE__, __LINE__ )


Je croyais que tu étais opposé à une solution à base de macro ?


Je préfèrerais sans, mais tant que ça remplace un (pseudo) appel de fonction
par un autre, ça m'est supportable. Ce qui me gênait dans ta macro c'est que
c'était transformé en for(;;) ou autre suite d'instructions.
Mais bon j'ai un peu fumé avec cette usine à gaz, la réalité des objectifs à
ternir dans les temps me rattrappe et j'ai plus le temps de m'amuser...
Mais je me suis fait une autre classe sympa, que je compte bien soumettre
ici un de ces quatre :-) (j'ai découvert qu'on pouvait supplanter une
fonction virtuelle d'une classe de base par celle d'une classe template
fille, ça m'a donné des idées...).

--
Aurélien REGAT-BARREL


Avatar
Olivier Azeau
Aurélien REGAT-BARREL wrote:
Jusque là j'aimerais avoir votre avis.


Mon avis c'est que la solution précédente était sympa grace à la
possibilité d'écrire son propre message.
Dans ta dernière solution, les messages risquent d'être fort peu
explicites puisque seules les valeurs numériques vont apparaître, pas
les appels qui les ont générées...

Verification failed with 12 > 10

Ca risque de ne pas être très parlant...


Mais attention j'ai l'esprit un peu tordu et ça va plus loin. Allons-y pour
l'usine à gaz. Moyennant une petite macro verify() qui permet de connaitre
le fichier & la ligne où on se situe :

#define verify(x) Verify( x, __FILE__, __LINE__ )


Si on en vient aux macros, on peut légitimement se demander l'utilité de
tout le reste.

#define verify(cond,txt)
{
if ( !(cond) )
{
std::ostringstream oss;
oss << txt << " [" << #cond << "] at line " << __LINE__ << " in
file " << __FILE__;
throw std::runtime_error( oss.str() );
}
}


verify( unEntier()<3, "mon entier (valeur=" << unEntier() << ") ne
convient pas" );

-> mon entier (valeur=5) ne convient pas [unEntier()<3] at line 1280 in
file toto.cpp

on peut imaginer que Verify initialiserait l'objet Verifier afin que ce
dernier localise l'emplacement de l'erreur. Comme assert() en fait. Sauf
que, j'ai parlé d'usine à gaz....

- Imaginons qu'en cas d'échec Verifier construise un code d'erreur qui
permette d'identifier le fichier et la ligne de l'échec.
- Imaginons qu'un stupide logiciel / script d'extraction de commentaires
génère un gros fichier qui contienne tous les commentaires situés sur la
même ligne ou une ligne au dessus d'un appel à verify()
- et imaginons enfin que ce fichier est structuré de manière à facilement
retrouver le commentaire "associé" à ce couple erreur (fichier - ligne)


usine à gaz, le mot est bien choisi...
Mis à part l'éventuelle possibilité d'avoir des descriptions d'erreurs
très détaillées avec du XML ou du HTML dans les commentaires pour sortir
qqe chose de nickel, je pense que l'on peut souvent se contenter d'un
message sur une ligne qui décrit le problème, message que l'on peut
directement faire sortir avec le stringstream...

eh ben dans un tel cas on pourrai obtenir en message d'erreur le commentaire
associé au verify() qui a échoué, et écrire des commentaires ferait partie
de la procédure de gestion des erreurs. Ces commentaires servent aussi à
documenter le projet...
Et on peut imaginer que le fichier résultat puisse être traduit par la suite
afin d'offrir des messages d'erreurs dans la langue que l'on veut.
Que pensez-vous de cette approche ?


Que le plus dur reste à faire : lister, écrire et faire exécuter tous
les test cases qui vont produire les messages d'erreurs.
Une bonne doc de messages d'erreurs devrait comporter une description du
cas d'utilisation qui a amené l'erreur.

Si on réussit à automatiser tout le reste, pourquoi ne pas automatiser
aussi la génération de ces descriptions ? (j'ai fait ça une fois sur une
appli mais la tache était relativement simplifiée par le fait qu'un test
case n'était rien de plus qu'un ensemble de paires {field,value})

Avatar
Gabriel Dos Reis
Olivier Azeau writes:

| drkm wrote:
| > Loïc Joly writes:
| >
| >>Aurélien REGAT-BARREL wrote:
| >
| >>>Moyennant une petite macro verify() qui permet de connaitre
| >>>le fichier & la ligne où on se situe :
| >>>#define verify(x) Verify( x, __FILE__, __LINE__ )
| >
| >>Je croyais que tu étais opposé à une solution à base de macro ?
| > S'il veut utiliser __LINE__ et __FILE__, il n'a pas beaucoup de
| > choix.
|
| Dans la série "implémentations délirantes pour retrouver l'endroit
| d'une erreur runtime", sur une appli Solaris, un collègue a implémenté
| une classe dérivée de std::exception avec dans le constructeur un
| appel à une commande système "pstack" qui permet de récupérer la stack
| d'appel courante de tous les threads du process.

Linux essaie de copier un comportement similaire avec le système de
fichiers virtuel /proc -- ce ne sont pas toutes les fonctionnalités de
Solaris qui y sont implémentées (j'en avais eu beosin il y a deux ans).

- -Gaby
Avatar
SerGioGio
Bonjour,

Je préférais de loin ton idée précédente.
verify(version != 1) << "incorrect file version: " << version;

1. Maintenant, du point de vue de l'implementation, on ne peut a priori pas
lancer d'exception dans un destructeur...
mais par contre on peut ecrire l'esprit tranquile:

preverif( verify(version != 1) << "incorrect file version: " << version );

ici, verify construit un objet avec la condition evaluee et le message
d'erreur.
preverif est une fonction qui si la condition est false throw une exception.

2. grace aux joies de la precedence des operateurs, on peut transformer
cette expression en:

preverif() | verify(version != 1) << "incorrect file version: " << version;

Cette fois, preverif() est un objet qui a une fonction membre operator|.
operator| a une precedence moins eleve que << donc c'est << qui est evalué
en premier.

3. enfin une petite macro:

#define verif(a) preverif() | verify(a)

et on peut écrire:
verify(version != 1) << "incorrect file version: " << version;

voici le source complet:

struct verify
{
bool m_b;
std::ostringstream m_stream;
verify(bool a_b) : m_b(a_b) {}
template<class T> verify& operator<< (T& a_t)
{
m_stream << a_t;
return *this;
}
};

struct preverif
{
void operator| (const verify& a_verif)
{
if (!a_verif.m_b) throw std::runtime_error(a_verif.m_stream.str());
}
};

#define verif(a) preverif() | verify(a)


SerGioGio




"Aurélien REGAT-BARREL" a écrit dans le
message de news:41eceddd$0$25762$
Bonjour à tous,
Si vous avez lu le thread "[TORDU] Exception dans un destructeur, oui
mais..." vous savez que je suis en train de réfléchir à une meilleur
gestion

des erreurs dans mon programme. Le critère numéro un est la simplicité
d'utilisation, pour encourager le programmeur à l"utiliser.
Le but est de simplifier au maximum un tel bloc de vérification:

int value = ...;
if ( value >= 10 )
{
std::ostringstream oss;
oss << "La valeur n'est pas un chiffre : " << value;
throw std::runtime_error( oss.str() );
}

j'étais parti dans un truc de ce genre:

verify( value < 10 ) << "La valeur n'est pas un chiffre : " << value;

Mais l'implémentation n'est pas top (exception lancée dans un
destructeur),

et finalement je trouve ça moyennement explicite.

Après réflexion, je me suis dit que ce qui était le plus important en fin
de

compte c'était de garder une trace des valeurs erronées. Le reste, à
partir

du moment où on a l'emplacement dans le fichier on retrouve tout.
Alors voici ma nouvelle idée:

// la valeur doit être un chiffre
verify( value ) < 10;

verify() construit et renvoie un Verifier et c'est l'opérateur == de ce
dernier qui fait le test et lève ou non l'exception.

template <typename T>
class Verifier
{
public:
Verifier( const T & p1 ) : p1_( p1 ) {}

void operator == ( const T & p2 )
{
if ( ! ( this->p1_ == p2 ) )
{
std::ostringstream oss;
oss << "Verification failed with "
<< this->p1_ << " == " << p2;
throw std::runtime_error( oss.str() );
}
}

private:
T p1_;
};

template<typename T>
Verifier<T> verify( const T & t )
{
return Verifier<T>( t );
}

Je sais que boost a un truc pour simplifier la déclaration des opérateurs.
Ne connaissant pas la bête, je me suis contenté d'une macro pour
simplifier

la tâche:

template <typename T>
class Verifier
{
public:
Verifier( const T & p1 ) : p1_( p1 ) {}

#define OPERATOR(op)
void operator op ( const T & p2 )
{
if ( ! ( this->p1_ op p2 ) )
{
std::ostringstream oss;
oss << "Verification failed with "
<< this->p1_ << " " #op " " << p2;
throw std::runtime_error( oss.str() );
}
}

OPERATOR( == )
OPERATOR( < )
OPERATOR( > )
OPERATOR( != )
OPERATOR( >= )
OPERATOR( <= )

#undef OPERATOR

private:
T p1_;
};

les opérateurs renvoient void, c'est pas innocent c'est pour empêcher de
les

utiliser dans un autre contexte, je sais pas trop ce que ça ferait.

Jusque là j'aimerais avoir votre avis.

Mais attention j'ai l'esprit un peu tordu et ça va plus loin. Allons-y
pour

l'usine à gaz. Moyennant une petite macro verify() qui permet de connaitre
le fichier & la ligne où on se situe :

#define verify(x) Verify( x, __FILE__, __LINE__ )

on peut imaginer que Verify initialiserait l'objet Verifier afin que ce
dernier localise l'emplacement de l'erreur. Comme assert() en fait. Sauf
que, j'ai parlé d'usine à gaz....

- Imaginons qu'en cas d'échec Verifier construise un code d'erreur qui
permette d'identifier le fichier et la ligne de l'échec.
- Imaginons qu'un stupide logiciel / script d'extraction de commentaires
génère un gros fichier qui contienne tous les commentaires situés sur la
même ligne ou une ligne au dessus d'un appel à verify()
- et imaginons enfin que ce fichier est structuré de manière à facilement
retrouver le commentaire "associé" à ce couple erreur (fichier - ligne)

eh ben dans un tel cas on pourrai obtenir en message d'erreur le
commentaire

associé au verify() qui a échoué, et écrire des commentaires ferait partie
de la procédure de gestion des erreurs. Ces commentaires servent aussi à
documenter le projet...
Et on peut imaginer que le fichier résultat puisse être traduit par la
suite

afin d'offrir des messages d'erreurs dans la langue que l'on veut.
Que pensez-vous de cette approche ?

--
Aurélien REGAT-BARREL




Avatar
SerGioGio
Il y a toujours plus simple, et moins problématique, en utilisant
stringstream.

struct verify
{
bool m_b;
verify(bool a_b) : m_b(a_b) { }
void operator| (const std::ostream& a_stream)
{
std::istreambuf_iterator<char> beg(a_stream.rdbuf());
std::istreambuf_iterator<char> end;
if (!m_b) throw std::runtime_error(std::string(beg, end));
}
};
#define verif(a) verify(a) | std::stringstream() << std::flush << __FILE__
<< "(" << __LINE__ << ") "

L'appel à flush est nécéssaire, car par exemple, aussi surprenant que ça
puisse paraitre,
std::stringstream() << "hello world" appelle la fonction membre operator<<
(void* ) au lieu de la fonction globale operator<<(std::ostream&, const
char*) comme on aurait pu s'y attendre.
D'après ce que j'ai compris, la raison est que on ne peut pas lier un object
temporaire à une référence non-const, or std::stringstream() est un
temporaire, std::ostream& est une référence non-const, donc la fonction
globale operator<<(std::ostream&, const char*) est disqualifiée...
Par contre, on peut toujours appeler une fonction membre d'un temporaire, et
cette fonction membre est autorisée à renvoyer une référence non-const sur
l'objet, d'où le flush...

Donc la macro est au minimum:
#define verif(a) verify(a) | std::stringstream() << std::flush

SerGioGio


"SerGioGio" a écrit dans le message de
news:41edec7a$
Bonjour,

Je préférais de loin ton idée précédente.
verify(version != 1) << "incorrect file version: " << version;

1. Maintenant, du point de vue de l'implementation, on ne peut a priori
pas

lancer d'exception dans un destructeur...
mais par contre on peut ecrire l'esprit tranquile:

preverif( verify(version != 1) << "incorrect file version: " << version );

ici, verify construit un objet avec la condition evaluee et le message
d'erreur.
preverif est une fonction qui si la condition est false throw une
exception.


2. grace aux joies de la precedence des operateurs, on peut transformer
cette expression en:

preverif() | verify(version != 1) << "incorrect file version: " <<
version;


Cette fois, preverif() est un objet qui a une fonction membre operator|.
operator| a une precedence moins eleve que << donc c'est << qui est evalué
en premier.

3. enfin une petite macro:

#define verif(a) preverif() | verify(a)

et on peut écrire:
verify(version != 1) << "incorrect file version: " << version;

voici le source complet:

struct verify
{
bool m_b;
std::ostringstream m_stream;
verify(bool a_b) : m_b(a_b) {}
template<class T> verify& operator<< (T& a_t)
{
m_stream << a_t;
return *this;
}
};

struct preverif
{
void operator| (const verify& a_verif)
{
if (!a_verif.m_b) throw
std::runtime_error(a_verif.m_stream.str());

}
};

#define verif(a) preverif() | verify(a)


SerGioGio




"Aurélien REGAT-BARREL" a écrit dans le
message de news:41eceddd$0$25762$
Bonjour à tous,
Si vous avez lu le thread "[TORDU] Exception dans un destructeur, oui
mais..." vous savez que je suis en train de réfléchir à une meilleur
gestion

des erreurs dans mon programme. Le critère numéro un est la simplicité
d'utilisation, pour encourager le programmeur à l"utiliser.
Le but est de simplifier au maximum un tel bloc de vérification:

int value = ...;
if ( value >= 10 )
{
std::ostringstream oss;
oss << "La valeur n'est pas un chiffre : " << value;
throw std::runtime_error( oss.str() );
}

j'étais parti dans un truc de ce genre:

verify( value < 10 ) << "La valeur n'est pas un chiffre : " << value;

Mais l'implémentation n'est pas top (exception lancée dans un
destructeur),

et finalement je trouve ça moyennement explicite.

Après réflexion, je me suis dit que ce qui était le plus important en
fin


de
compte c'était de garder une trace des valeurs erronées. Le reste, à
partir

du moment où on a l'emplacement dans le fichier on retrouve tout.
Alors voici ma nouvelle idée:

// la valeur doit être un chiffre
verify( value ) < 10;

verify() construit et renvoie un Verifier et c'est l'opérateur == de ce
dernier qui fait le test et lève ou non l'exception.

template <typename T>
class Verifier
{
public:
Verifier( const T & p1 ) : p1_( p1 ) {}

void operator == ( const T & p2 )
{
if ( ! ( this->p1_ == p2 ) )
{
std::ostringstream oss;
oss << "Verification failed with "
<< this->p1_ << " == " << p2;
throw std::runtime_error( oss.str() );
}
}

private:
T p1_;
};

template<typename T>
Verifier<T> verify( const T & t )
{
return Verifier<T>( t );
}

Je sais que boost a un truc pour simplifier la déclaration des
opérateurs.


Ne connaissant pas la bête, je me suis contenté d'une macro pour
simplifier

la tâche:

template <typename T>
class Verifier
{
public:
Verifier( const T & p1 ) : p1_( p1 ) {}

#define OPERATOR(op)
void operator op ( const T & p2 )
{
if ( ! ( this->p1_ op p2 ) )
{
std::ostringstream oss;
oss << "Verification failed with "
<< this->p1_ << " " #op " " << p2;
throw std::runtime_error( oss.str() );
}
}

OPERATOR( == )
OPERATOR( < )
OPERATOR( > )
OPERATOR( != )
OPERATOR( >= )
OPERATOR( <= )

#undef OPERATOR

private:
T p1_;
};

les opérateurs renvoient void, c'est pas innocent c'est pour empêcher de
les

utiliser dans un autre contexte, je sais pas trop ce que ça ferait.

Jusque là j'aimerais avoir votre avis.

Mais attention j'ai l'esprit un peu tordu et ça va plus loin. Allons-y
pour

l'usine à gaz. Moyennant une petite macro verify() qui permet de
connaitre


le fichier & la ligne où on se situe :

#define verify(x) Verify( x, __FILE__, __LINE__ )

on peut imaginer que Verify initialiserait l'objet Verifier afin que ce
dernier localise l'emplacement de l'erreur. Comme assert() en fait. Sauf
que, j'ai parlé d'usine à gaz....

- Imaginons qu'en cas d'échec Verifier construise un code d'erreur qui
permette d'identifier le fichier et la ligne de l'échec.
- Imaginons qu'un stupide logiciel / script d'extraction de commentaires
génère un gros fichier qui contienne tous les commentaires situés sur la
même ligne ou une ligne au dessus d'un appel à verify()
- et imaginons enfin que ce fichier est structuré de manière à
facilement


retrouver le commentaire "associé" à ce couple erreur (fichier - ligne)

eh ben dans un tel cas on pourrai obtenir en message d'erreur le
commentaire

associé au verify() qui a échoué, et écrire des commentaires ferait
partie


de la procédure de gestion des erreurs. Ces commentaires servent aussi à
documenter le projet...
Et on peut imaginer que le fichier résultat puisse être traduit par la
suite

afin d'offrir des messages d'erreurs dans la langue que l'on veut.
Que pensez-vous de cette approche ?

--
Aurélien REGAT-BARREL








Avatar
Aurélien REGAT-BARREL
Il y a toujours plus simple, et moins problématique, en utilisant
stringstream.

struct verify
{
bool m_b;
verify(bool a_b) : m_b(a_b) { }
void operator| (const std::ostream& a_stream)
{
std::istreambuf_iterator<char> beg(a_stream.rdbuf());
std::istreambuf_iterator<char> end;
if (!m_b) throw std::runtime_error(std::string(beg, end));
}
};
#define verif(a) verify(a) | std::stringstream() << std::flush << __FILE__
<< "(" << __LINE__ << ") "

L'appel à flush est nécéssaire, car par exemple, aussi surprenant que ça
puisse paraitre,
std::stringstream() << "hello world" appelle la fonction membre operator<<
(void* ) au lieu de la fonction globale operator<<(std::ostream&, const
char*) comme on aurait pu s'y attendre.
D'après ce que j'ai compris, la raison est que on ne peut pas lier un
object

temporaire à une référence non-const, or std::stringstream() est un
temporaire, std::ostream& est une référence non-const, donc la fonction
globale operator<<(std::ostream&, const char*) est disqualifiée...
Par contre, on peut toujours appeler une fonction membre d'un temporaire,
et

cette fonction membre est autorisée à renvoyer une référence non-const sur
l'objet, d'où le flush...

Donc la macro est au minimum:
#define verif(a) verify(a) | std::stringstream() << std::flush


A oui tu récupères l'idée de l'opérateur * dans ENFORCE. J'ai exploré cette
piste avec l'opérateur =:

verify( a != b ) = "erreur";

Mais ce qui m'a gêné c'est si le mec oublie de mettre l'opérateur le verify
ne fait rien :

verify( a != b );

J'avais pas pensé à parer le truc avec une macro.

Y'a juste quand même une nuance qui peut être gênante avec ta proposition,
c'est que le message stringstream est toujours construit même s'il n'y a pas
d'erreur, contrairement à ENFORCE ou ma précédente proposition. Dans le cas
d'un boucle de calcul un peu sensible...

Mais ton message m'a inspiré une autre solution, qui je crois est plutôt
propre... Je vais poster en début de thread.

--
Aurélien REGAT-BARREL

Avatar
Aurélien REGAT-BARREL
Ah, ah, ah... Je crois que je le tiens là. Suite à vos remarques, j'ai eu
l'idée de déplacer le problème.

Au départ:

verify( a != b ) << "erreur";

puis j'ai pensé à quelque chose comme :

verify( a != b ) = msg() << "erreur";

Et puis j'ai essayé de déplacer le test:

verify() = msg( a != b ) << "erreur";

msg() renvoie un "flux conditionnel" qui en fonction du test donné en
paramètre va s'initialiser ou non (pas de pénalité de performance à
construire un message d'erreur s'il n'y a pas d'erreur).
Le flux résultant sert à initialiser un objet Verifier renvoyé par verify().
L'opérateur = de ce dernier utilise la condition conservée dans le flux reçu
pour lever une exception ou non. On mélange le tout dans une macro, et voici
ce que ça donne:

class cond_ostream // conditionnal ostream
{
public:
// ctor: descriptif de la condition, sa valeur
// ex: cond_ostream( "10 == 10", true );
cond_ostream( const char * CondStr, bool CondResult )
{
this->do_throw_ = !CondResult;
if ( this->do_throw_ )
{
this->oss_ << "Echec du test '"
<< CondStr << "' : ";
}
}

template<typename T>
cond_ostream & operator << ( const T & t )
{
if ( this->do_throw_ )
{
this->oss_ << t;
}
return *this;
}

bool do_throw() const
{
return this->do_throw_;
}

std::string err_msg() const
{
return this->oss_.str();
}

private:
std::ostringstream oss_;
bool do_throw_;
};

struct Verifier
{
// throw runtime_error si le flux reçu le demande
void operator = ( const cond_ostream & o )
{
if ( o.do_throw() )
{
throw std::runtime_error( o.err_msg() );
}
}
};

#define verify( x ) Verifier() = cond_ostream( #x, x )

et ça s'utilise comme ça:

int main()
{
try
{
verify( 10 == 10 ); // sans message d'erreur, ça marche
verify( 10 == 9 ); // << "erreur";
}
catch ( const std::exception & e )
{
std::cerr << e.what() << 'n';
}
}

on pourrait envisager d'enrichir un peu la macro et le flux / le Verifier
pour tracer __FILE__ et __LINE__.

Qu'en pensez-vous ?

--
Aurélien REGAT-BARREL


--
Aurélien REGAT-BARREL
"Aurélien REGAT-BARREL" a écrit dans le
message de news:41eceddd$0$25762$
Bonjour à tous,
Si vous avez lu le thread "[TORDU] Exception dans un destructeur, oui
mais..." vous savez que je suis en train de réfléchir à une meilleur
gestion

des erreurs dans mon programme. Le critère numéro un est la simplicité
d'utilisation, pour encourager le programmeur à l"utiliser.
Le but est de simplifier au maximum un tel bloc de vérification:

int value = ...;
if ( value >= 10 )
{
std::ostringstream oss;
oss << "La valeur n'est pas un chiffre : " << value;
throw std::runtime_error( oss.str() );
}

j'étais parti dans un truc de ce genre:

verify( value < 10 ) << "La valeur n'est pas un chiffre : " << value;

Mais l'implémentation n'est pas top (exception lancée dans un
destructeur),

et finalement je trouve ça moyennement explicite.

Après réflexion, je me suis dit que ce qui était le plus important en fin
de

compte c'était de garder une trace des valeurs erronées. Le reste, à
partir

du moment où on a l'emplacement dans le fichier on retrouve tout.
Alors voici ma nouvelle idée:

// la valeur doit être un chiffre
verify( value ) < 10;

verify() construit et renvoie un Verifier et c'est l'opérateur == de ce
dernier qui fait le test et lève ou non l'exception.

template <typename T>
class Verifier
{
public:
Verifier( const T & p1 ) : p1_( p1 ) {}

void operator == ( const T & p2 )
{
if ( ! ( this->p1_ == p2 ) )
{
std::ostringstream oss;
oss << "Verification failed with "
<< this->p1_ << " == " << p2;
throw std::runtime_error( oss.str() );
}
}

private:
T p1_;
};

template<typename T>
Verifier<T> verify( const T & t )
{
return Verifier<T>( t );
}

Je sais que boost a un truc pour simplifier la déclaration des opérateurs.
Ne connaissant pas la bête, je me suis contenté d'une macro pour
simplifier

la tâche:

template <typename T>
class Verifier
{
public:
Verifier( const T & p1 ) : p1_( p1 ) {}

#define OPERATOR(op)
void operator op ( const T & p2 )
{
if ( ! ( this->p1_ op p2 ) )
{
std::ostringstream oss;
oss << "Verification failed with "
<< this->p1_ << " " #op " " << p2;
throw std::runtime_error( oss.str() );
}
}

OPERATOR( == )
OPERATOR( < )
OPERATOR( > )
OPERATOR( != )
OPERATOR( >= )
OPERATOR( <= )

#undef OPERATOR

private:
T p1_;
};

les opérateurs renvoient void, c'est pas innocent c'est pour empêcher de
les

utiliser dans un autre contexte, je sais pas trop ce que ça ferait.

Jusque là j'aimerais avoir votre avis.

Mais attention j'ai l'esprit un peu tordu et ça va plus loin. Allons-y
pour

l'usine à gaz. Moyennant une petite macro verify() qui permet de connaitre
le fichier & la ligne où on se situe :

#define verify(x) Verify( x, __FILE__, __LINE__ )

on peut imaginer que Verify initialiserait l'objet Verifier afin que ce
dernier localise l'emplacement de l'erreur. Comme assert() en fait. Sauf
que, j'ai parlé d'usine à gaz....

- Imaginons qu'en cas d'échec Verifier construise un code d'erreur qui
permette d'identifier le fichier et la ligne de l'échec.
- Imaginons qu'un stupide logiciel / script d'extraction de commentaires
génère un gros fichier qui contienne tous les commentaires situés sur la
même ligne ou une ligne au dessus d'un appel à verify()
- et imaginons enfin que ce fichier est structuré de manière à facilement
retrouver le commentaire "associé" à ce couple erreur (fichier - ligne)

eh ben dans un tel cas on pourrai obtenir en message d'erreur le
commentaire

associé au verify() qui a échoué, et écrire des commentaires ferait partie
de la procédure de gestion des erreurs. Ces commentaires servent aussi à
documenter le projet...
Et on peut imaginer que le fichier résultat puisse être traduit par la
suite

afin d'offrir des messages d'erreurs dans la langue que l'on veut.
Que pensez-vous de cette approche ?

--
Aurélien REGAT-BARREL




Avatar
Matthieu Moy
"Aurélien REGAT-BARREL" writes:

Cette stack était enfin stockée dans une std::string de la classe
dérivée de std::exception.
Et une méthode "const char *where()" venait donc compléter "const char
*what()" pour renvoyer le lieu de l'exception...


Et ça sert à quoi de connaitre l'adresse dans la pile ?


A les donner a manger à un deboggueur, ou a afficher les numéros de
lignes si on a l'API qu'il faut (ca existe en C++ ?) et que le binaire
est compilé en debug.

--
Matthieu


1 2