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 ) {}
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() );\
}\
}
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 ?
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.
En fait, le couple (numéro de ligne,nom de fichier) n'est pas une information tres intéressante pour une erreur run-time : si on a un code pas trop mal écrit avec des fonctions qui ne font pas plus d'une page écran et qui ne lancent pas des exceptions a toutes les lignes, le nom de la fonction qui lance l'exception suffit en général a retrouver le fichier et la ligne.
Ce que la pile donne en plus et que le compilo ne pourra jamais donner, c'est le chemin d'execution : l'ensemble des appels en cours dans la pile au moment de la levée d'exception. C'est particulierement utile quand la fonction qui leve l'exception est une fonction de base utilisée a plusieurs endroits dans le code : on peut retrouver plus facilement la scenario qui est a l'origine du probleme.
Le probleme des erreurs run-time c'est que tant que tu réussis a les reproduire chez toi, tu n'as pas trop de probleme pour faire le diagnostic avec ton debugger. Mais si elle se produit dans l'environnement d'un utilisateur et que tu n'arrives pas a la reproduire dans le tien (ce qui arrive un peu trop souvent a mon gout), il est vraiment tres utile d'avoir une option de config 'activation de logs détaillés d'erreurs run-time' qui se traduit dans le cas présent par un test dans le constructeur de l'exception pour savoir si on a besoin de récupérer la pile au moment de l'erreur.
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.
En fait, le couple (numéro de ligne,nom de fichier) n'est pas une
information tres intéressante pour une erreur run-time : si on a un
code pas trop mal écrit avec des fonctions qui ne font pas plus d'une
page écran et qui ne lancent pas des exceptions a toutes les lignes,
le nom de la fonction qui lance l'exception suffit en général a
retrouver le fichier et la ligne.
Ce que la pile donne en plus et que le compilo ne pourra jamais donner,
c'est le chemin d'execution : l'ensemble des appels en cours dans la
pile au moment de la levée d'exception.
C'est particulierement utile quand la fonction qui leve l'exception est
une fonction de base utilisée a plusieurs endroits dans le code : on
peut retrouver plus facilement la scenario qui est a l'origine du
probleme.
Le probleme des erreurs run-time c'est que tant que tu réussis a les
reproduire chez toi, tu n'as pas trop de probleme pour faire le
diagnostic avec ton debugger. Mais si elle se produit dans
l'environnement d'un utilisateur et que tu n'arrives pas a la
reproduire dans le tien (ce qui arrive un peu trop souvent a mon gout),
il est vraiment tres utile d'avoir une option de config 'activation de
logs détaillés d'erreurs run-time' qui se traduit dans le cas
présent par un test dans le constructeur de l'exception pour savoir si
on a besoin de récupérer la pile au moment de l'erreur.
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.
En fait, le couple (numéro de ligne,nom de fichier) n'est pas une information tres intéressante pour une erreur run-time : si on a un code pas trop mal écrit avec des fonctions qui ne font pas plus d'une page écran et qui ne lancent pas des exceptions a toutes les lignes, le nom de la fonction qui lance l'exception suffit en général a retrouver le fichier et la ligne.
Ce que la pile donne en plus et que le compilo ne pourra jamais donner, c'est le chemin d'execution : l'ensemble des appels en cours dans la pile au moment de la levée d'exception. C'est particulierement utile quand la fonction qui leve l'exception est une fonction de base utilisée a plusieurs endroits dans le code : on peut retrouver plus facilement la scenario qui est a l'origine du probleme.
Le probleme des erreurs run-time c'est que tant que tu réussis a les reproduire chez toi, tu n'as pas trop de probleme pour faire le diagnostic avec ton debugger. Mais si elle se produit dans l'environnement d'un utilisateur et que tu n'arrives pas a la reproduire dans le tien (ce qui arrive un peu trop souvent a mon gout), il est vraiment tres utile d'avoir une option de config 'activation de logs détaillés d'erreurs run-time' qui se traduit dans le cas présent par un test dans le constructeur de l'exception pour savoir si on a besoin de récupérer la pile au moment de l'erreur.
Aurélien REGAT-BARREL
Ce que la pile donne en plus et que le compilo ne pourra jamais donner, c'est le chemin d'execution : l'ensemble des appels en cours dans la pile au moment de la levée d'exception. C'est particulierement utile quand la fonction qui leve l'exception est une fonction de base utilisée a plusieurs endroits dans le code : on peut retrouver plus facilement la scenario qui est a l'origine du probleme.
Mais l'inlining & l'optimisation ne vient-elle pas perturber tout ça ? Je comprend l'utilité de tracer les appels, mais je pige pas en quoi l'adresse du lieu de l'exception est utile. Il faut arriver à dépiler plusieurs adresses pour obtenir le chemin d'exécution, à la limite l'adresse de l'exception est inutile. Pour localiser le lieu d'une exception, je suis en train de tester l'emploi d'une macro THROW(). Je trouve une macro pas trop mal dans ce cas car ça permet de mieux localiser à l'oeil les throw. J'ai dû passer par une macro pour pas me prendre des warning du compilo:
std::string toto( int i ) { switch ( i ) { case 0 : return "zéro"; case 1 : return "un"; case 2 : return "deux"; } Logger::Throw( std::logic_error( "valeur de i incorrecte" ) ); } "warning : possibilité de valeur non retournée"
C'est dans cette optique que je réfléchi depuis un certaisn temps à une classe de tracing des passages dans une fonction (actuellement je vais des TRACE aux endroits critiques). Ce serait un simple objet qui dans son constructeur loguerait le message "entrée dans la fonction XXX" et dans son destructeur le message "sortie de fonction XXX". Un petit coup d'usine à gaz et l'objet peut avoir le principe des scopeguards et considérer que la fonction a été quittée avec une erreur si une méthode leaveOk() n'est pas appelée. On pourrait alors lui demander de dumper uniquement dans ce cas ainsi que la valeur de certaines variables :
// affiche la valeur de a si sortie incorrecte trace.Remind( a );
while ( a > 0 ) { doSomething( a, b ); // peut lever une exception --a; }
tracer.leaveOk(); }
si leaveOk() n'a pas été appelé quand on est dans le destructeur de CallTracer, hop il dump la valeur des variables passées à Remind() (passage par référence => dump sa valeur réelle au moment de la sortie).
Le probleme des erreurs run-time c'est que tant que tu réussis a les reproduire chez toi, tu n'as pas trop de probleme pour faire le diagnostic avec ton debugger. Mais si elle se produit dans l'environnement d'un utilisateur et que tu n'arrives pas a la reproduire dans le tien (ce qui arrive un peu trop souvent a mon gout),
Oui. Je suis encore novice, mais je sens bien venir le coup. Car ça fait un certains temps que je développe une appli et on va finir par la vendre, et là je vois bien un mec venir "ça marche pas". Donc j'ai bricolé un fichier de log, mais c'est contraignant et c'est pour ça que je bosse en ce moment à simplifier son utilisation / la gestion des erreurs.
il est vraiment tres utile d'avoir une option de config 'activation de logs détaillés d'erreurs run-time' qui se traduit dans le cas présent par un test dans le constructeur de l'exception pour savoir si on a besoin de récupérer la pile au moment de l'erreur.
Et un core dump ça fait pas l'affaire ?
-- Aurélien REGAT-BARREL
Ce que la pile donne en plus et que le compilo ne pourra jamais donner,
c'est le chemin d'execution : l'ensemble des appels en cours dans la
pile au moment de la levée d'exception.
C'est particulierement utile quand la fonction qui leve l'exception est
une fonction de base utilisée a plusieurs endroits dans le code : on
peut retrouver plus facilement la scenario qui est a l'origine du
probleme.
Mais l'inlining & l'optimisation ne vient-elle pas perturber tout ça ?
Je comprend l'utilité de tracer les appels, mais je pige pas en quoi
l'adresse du lieu de l'exception est utile. Il faut arriver à dépiler
plusieurs adresses pour obtenir le chemin d'exécution, à la limite l'adresse
de l'exception est inutile.
Pour localiser le lieu d'une exception, je suis en train de tester l'emploi
d'une macro THROW(). Je trouve une macro pas trop mal dans ce cas car ça
permet de mieux localiser à l'oeil les throw. J'ai dû passer par une macro
pour pas me prendre des warning du compilo:
std::string toto( int i )
{
switch ( i )
{
case 0 : return "zéro";
case 1 : return "un";
case 2 : return "deux";
}
Logger::Throw( std::logic_error( "valeur de i incorrecte" ) );
}
"warning : possibilité de valeur non retournée"
C'est dans cette optique que je réfléchi depuis un certaisn temps à une
classe de tracing des passages dans une fonction (actuellement je vais des
TRACE aux endroits critiques). Ce serait un simple objet qui dans son
constructeur loguerait le message "entrée dans la fonction XXX" et dans son
destructeur le message "sortie de fonction XXX".
Un petit coup d'usine à gaz et l'objet peut avoir le principe des
scopeguards et considérer que la fonction a été quittée avec une erreur si
une méthode leaveOk() n'est pas appelée. On pourrait alors lui demander de
dumper uniquement dans ce cas ainsi que la valeur de certaines variables :
// affiche la valeur de a si sortie incorrecte
trace.Remind( a );
while ( a > 0 )
{
doSomething( a, b ); // peut lever une exception
--a;
}
tracer.leaveOk();
}
si leaveOk() n'a pas été appelé quand on est dans le destructeur de
CallTracer, hop il dump la valeur des variables passées à Remind() (passage
par référence => dump sa valeur réelle au moment de la sortie).
Le probleme des erreurs run-time c'est que tant que tu réussis a les
reproduire chez toi, tu n'as pas trop de probleme pour faire le
diagnostic avec ton debugger. Mais si elle se produit dans
l'environnement d'un utilisateur et que tu n'arrives pas a la
reproduire dans le tien (ce qui arrive un peu trop souvent a mon gout),
Oui. Je suis encore novice, mais je sens bien venir le coup. Car ça fait un
certains temps que je développe une appli et on va finir par la vendre, et
là je vois bien un mec venir "ça marche pas". Donc j'ai bricolé un fichier
de log, mais c'est contraignant et c'est pour ça que je bosse en ce moment à
simplifier son utilisation / la gestion des erreurs.
il est vraiment tres utile d'avoir une option de config 'activation de
logs détaillés d'erreurs run-time' qui se traduit dans le cas
présent par un test dans le constructeur de l'exception pour savoir si
on a besoin de récupérer la pile au moment de l'erreur.
Ce que la pile donne en plus et que le compilo ne pourra jamais donner, c'est le chemin d'execution : l'ensemble des appels en cours dans la pile au moment de la levée d'exception. C'est particulierement utile quand la fonction qui leve l'exception est une fonction de base utilisée a plusieurs endroits dans le code : on peut retrouver plus facilement la scenario qui est a l'origine du probleme.
Mais l'inlining & l'optimisation ne vient-elle pas perturber tout ça ? Je comprend l'utilité de tracer les appels, mais je pige pas en quoi l'adresse du lieu de l'exception est utile. Il faut arriver à dépiler plusieurs adresses pour obtenir le chemin d'exécution, à la limite l'adresse de l'exception est inutile. Pour localiser le lieu d'une exception, je suis en train de tester l'emploi d'une macro THROW(). Je trouve une macro pas trop mal dans ce cas car ça permet de mieux localiser à l'oeil les throw. J'ai dû passer par une macro pour pas me prendre des warning du compilo:
std::string toto( int i ) { switch ( i ) { case 0 : return "zéro"; case 1 : return "un"; case 2 : return "deux"; } Logger::Throw( std::logic_error( "valeur de i incorrecte" ) ); } "warning : possibilité de valeur non retournée"
C'est dans cette optique que je réfléchi depuis un certaisn temps à une classe de tracing des passages dans une fonction (actuellement je vais des TRACE aux endroits critiques). Ce serait un simple objet qui dans son constructeur loguerait le message "entrée dans la fonction XXX" et dans son destructeur le message "sortie de fonction XXX". Un petit coup d'usine à gaz et l'objet peut avoir le principe des scopeguards et considérer que la fonction a été quittée avec une erreur si une méthode leaveOk() n'est pas appelée. On pourrait alors lui demander de dumper uniquement dans ce cas ainsi que la valeur de certaines variables :
// affiche la valeur de a si sortie incorrecte trace.Remind( a );
while ( a > 0 ) { doSomething( a, b ); // peut lever une exception --a; }
tracer.leaveOk(); }
si leaveOk() n'a pas été appelé quand on est dans le destructeur de CallTracer, hop il dump la valeur des variables passées à Remind() (passage par référence => dump sa valeur réelle au moment de la sortie).
Le probleme des erreurs run-time c'est que tant que tu réussis a les reproduire chez toi, tu n'as pas trop de probleme pour faire le diagnostic avec ton debugger. Mais si elle se produit dans l'environnement d'un utilisateur et que tu n'arrives pas a la reproduire dans le tien (ce qui arrive un peu trop souvent a mon gout),
Oui. Je suis encore novice, mais je sens bien venir le coup. Car ça fait un certains temps que je développe une appli et on va finir par la vendre, et là je vois bien un mec venir "ça marche pas". Donc j'ai bricolé un fichier de log, mais c'est contraignant et c'est pour ça que je bosse en ce moment à simplifier son utilisation / la gestion des erreurs.
il est vraiment tres utile d'avoir une option de config 'activation de logs détaillés d'erreurs run-time' qui se traduit dans le cas présent par un test dans le constructeur de l'exception pour savoir si on a besoin de récupérer la pile au moment de l'erreur.
Et un core dump ça fait pas l'affaire ?
-- Aurélien REGAT-BARREL
Olivier Azeau
Aurélien REGAT-BARREL wrote:
C'est dans cette optique que je réfléchi depuis un certaisn temps à une classe de tracing des passages dans une fonction (actuellement je vais des TRACE aux endroits critiques). Ce serait un simple objet qui dans son constructeur loguerait le message "entrée dans la fonction XXX" et dans son destructeur le message "sortie de fonction XXX". Un petit coup d'usine à gaz et l'objet peut avoir le principe des scopeguards et considérer que la fonction a été quittée avec une erreur si une méthode leaveOk() n'est pas appelée. On pourrait alors lui demander de dumper uniquement dans ce cas ainsi que la valeur de certaines variables :
// affiche la valeur de a si sortie incorrecte trace.Remind( a );
while ( a > 0 ) { doSomething( a, b ); // peut lever une exception --a; }
tracer.leaveOk(); }
si leaveOk() n'a pas été appelé quand on est dans le destructeur de CallTracer, hop il dump la valeur des variables passées à Remind() (passage par référence => dump sa valeur réelle au moment de la sortie).
Oui, ça permet de tracer l'exécution en restant dans le langage C++ et sans utiliser d'outil de l'OS mais ça demande quand même une bonne dose de rigueur de la part des développeur...
Je me demande si ce genre de truc existe avec un outil externe qui ferait ça de façon systématique. Par exemple un outil qui s'intercalerait entre le préprocesseur et le compilateur pour instrumenter le code : - un CallTracer au début de chaque fonction avec un Remind pour chaque paramètre de la fonction. - un leaveOk avant chaque return.
il est vraiment tres utile d'avoir une option de config 'activation de logs détaillés d'erreurs run-time' qui se traduit dans le cas présent par un test dans le constructeur de l'exception pour savoir si on a besoin de récupérer la pile au moment de l'erreur.
Et un core dump ça fait pas l'affaire ?
Oui, si tu peux te le permettre (typiquement impossible sur une appli serveur par exemple)
Aurélien REGAT-BARREL wrote:
C'est dans cette optique que je réfléchi depuis un certaisn temps à une
classe de tracing des passages dans une fonction (actuellement je vais des
TRACE aux endroits critiques). Ce serait un simple objet qui dans son
constructeur loguerait le message "entrée dans la fonction XXX" et dans son
destructeur le message "sortie de fonction XXX".
Un petit coup d'usine à gaz et l'objet peut avoir le principe des
scopeguards et considérer que la fonction a été quittée avec une erreur si
une méthode leaveOk() n'est pas appelée. On pourrait alors lui demander de
dumper uniquement dans ce cas ainsi que la valeur de certaines variables :
// affiche la valeur de a si sortie incorrecte
trace.Remind( a );
while ( a > 0 )
{
doSomething( a, b ); // peut lever une exception
--a;
}
tracer.leaveOk();
}
si leaveOk() n'a pas été appelé quand on est dans le destructeur de
CallTracer, hop il dump la valeur des variables passées à Remind() (passage
par référence => dump sa valeur réelle au moment de la sortie).
Oui, ça permet de tracer l'exécution en restant dans le langage C++ et
sans utiliser d'outil de l'OS mais ça demande quand même une bonne dose
de rigueur de la part des développeur...
Je me demande si ce genre de truc existe avec un outil externe qui
ferait ça de façon systématique.
Par exemple un outil qui s'intercalerait entre le préprocesseur et le
compilateur pour instrumenter le code :
- un CallTracer au début de chaque fonction avec un Remind pour chaque
paramètre de la fonction.
- un leaveOk avant chaque return.
il est vraiment tres utile d'avoir une option de config 'activation de
logs détaillés d'erreurs run-time' qui se traduit dans le cas
présent par un test dans le constructeur de l'exception pour savoir si
on a besoin de récupérer la pile au moment de l'erreur.
Et un core dump ça fait pas l'affaire ?
Oui, si tu peux te le permettre (typiquement impossible sur une appli
serveur par exemple)
C'est dans cette optique que je réfléchi depuis un certaisn temps à une classe de tracing des passages dans une fonction (actuellement je vais des TRACE aux endroits critiques). Ce serait un simple objet qui dans son constructeur loguerait le message "entrée dans la fonction XXX" et dans son destructeur le message "sortie de fonction XXX". Un petit coup d'usine à gaz et l'objet peut avoir le principe des scopeguards et considérer que la fonction a été quittée avec une erreur si une méthode leaveOk() n'est pas appelée. On pourrait alors lui demander de dumper uniquement dans ce cas ainsi que la valeur de certaines variables :
// affiche la valeur de a si sortie incorrecte trace.Remind( a );
while ( a > 0 ) { doSomething( a, b ); // peut lever une exception --a; }
tracer.leaveOk(); }
si leaveOk() n'a pas été appelé quand on est dans le destructeur de CallTracer, hop il dump la valeur des variables passées à Remind() (passage par référence => dump sa valeur réelle au moment de la sortie).
Oui, ça permet de tracer l'exécution en restant dans le langage C++ et sans utiliser d'outil de l'OS mais ça demande quand même une bonne dose de rigueur de la part des développeur...
Je me demande si ce genre de truc existe avec un outil externe qui ferait ça de façon systématique. Par exemple un outil qui s'intercalerait entre le préprocesseur et le compilateur pour instrumenter le code : - un CallTracer au début de chaque fonction avec un Remind pour chaque paramètre de la fonction. - un leaveOk avant chaque return.
il est vraiment tres utile d'avoir une option de config 'activation de logs détaillés d'erreurs run-time' qui se traduit dans le cas présent par un test dans le constructeur de l'exception pour savoir si on a besoin de récupérer la pile au moment de l'erreur.
Et un core dump ça fait pas l'affaire ?
Oui, si tu peux te le permettre (typiquement impossible sur une appli serveur par exemple)
Aurélien REGAT-BARREL
Oui, ça permet de tracer l'exécution en restant dans le langage C++ et sans utiliser d'outil de l'OS mais ça demande quand même une bonne dose de rigueur de la part des développeur...
Je viens de tester le truc, et je suis en train de l'adopter partout dans mon code tellement c'est bien. Vu que je suis en pleine modification de mon système de log ça tombe pile poile. Ca permet de structurer le fichier log de manière arborescente en fonction de la profondeur des appels. En effet le nombre d'instances de CallTracer est compté ainsi la fonction log() qui renvoie un flux sur lequel logguer des infos insère auparavant autant de tabulations qu'il y a d'instances de CallTrace. C'est du plus bel effet dans le fichier de log final. Je me prend à rêver de html et de couleur en fonction du type de log (trace, erreur, ...).
Je me demande si ce genre de truc existe avec un outil externe qui ferait ça de façon systématique. Par exemple un outil qui s'intercalerait entre le préprocesseur et le compilateur pour instrumenter le code : - un CallTracer au début de chaque fonction avec un Remind pour chaque paramètre de la fonction. - un leaveOk avant chaque return.
Le problème avec un tel outil c'est que le moindre getter va être loggé, et logger un "return this->value_;" ne sert pas à grand chose à part plomber les performances. De plus le but n'est pas de faire leaveOk() à chaque return:
bool ReadCfg() { CALL_TRACER( tracer );
if ( ... ) return false; // pas de leaveOk() //... tracer.leaveOk(); return true; }
Mais seulement là où ça s'est bien passé. Le leaveOk() est contraignant je suis d'accord. Pour l'instant (depuis 24H :-) je m'en passe et utilise une toute petite macro pour logger l'E/S d'une fonction:
Donc un log_scope() en début des fonctions voire des blocs qui m'intéresse et c'est réglé. Pour l'instant ça me suffit. Ca dépend de ce que tu recherches je suppose. Si tu veux un mécanisme qui trace n'importe quel appel / passage d'argument, il faut s'orienter vers une solution runtime AMHA (genre ce que tu as dit au début, ou génération de mini dumps sous Windows).
-- Aurélien REGAT-BARREL
Oui, ça permet de tracer l'exécution en restant dans le langage C++ et
sans utiliser d'outil de l'OS mais ça demande quand même une bonne dose
de rigueur de la part des développeur...
Je viens de tester le truc, et je suis en train de l'adopter partout dans
mon code tellement c'est bien. Vu que je suis en pleine modification de mon
système de log ça tombe pile poile. Ca permet de structurer le fichier log
de manière arborescente en fonction de la profondeur des appels. En effet le
nombre d'instances de CallTracer est compté ainsi la fonction log() qui
renvoie un flux sur lequel logguer des infos insère auparavant autant de
tabulations qu'il y a d'instances de CallTrace. C'est du plus bel effet dans
le fichier de log final. Je me prend à rêver de html et de couleur en
fonction du type de log (trace, erreur, ...).
Je me demande si ce genre de truc existe avec un outil externe qui
ferait ça de façon systématique.
Par exemple un outil qui s'intercalerait entre le préprocesseur et le
compilateur pour instrumenter le code :
- un CallTracer au début de chaque fonction avec un Remind pour chaque
paramètre de la fonction.
- un leaveOk avant chaque return.
Le problème avec un tel outil c'est que le moindre getter va être loggé, et
logger un "return this->value_;" ne sert pas à grand chose à part plomber
les performances.
De plus le but n'est pas de faire leaveOk() à chaque return:
bool ReadCfg()
{
CALL_TRACER( tracer );
if ( ... ) return false; // pas de leaveOk()
//...
tracer.leaveOk();
return true;
}
Mais seulement là où ça s'est bien passé.
Le leaveOk() est contraignant je suis d'accord. Pour l'instant (depuis 24H
:-) je m'en passe et utilise une toute petite macro pour logger l'E/S d'une
fonction:
Donc un log_scope() en début des fonctions voire des blocs qui m'intéresse
et c'est réglé. Pour l'instant ça me suffit.
Ca dépend de ce que tu recherches je suppose. Si tu veux un mécanisme qui
trace n'importe quel appel / passage d'argument, il faut s'orienter vers une
solution runtime AMHA (genre ce que tu as dit au début, ou génération de
mini dumps sous Windows).
Oui, ça permet de tracer l'exécution en restant dans le langage C++ et sans utiliser d'outil de l'OS mais ça demande quand même une bonne dose de rigueur de la part des développeur...
Je viens de tester le truc, et je suis en train de l'adopter partout dans mon code tellement c'est bien. Vu que je suis en pleine modification de mon système de log ça tombe pile poile. Ca permet de structurer le fichier log de manière arborescente en fonction de la profondeur des appels. En effet le nombre d'instances de CallTracer est compté ainsi la fonction log() qui renvoie un flux sur lequel logguer des infos insère auparavant autant de tabulations qu'il y a d'instances de CallTrace. C'est du plus bel effet dans le fichier de log final. Je me prend à rêver de html et de couleur en fonction du type de log (trace, erreur, ...).
Je me demande si ce genre de truc existe avec un outil externe qui ferait ça de façon systématique. Par exemple un outil qui s'intercalerait entre le préprocesseur et le compilateur pour instrumenter le code : - un CallTracer au début de chaque fonction avec un Remind pour chaque paramètre de la fonction. - un leaveOk avant chaque return.
Le problème avec un tel outil c'est que le moindre getter va être loggé, et logger un "return this->value_;" ne sert pas à grand chose à part plomber les performances. De plus le but n'est pas de faire leaveOk() à chaque return:
bool ReadCfg() { CALL_TRACER( tracer );
if ( ... ) return false; // pas de leaveOk() //... tracer.leaveOk(); return true; }
Mais seulement là où ça s'est bien passé. Le leaveOk() est contraignant je suis d'accord. Pour l'instant (depuis 24H :-) je m'en passe et utilise une toute petite macro pour logger l'E/S d'une fonction:
Donc un log_scope() en début des fonctions voire des blocs qui m'intéresse et c'est réglé. Pour l'instant ça me suffit. Ca dépend de ce que tu recherches je suppose. Si tu veux un mécanisme qui trace n'importe quel appel / passage d'argument, il faut s'orienter vers une solution runtime AMHA (genre ce que tu as dit au début, ou génération de mini dumps sous Windows).
-- Aurélien REGAT-BARREL
Aurélien REGAT-BARREL
Bon je conclus en recopiant la solution sans exception (mais avec macro) issue de mon autre thread "Classe pour la verification de conditions". Merci de m'avoir aidé.
La syntaxe du verify() a été modifiée afin de ne plus renvoyer un objet qui lève une exception dans le destructeur mais lève l'exception au moyen de l'opérateur = qui reçoit en paramètre un "flux d'erreur conditionnel".
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; }
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() ); } } };
Une macro simplifie la syntaxe et permet éventuellement de conserver __LINE__ etc...
#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'; } }
-- Aurélien REGAT-BARREL
Bon je conclus en recopiant la solution sans exception (mais avec macro)
issue de mon autre thread "Classe pour la verification de conditions".
Merci de m'avoir aidé.
La syntaxe du verify() a été modifiée afin de ne plus renvoyer un objet qui
lève une exception dans le destructeur mais lève l'exception au moyen de
l'opérateur = qui reçoit en paramètre un "flux d'erreur conditionnel".
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;
}
Bon je conclus en recopiant la solution sans exception (mais avec macro) issue de mon autre thread "Classe pour la verification de conditions". Merci de m'avoir aidé.
La syntaxe du verify() a été modifiée afin de ne plus renvoyer un objet qui lève une exception dans le destructeur mais lève l'exception au moyen de l'opérateur = qui reçoit en paramètre un "flux d'erreur conditionnel".
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; }