• # Une autre vision des exceptions

    Posté par (site web personnel) . En réponse au journal De l'influence néfaste de Google sur les développeurs C++. Évalué à 7.

    Ma modeste contribution au débat...

    Soit le pseudo-code suivant :

    Lire octet
    TantQue octet != 0
     Si octet == 1
     Faire ActionA 
     Sinon Si octet == 2
     Lire octet
     Si octet == 0
     Faire ActionB
     Sinon
     Faire ActionC
     FinSi
     Sinon
     Faire ActionD
     FinSi
     Lire octet
    FinTantQue

    Transcription naïve en C, en utilisant getc (j'écarte volontairement l'utilisation de switch...case) :

    int main(...) {
     int c;
     c = getc(...);
     while ( c != 0 ) {
     if ( c == 1 )
     ActionA();
     else if ( c == 2 ) {
     if ( getc(...) == 0 )
     ActionB();
     else
     ActionC();
     } else
     ActionD();
     c = getc(...);
     }
     return EXIT_SUCCESS;
    }

    Avec ce code, s'il y a le moindre problème avec le flux lu par getc(...), on rentre dans une boucle infinie.

    Implémentons maintenant une gestion d'erreurs à la C.

    int main(...) {
     int c;
     c = getc(...);
     if ( c == EOF )
     return EXIT_FAILURE;
     while ( c != 0 ) {
     if ( c == 1 )
     ActionA();
     else if ( c == 2 ) {
     c = getc(...);
     if ( c == EOF )
     return EXIT_FAILURE;
     if ( c == 0 )
     ActionB();
     else
     ActionC();
     } else
     ActionD();
     c = getc(...);
     if ( c == EOF )
     return EXIT_FAILURE;
     }
     return EXIT_SUCCESS;
    }

    On voit que, par rapport à la transcription naïve du pseudo-code, on a a doublé le nombre de if à cause de la gestion d'erreurs. Sans compter que j'ai fait l'impasse sur la gestion d'erreurs des fonctions ActionX !

    Utilisons maintenant les exceptions :

    int get(void) {
     int c = getc(...);
     if ( c == EOF )
     throw ...;
     else return c;
    }
    int main(...) {
     int c;
     try {
     c = get();
     while ( c != 0 ) {
     if ( c == 1 )
     ActionA();
     else if ( c == 2 ) {
     if ( get() == 0 )
     ActionB();
     else
     ActionC();
     } else
     ActionD();
     c = get();
     }
     } catch (...) {
     return EXIT_FAILURE;
     }
     return EXIT_SUCCESS;
    }

    On voit que le code entre le try et le catch correspond presque ligne à ligne avec le pseudo-code correspondant. Je précise que l'ensemble du code ci-dessus est quand même une énorme simplification de la manière dont j'utilise les exceptions.

    Pour résumer, j'utilise les exceptions afin que mon code soit très proche du pseudo-code que l'on pourrait écrire pour l'algorithme correspondant, ce qui, d’après moi, facilite grandement sa compréhension, sa maintenabilité et son évolutivité, en évitant qu'il ne soit pollué par le code dédié à la gestion des erreurs.

    Cette approche, je l'ai mise en œuvre alors que je programmais encore en C, lorsque je ne connaissais ni le C++, ni son mécanisme d'exceptions. Les throw, try, catch étaient simulés à l'aide de macros s'appuyant sur la bibliothèque setjmp. Ces macros garantissaient par ailleurs que toutes les ressources étaient correctement libérées lors de la survenue d'une «exception» (en fait, un appel à longjmp).

    Lorsque je suis passé au C++, j'ai réimplémenté ces macros en m'appuyant sur les exceptions, tout en conservant leur implémentation originale, les premiers compilateurs C++ n'implémentant pas tous les exceptions.

    Avec cette approche, je n'ai jamais rencontré de problèmes particuliers avec les exceptions. Pour autant, je n'irais pas jusqu'à affirmer qu'elle n'a pas son lot d'inconvénients qui la rendrait inapplicable dans certaines situations...

    Zelbinium: pour la génération qui crée, pas celle qui scrolle...