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
Aurelien REGAT-BARREL
Désolé Loïc, mais j'essaye au maximum d'éviter les macros, surtout pour
quelque chose de sensible comme ça.

--
Aurélien REGAT-BARREL
Avatar
SerGioGio
Je crois que ce que tu recherches ce sont les "enforcements" de Andrei
Alexandrescu de l' article suivant:
http://www.cuj.com/documents/s‚50/cujcexp2106alexandr/alexandr.htm

Malheurusement il ne parle jamais de formatage d' erreur.

SerGioGioGio
Avatar
Olivier Azeau
Aurelien REGAT-BARREL wrote:
Bon, désolé pour avoir parlé d'assertion. C'est verify() et pas assert(). Je
compte aussi m'en servir comme d'un assert(), mais avant tout comme d'un
moyen simple de faire des vérif et de lever des exceptions si elles ne sont
pas ok.
Par exemple, à la lecture d'un fichier de configuration, vous exigez que la
version de ce fichier soit "1".
Je trouve ceci :

int version = get_version();
verify( version == 1 ) << "numéro de version non supporté : " << version;

plus simple et parlant que :

int version = get_version();
if ( version != 1 )
{
std::ostringstream oss;
oss << "numéro de version non supporté : " << version;
Logger::Error( oss.str() );
throw OperationFailed( oss.str() );
}


Moi aussi.


Jusque là j'avais des fonctions genre:

void OperationFailedError( const char * msg, int n )
{
std::ostringstream oss;
oss << msg << n;
Logger::Error( oss.str() );
throw OperationFailed( oss.str() );
}

Et j'ai au moins 10 prototypes de OperationFailedError en fonction des
arguments voulus :

void OperationFailedError( const char * msg );
void OperationFailedError( const char * msg, int n );
void OperationFailedError( const char * msg1, int n, const char * msg2 );

D'ailleurs j'aurais pu faire ça avec des templates. Mais n'empêche que je
dois quand même écrire tout ça :

if ( version != 1 )
{
OperationFailedError( "numéro de version non supporté : ", version );
}

Arriver à condenser ce bloc en une seule ligne je trouve ça séduisant.

Car j'ai une question simple : comment faites vous pour gérer ce type de
vérifications, qui sont si fréquentes et barbantes à écrires ?


Sur l'aspect construction du log, je suis entièrement d'accord.
Je trouve assez idiomatique l'utilisation de l'opérateur << pour insérer
une liste d'arguments dont le nombre n'est pas connu à l'avance.

Pour la levée d'exception, je suis plus réservé : lever une exception
c'est simple. Ce qui l'est moins c'est de la rattraper et d'exécuter le
traitement adéquat.

Par ailleurs, je ne trouve par forcément :

verify( version == 1 ) << "numéro de version non supporté : " << version;

plus expressif que, par exemple :

if ( version != 1 )
return operationFailed.fire( errorLog << "numéro de version non
supporté : " << version );

Le problème que j'ai rencontré à plusieurs reprises avec les levées
d'exception, c'est qu'elles cassent le workflow du niveau supérieur et
qu'elles ne sont donc vraiment utiles que quand on veut vraiment annuler
tout un ensemble d'opération sur plusieurs niveaux.
C'est pour cela que que trouve souvent utile d'utiliser un objet
intermédiaire pour transporter l'information de réussite/échec.

En fait tout cela dépend de la complexité du niveau qui doit traiter
l'exception...

Avatar
Olivier Azeau
SerGioGio wrote:
Je crois que ce que tu recherches ce sont les "enforcements" de Andrei
Alexandrescu de l' article suivant:
http://www.cuj.com/documents/s‚50/cujcexp2106alexandr/alexandr.htm

Malheurusement il ne parle jamais de formatage d' erreur.



J'ai peut être mal compris mais j'avais surtout eu l'impression que ces
"enforcements" étaient des run-times assertions, pas vraiment des
vérifications pour lesquelles le traitement des cas d'erreur a bcp plus
d'importance.

Avatar
Aurelien REGAT-BARREL
Par ailleurs, je ne trouve par forcément :

verify( version == 1 ) << "numéro de version non supporté : " << version;

plus expressif que, par exemple :

if ( version != 1 )
return operationFailed.fire( errorLog << "numéro de version non supporté
: " << version );


Je comprends. Mais le coding style que j'ai adopté fait que je dois créer un
bloc accolades ouvrantes / fermantes même pour un if() avec une seule
instruction, c'est déjà moins motivant à écrire. Et surtout, que doit
renvoyer operationFailed.fire() ? Car j'ai déjà testé cette approche, avec
une fonction Error qui fait le cleaning nécessaire et renvoie false. Mais
des fois ma fonction doit renvoyer un smart ptr par exemple :

ConfigFilePtr ReadCfg( std::string FileName )
{
ConfigFilePtr cfg = ConfigFilePtr::New();
if ( !cfg->Read( FileName ) )
{
// qu'est-ce qui est renvoyé ?
return operationFailed.fire( errorLog << "Impossible de lire " <<
FileName );
}
return cfg;
}

ou encore le fait de renvoyer un booléen oblige à chainer des tests:

bool ReadCfg();

bool Init()
{
if ( !ReadCfg() )
{
return false;
}
}

bool DoSomething()
{
if ( !Init() )
{
return false;
}
}

etc...

Si bien que rapidement on ne sait plus très bien où est gérée l'erreur, qui
doit afficher un message d'erreur, et même quelle est l'erreur...
C'est pour ça que j'essaye de simplifier cette gestion avec un seul bloc
try...catch en amont:

bool ReadCfg();

bool Init()
{
ReadCfg();
}

bool DoSomething()
{
try
{
Init();
}
catch ( const OperationFailed & e )
{
MessageBoxError( "Echec de l'opération", e.what() );
}
}

Le problème que j'ai rencontré à plusieurs reprises avec les levées
d'exception, c'est qu'elles cassent le workflow du niveau supérieur et
qu'elles ne sont donc vraiment utiles que quand on veut vraiment annuler
tout un ensemble d'opération sur plusieurs niveaux.


Je me doute bien que ce n'est pas la solution miracle. Mais dans mon cas
elle me parraîssent intéressantes, car elles serviront dans une IHM à gérer
l'échec d'une opération lancée en réaction d'une action de l'utilisateur.

--
Aurélien REGAT-BARREL

Avatar
Olivier Azeau
Aurelien REGAT-BARREL wrote:
Par ailleurs, je ne trouve par forcément :

verify( version == 1 ) << "numéro de version non supporté : " << version;

plus expressif que, par exemple :

if ( version != 1 )
return operationFailed.fire( errorLog << "numéro de version non supporté
: " << version );



Je comprends. Mais le coding style que j'ai adopté fait que je dois créer un
bloc accolades ouvrantes / fermantes même pour un if() avec une seule
instruction, c'est déjà moins motivant à écrire. Et surtout, que doit
renvoyer operationFailed.fire() ?


Rien n'empêche de mettre cette méthode comme un template et de fixer la
valeur de retour en cas d'échec chez l'appelant. En d'autres termes
c'est le niveau supérieur qui initialise operationFailed par rapport à
ses besoins, en laissant éventuellement faire le constructeur par défaut
du paramètre template s'il n'utilise pas la valeur de retour en cas d'échec.

Le problème que j'ai rencontré à plusieurs reprises avec les levées
d'exception, c'est qu'elles cassent le workflow du niveau supérieur et
qu'elles ne sont donc vraiment utiles que quand on veut vraiment annuler
tout un ensemble d'opération sur plusieurs niveaux.



Je me doute bien que ce n'est pas la solution miracle. Mais dans mon cas
elle me parraîssent intéressantes, car elles serviront dans une IHM à gérer
l'échec d'une opération lancée en réaction d'une action de l'utilisateur.



Tout à fait, c'est ce que je disais en parlant de grosse sous-partie de
programme : la justification de l'utilisation des exceptions se mesure
au nombre de blocs try/catch nécessaires pour les gérer.
Dans le cas où l'on n'a besoin que d'un seul try/catch, la question ne
se pose même pas !


Avatar
drkm
Olivier Azeau writes:

la justification de l'utilisation des exceptions se mesure
au nombre de blocs try/catch nécessaires pour les gérer.
Dans le cas où l'on n'a besoin que d'un seul try/catch, la question ne
se pose même pas !


Je ne comprend pas.

--drkm

Avatar
Olivier Azeau
Aurélien REGAT-BARREL wrote:
la justification de l'utilisation des exceptions se
mesure



au nombre de blocs try/catch nécessaires pour les gérer.
Dans le cas où l'on n'a besoin que d'un seul try/catch, la
question ne



se pose même pas !


Je ne comprend pas.


Je pense qu'il veut dire que si on doit multiplier les blocs
try...catch ça

devient super lourd les exceptions. Typiquement:

void ScanCompressedFilesForViruses( StringList list )
{
try
{
for ( int i = 0; i < list.size(); ++i )
{
std::string file_name = list[ i ];
try
{
File * file = DecompressFile( file_name );
if ( file->ScanViruses() ) // virus trouvé
{
try
{
AskUserForOperation( file );
}
catch( const UserInterfaceError & e )
{
Logger::Error( e.what() );
}
}
}
catch ( const DecompressionFailed & e )
{
ErrorMsg( "Impossible de décompresser le fichier ",
file_name );
}
}
}
catch ( const std::exception & e )
{
ErrorMsg( "L'opération a échoué", e.what() );
}
}

on se retrouve facilement avec 2 ou 3 blocs try...catch imbriqués,
des fois

juste pour tester le résultat d'une seule fonction qui si elle
échoue ne

doit pas faire aborder tout le traitement. Comme dans cet exemple où
on fait

une opération sur n fichier, si l'opération échoue sur l'un
d'entre eux, ça

doit pas empêcher de continuer sur les autres.


Tout a fait.



Avatar
kanze
Olivier Azeau wrote:
James Kanze wrote:
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 ?


Proprement, c'est rélatif. L'exception ne peut pas créer la
propreté une fois que c'est partie. En fait, il y a une
risque que les destructeurs l'empire. En fait, quand rien ne
va comme on s'y attendait, un core dump reste ce qu'il y a
de plus propre. Au moins on est sûr de ne pas faire plus de
dégats qu'on n'en a déjà fait.


C'est peut-être pour cela que j'ai l'impression que l'utilité
des exceptions est en fait très limitée :


Disons que comme tout, on peut en abuser. La mode actuelle
semble aller dans ce sens.

- soit on est dans un cas où rien ne va plus (risque de
corruption, ...) et alors autant sortir immédiatement


Tout à fait.

- soit on est dans un cas où il y a erreur, mais cette erreur
n'a rien "d'exceptionnel", au sens où elle est prévue par le
workflow et, là je préfère avoir mon propre système pour gérer
les cas de fonctionnement "dégradés" : ils sont souvent gérés
localement et le mécanisme de remontée d'exception n'apporte
rien.


Tout à fait. C'est en fait un des comportements « normaux »
qu'il faut prévoir.

En fin de compte, j'ai l'impression que la levée d'exception
sert surtout dans le cas d'un problème important qui impacte
tout une "grosse" sous-partie du programme, l'exception
permettant alors de détruire entièrement cette sous-partie et
de redonner la main à un niveau élevé qui va la reconstruire
de zéro.


C'est surtout ça, à mon avis. Dans un serveur, par exemple, on
n'arrive pas à traiter une requête pour une raison quelconque. À
ce moment-là, on lève une exception pour avorter la requête.

--
James Kanze GABI Software http://www.gabi-soft.fr
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 REGAT-BARREL
Je crois que ce que tu recherches ce sont les "enforcements" de Andrei
Alexandrescu de l' article suivant:
http://www.cuj.com/documents/s‚50/cujcexp2106alexandr/alexandr.htm

Malheurusement il ne parle jamais de formatage d' erreur.


J'ai lu avec intérrêt l'article. C'est effectivement proche de ce que je
souhaite, mais l'utilisation est moins agréable je trouve. L'implémentation
est plus clean c'est sûr. Mais je crois que je vais prendre le risque et
tester un peu ce que donne mon approche. Vu que je ne programme pas l'IA
d'une centrale nucléaire ou d'une sonde spaciale, je défis Murphy en
décrétant que le risque d'avoir une exception levée sur un des paramètres
passé à operator<<() est négligeable.

--
Aurélien REGAT-BARREL

1 2 3 4