OVH Cloud OVH Cloud

[TORDU] Exception dans un destructeur, oui mais...

32 réponses
Avatar
Aurélien REGAT-BARREL
Bonjour à tous,
tout d'abord bonne année.
Je souhaite améliorer la gestion des erreurs et le système de logs de mon
programme. J'ai ragardé un peu log4cpp, pour l'instant c'est trop usine à
gaz pour moi et ça me satisfait pas trop. Mais la syntaxe utilisée m'a
inspiré un nouveau style d'assertion. Le but est d'écrire ceci:

void DoSomething( int A ) // A doit être un chiffre
{
verify( A < 10 ) << "A n'est pas un chiffre : " << A;
// ...
}

Si l'assertion n'est pas vérifiée, alors le message qui suit sert à
construire un message d'erreur (loggé, affiché, ...) et une exception
VerificattionFailed est levée. Sinon tout continue.
Pour cela je me suis dit que verify() pouvait renvoyer une espèce de flux
qui dans son destructeur déclenche une exception (s'il le faut). Voici mon
programme de test qui fonctionne sous VC++ 7.1 et Devcpp (gcc version
3.3.1).

#include <iostream>
#include <sstream>
#include <string>

class VerificationFailed
{
public:
VerificationFailed( bool Throw ) : // si true lève l'exception dans le
destructeur
throw_( Throw ),
msg_( "VerificationFailed : " )
{}

~VerificationFailed()
{
if ( this->throw_ )
{
throw this->msg_;
}
}

template<typename T>
VerificationFailed & operator << ( const T & t )
{
if ( this->throw_ )
{
std::ostringstream oss;
oss << t;
this->msg_ += oss.str();
}
return *this;
}

private:
bool throw_;
std::string msg_;
};

VerificationFailed verify( bool b )
{
return VerificationFailed( !b );
}

int main()
{
try
{
int A = 9;
std::cout << "debut du programme\n";
verify( A < 10 ) << "A n'est pas un chiffre : " << A;
std::cout << "suite du programme\n";
++A;
verify( A < 10 ) << "A n'est pas un chiffre : " << A;
std::cout << "fin du programme\n";
}
catch ( const std::string & err )
{
std::cerr << err << '\n';
}
std::cin.ignore();
}

J'obtiens le résultat suivant:

debut du programme
suite du programme
VerificationFailed : A n'est pas un chiffre : 10

Cela semble donc marcher, bien que lever une exception dans un destructeur
soit une mauvaise pratique (c'est plutôt sur la règle de la durée de vie
d'un objet renvoyé par une fonction que je m'interroge). Serait-ce
l'exception qui confirme la règle ?
Merci à vous.

--
Aurélien REGAT-BARREL

10 réponses

1 2 3 4
Avatar
drkm
"Aurélien REGAT-BARREL" writes:

Pour cela je me suis dit que verify() pouvait renvoyer une espèce de flux
qui dans son destructeur déclenche une exception (s'il le faut).


A priori, je dirais que si l'un des opérateurs << lance une
exception, le destructeur du flux sera appelé, et là, BOOM ! ÀMHA,
lancer une exception dans un destructeur est extrêmement délicat.

Il faut pouvoir assurer qu'aucune exception ne sera lancée entre la
création et la destruction de chacune des instances. Ce qui doit
pourvoir néanmoins être fait dans certains cas particuliers. Dans ce
cas-ci, il faudrait imposer que les surcharges de l'opérateur << pour
ce flux ne lancent pas d'exceptions.

--drkm

Avatar
Olivier Azeau
Aurélien REGAT-BARREL wrote:
void DoSomething( int A ) // A doit être un chiffre
{
verify( A < 10 ) << "A n'est pas un chiffre : " << A;
// ...
}

Si l'assertion n'est pas vérifiée, alors le message qui suit sert
à

construire un message d'erreur (loggé, affiché, ...) et une
exception

VerificattionFailed est levée. Sinon tout continue.


Pourquoi lever une exception au lieu de tout arreter si c'est une
assertion ?

Pour cela je me suis dit que verify() pouvait renvoyer une espèce de
flux

qui dans son destructeur déclenche une exception (s'il le faut).
Voici mon

programme de test qui fonctionne sous VC++ 7.1 et Devcpp (gcc version
3.3.1).

#include <iostream>
#include <sstream>
#include <string>

class VerificationFailed
{
public:
VerificationFailed( bool Throw ) : // si true lève l'exception
dans le

destructeur
throw_( Throw ),
msg_( "VerificationFailed : " )
{}

~VerificationFailed()
{
if ( this->throw_ )
{
throw this->msg_;
}
}

template<typename T>
VerificationFailed & operator << ( const T & t )
{
if ( this->throw_ )
{
std::ostringstream oss;
oss << t;
this->msg_ += oss.str();
}
return *this;
}

private:
bool throw_;
std::string msg_;
};

VerificationFailed verify( bool b )
{
return VerificationFailed( !b );
}

int main()
{
try
{
int A = 9;
std::cout << "debut du programmen";
verify( A < 10 ) << "A n'est pas un chiffre : " << A;
std::cout << "suite du programmen";
++A;
verify( A < 10 ) << "A n'est pas un chiffre : " << A;
std::cout << "fin du programmen";
}
catch ( const std::string & err )
{
std::cerr << err << 'n';
}
std::cin.ignore();
}

J'obtiens le résultat suivant:

debut du programme
suite du programme
VerificationFailed : A n'est pas un chiffre : 10

Cela semble donc marcher, bien que lever une exception dans un
destructeur

soit une mauvaise pratique (c'est plutôt sur la règle de la durée
de vie

d'un objet renvoyé par une fonction que je m'interroge). Serait-ce
l'exception qui confirme la règle ?


- Si tu utilises ton 'verify' a l'intérieur d'un destructeur, il
faudra prendre garde a rattraper l'exception dans le destructeur pour
ne pas risquer d'avoir une destruction inachevée.

- Si tu inseres dans ton operator<< le résultat d'une fonction qui
leve une exception, tu auras une 2eme levée d'exception non rattrapée
lors de la destruction de la pile, ce qui est justement le truc a
éviter quand on dit de ne pas lever d'exception dans un destructeur.

Avatar
Aurélien REGAT-BARREL
A priori, je dirais que si l'un des opérateurs << lance une
exception, le destructeur du flux sera appelé, et là, BOOM ! ÀMHA,
lancer une exception dans un destructeur est extrêmement délicat.


Oui. Mais à priori l'opérateur << ne sert qu'à créer le message décrivant
l'erreur, il sera utilisé comme dans cet exemple dans le cadre de
ostringstream. Y'a la concaténation de std::string à surveiller aussi. Je
peux faire ça à la bourrin try {} catch(...) {}.

Il faut pouvoir assurer qu'aucune exception ne sera lancée entre la
création et la destruction de chacune des instances. Ce qui doit
pourvoir néanmoins être fait dans certains cas particuliers. Dans ce
cas-ci, il faudrait imposer que les surcharges de l'opérateur << pour
ce flux ne lancent pas d'exceptions.


Mon interrogation concerne plutot le moment de la destruction de l'objet
renvoyé par verify(). Est-ce que l'objet renvoyé par une fonction (par
copie) est immédiatement détruit ?
En gros, est-ce que le code suivant :

VerificationFailed verify( bool b )
{
return VerificationFailed( !b );
}

int a = 0;
verify( a == 0 ) << "coucou";
verify( a == 1 ) << "coucou";


est équivalent au niveau de la portée de l'objet retourné à:

int a = 0;
{
VarificationFailed v1 = verify( a == 0 );
v1 << "coucou";
}// destruction v1
{
VarificationFailed v2 = verify( a == 1 );
v2 << "coucou";
}// destruction v2

ou y a-t-il un risque qu'un compilateur réalise ceci :

{
int a = 0;
VarificationFailed v1 = verify( a == 0 );
v1 << "coucou";
VarificationFailed v2 = verify( a == 1 );
v2 << "coucou";
}// destruction v1 et v2

Dans ce cas je serais bien embêté.


--
Aurélien REGAT-BARREL

Avatar
Aurélien REGAT-BARREL
Pourquoi lever une exception au lieu de tout arreter si c'est une
assertion ?


Ben l'exception est un bon moyen de tout arrêter proprement non ?
Je compte l'utiliser comme un assert() amélioré certes, mais pas seulement,
aussi voire surtout pour vérifier le code de retour de fonctions systèmes /
d'une bibliothèque tierce (pilotage d'une caméra scientifique...). Si une
telle fonction échoue, je peux pas faire grand chose, mais pas la peine de
vautrer tout le logiciel. Cette exception sera catchée un peu plus haut et
transformée en OperationFailed, l'utilisateur averti et puis c'est tout. Le
log contiendra plus de détails sur l'origine de l'échec (le message associé
au verify()).

- Si tu utilises ton 'verify' a l'intérieur d'un destructeur, il
faudra prendre garde a rattraper l'exception dans le destructeur pour
ne pas risquer d'avoir une destruction inachevée.


Peu de risques que cela se produise, j'ai quasiment aucun destructeur (je me
suis converti au RAII).

- Si tu inseres dans ton operator<< le résultat d'une fonction qui
leve une exception, tu auras une 2eme levée d'exception non rattrapée
lors de la destruction de la pile, ce qui est justement le truc a
éviter quand on dit de ne pas lever d'exception dans un destructeur.


C'est effectivement une possibilité, genre:

verify( value != 0 ) << "La valeur de la classe " << this->getName() << "
est nulle.";

si getName() lève une exception je suis mal...
Peu de risques malgré tout, bad_alloc me semble le principal. Moyennant une
bonne doc écrite en rouge clignotant, on devrait pouvoir s'assurer de la
chose.

Au niveau utilisation, vous en pensez quoi ? Je trouve ça bien plus pratique
que ce que j'ai fait jusque là:

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

Vous faîtes comment vous ?

--
Aurélien REGAT-BARREL

Avatar
Alexandre
ou y a-t-il un risque qu'un compilateur réalise ceci :

{
int a = 0;
VarificationFailed v1 = verify( a == 0 );
v1 << "coucou";
VarificationFailed v2 = verify( a == 1 );
v2 << "coucou";
}// destruction v1 et v2

Dans ce cas je serais bien embêté.


AMA c'est le cas... je ne sais pas exactement ce que dit la norme, mais il
me semble que tant que v1 est dans la portée (même s'il n'est plus utilisé)
il n'est pas détruit... donc v1 et v2 doivent être détruits lors de la
sortie de la portée...



--
Aurélien REGAT-BARREL




Avatar
SerGioGio
"Alexandre" a écrit dans le message de
news:41e80c66$0$19576$
ou y a-t-il un risque qu'un compilateur réalise ceci :

{
int a = 0;
VarificationFailed v1 = verify( a == 0 );
v1 << "coucou";
VarificationFailed v2 = verify( a == 1 );
v2 << "coucou";
}// destruction v1 et v2

Dans ce cas je serais bien embêté.


AMA c'est le cas... je ne sais pas exactement ce que dit la norme, mais il
me semble que tant que v1 est dans la portée (même s'il n'est plus
utilisé)

il n'est pas détruit... donc v1 et v2 doivent être détruits lors de la
sortie de la portée...



Pour ma part je ne crois pas. Je pense que la methode d Alexandre
fonctionne. Voir l' article de Bjarne Stroustrup
http://www.research.att.com/~bs/wrapper.pdf ou il utilise la meme technique
pour faire des fonctions prefixes et suffixes.

Pour ce qui est des exceptions dans le destructeur je m'y connais pas
assez... mais une chose est sure la syntaxe de "verify" est interessante!

SerGioGioGio


Avatar
Loïc Joly
Aurélien REGAT-BARREL wrote:
Bonjour à tous,
tout d'abord bonne année.
Je souhaite améliorer la gestion des erreurs et le système de logs de mon
programme. J'ai ragardé un peu log4cpp, pour l'instant c'est trop usine à
gaz pour moi et ça me satisfait pas trop.


Pareil pour moi. J'ai aussi regardé la bibliothèque de log qui se trouve
dans la zone de fichiers de la mailing list boost. Je l'ai trouvée
plutôt sympa, même si je n'ai pas encore eu l'occasion d'essayer.

--
Loïc

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

Pourquoi lever une exception au lieu de tout arreter si c'est une
assertion ?



Ben l'exception est un bon moyen de tout arrêter proprement non ?


Ca se discute. Certains disent qu'en cas d'assert, tu n'es pas certain
de l'état dans lequel tu es, et donc il vaut mieux arrêter tout au plus
vite.

D'autres disent que même si le premier argument est vrai, si le
désagrément causé par un arrêt brutal est relativement élevé, et si ce
qui peut se passer au cas ou le programme perdre totalement les pédales
n'est pas dramatique, vu que la probalité que ça se passe vraiment mal
est assez faible (estimation sans justification...), il vaut mieux
lancer une exception qui va arrêter calmement le programme.

J'avoue ne pas savoir me situer entre les deux camps.



Vous faîtes comment vous ?


Je ne fais pas, mais je me demande si un truc tordu comme le suivant
pourrait marcher :

class DoOnceAndThrow
{
DoOnceAndThrow() isAlreadyDone(false) {}
void tryToDoIt()
{
if (isAlreadyDone)
throw std::exception;
isAlreadyDone = true;
}
bool isAlreadyDone;
}

#define verify(b)
DoOnceAndThrow doat##__LINE__; // 1
if (b) ; // 2
else
while(doOnceAndThrow())
log

// 1 : Pour essayer de ne pas avoir deux identificateurs portant le même
nom dans la même portée. Ne marchera que s'il n'y a pas 2 verifie sur
une seule ligne

// 2 : Permet d'éviter des trucs étranges si verifie est appelé dans un
if qui comporte un else, par exemple.

--
Loïc


Avatar
Olivier Azeau
Aurélien REGAT-BARREL wrote:
Pourquoi lever une exception au lieu de tout arreter si c'est une
assertion ?



Ben l'exception est un bon moyen de tout arrêter proprement non ?
Je compte l'utiliser comme un assert() amélioré certes, mais pas seulement,
aussi voire surtout pour vérifier le code de retour de fonctions systèmes /
d'une bibliothèque tierce (pilotage d'une caméra scientifique...). Si une
telle fonction échoue, je peux pas faire grand chose, mais pas la peine de
vautrer tout le logiciel. Cette exception sera catchée un peu plus haut et
transformée en OperationFailed, l'utilisateur averti et puis c'est tout. Le
log contiendra plus de détails sur l'origine de l'échec (le message associé
au verify()).


En fait, c'est plutôt sur le vocabulaire "assertion" que portait ma
remarque : chez moi une assertion c'est un contrôle qui est *toujours*
vrai quel que soit le contexte d'exécution (entrées utilisateur, ...) et
qui n'est présent qu'en version debug en tant que "auto-controle" du code.

[snip]

Au niveau utilisation, vous en pensez quoi ? Je trouve ça bien plus pratique
que ce que j'ai fait jusque là:

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

Vous faîtes comment vous ?


Je fais pas.
Ce que je veux dire c'est que j'ai l'impression de ne pas connaître de
séquences aussi directes "test->message->on arrête tout"


Avatar
drkm
"Aurélien REGAT-BARREL" writes:

En gros, est-ce que le code suivant :


[...]

est équivalent au niveau de la portée de l'objet retourné à:


Un temporaire est détruit à la fin de l'instruction dans laquelle il
a été créé.

--drkm

1 2 3 4