IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

Langage C++ Discussion :

Votre avis sur la gestion d'erreur avec macros throwIf(), throwIfNot(), throwError()


Sujet :

Langage C++

Vue hybride

Message précédent Message précédent   Message suivant Message suivant
  1. #1
    Expert confirmé

    Homme Profil pro
    pdg
    Inscrit en
    Juin 2003
    Messages
    5 756
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 43
    Localisation : France, Hérault (Languedoc Roussillon)

    Informations professionnelles :
    Activité : pdg

    Informations forums :
    Inscription : Juin 2003
    Messages : 5 756
    Billets dans le blog
    3
    Par défaut Votre avis sur la gestion d'erreur avec macros throwIf(), throwIfNot(), throwError()
    Bonjour,

    La gestion des erreurs est une composante aussi importante que délicate dans une appli. Et je sollicite votre opinion sur 3 macros que je viens d'adopter dans un de mes projets.

    Elles ont pour but de simplifier la gestion d'erreurs internes au moyen d'exception. Leur design vise à satisfaire les contraintes suivantes:
    1. simplifier au maximum le test et de remontée d'erreur (éliminer les if)
    2. faciliter l'écriture de messages d'erreurs détaillés (inclure les code d'erreur...)
    3. permettre la recherche automatique des erreurs dans le source (au moyen d'un script python...) afin de les documenter
    4. permettre la traduction des messages d'erreur si on le souhaite
    5. permettre d'inclure l'emplacement dans le source (fichier, ligne...) où l'erreur a été émise


    Ces points réunis m'ont convaincu de la pertinence d'utiliser des macros que j'ai appelé throwError, throwIf et throwIfNot. Elles s'utilisent ainsi:

    Code cpp : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    void saveToFile(const gp::Message & msg, const fs::path & outputName) {
        std::ofstream file(outputName, std::ios::binary);
        throwIfNot(file, "Failed to create file {0}", outputName);
     
        std::string outputText;
        if (outputName.extension() == ".bin") {
            throwIfNot(msg.SerializeToString(&outputText),
                "Failed to encode to raw data"
            );
        }
        else if (outputName.extension() == ".txt") {
            throwIfNot(gp::TextFormat::PrintToString(msg, &outputText),
                "Failed to encode to text"
            );
        }
        else {
            throwError("Unsupported output file type {0}", outputName.extension());
        }
     
        throwIfNot(file << outputText,
            "Failed to write to file {0}", outputName
        );
    }

    Voilà pour "l'API" de ces macros. Voici mon implémentation actuelle, basée sur la fmtlib:

    Code cpp : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #include <fmt/format.h>
    #include <fmt/ostream.h>
     
    #define throwError(...)\
        throw std::runtime_error{ fmt::format(__VA_ARGS__) }
    #define throwIf(cond, ...)\
        for (;(cond);) {\
            throwError(__VA_ARGS__);\
        }
    #define throwIfNot(cond, ...)\
        for (;!(cond);) {\
            throwError(__VA_ARGS__);\
        }

    On pourrait de façon assez similaire se baser sur Qt, et plus précisément QObject::tr() afin de rendre les messages d'erreur traduisibles. Il n'y aurait alors que la syntaxe des paramètres acceptés qui change ("{1}" => "%1").

    En ce qui me concerne, je ne suis pas fan de la création de moulte types d'exceptions. Je préfère ne lancer que des std::runtime_error et ne catcher que des std::exception. C'est à ce jour le seul souci que je vois avec ces macros : la difficulté de modifier au cas par cas le type de l'erreur levée. Mais pour moi c'est plutôt une bonne chose car j'utilise les exceptions pour remonter des erreurs critiques qui ne nécessitent (ne peuvent) pas être corrigées, mais simplement signalées. Je trouve en effet la gestion d'erreurs via une hiérarchie d'exceptions plus complexe, mais c'est un autre débat.

    Qu'en pensez-vous ?

    PS: l'utilisation de for(;;) dans la macro est une habitude prise vis à vis d'un warning VC++ sur le test de conditions qui est toujours vraie (si on passe "true" par exemple) mais c'est peut-être pas pertinent ici

  2. #2
    Membre Expert
    Avatar de Pyramidev
    Homme Profil pro
    Tech Lead
    Inscrit en
    Avril 2016
    Messages
    1 513
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Tech Lead

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 513
    Par défaut
    Pourquoi pas.

    Par contre, pour le code des macros, à la place de :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    #define throwIf(cond, ...)\
        for (;(cond);) {\
            throwError(__VA_ARGS__);\
        }
    il faudrait écrire soit ceci :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    #define throwIf(cond, ...)\
        if(cond)\
            throwError(__VA_ARGS__);\
        else\
            (void)0
    soit cela :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    #define throwIf(cond, ...)\
        do {\
            if(cond)\
                throwError(__VA_ARGS__);\
        } while(false)
    C'est pour pouvoir compiler ce genre de code :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    if(condition)
        throwIf(paramètres...);
    else
        doSomething();
    Ces deux astuces syntaxiques sont expliquées dans 2 entrées de la FAQ C++ de isocpp.org, respectivement ici et .

  3. #3
    Expert confirmé

    Homme Profil pro
    pdg
    Inscrit en
    Juin 2003
    Messages
    5 756
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 43
    Localisation : France, Hérault (Languedoc Roussillon)

    Informations professionnelles :
    Activité : pdg

    Informations forums :
    Inscription : Juin 2003
    Messages : 5 756
    Billets dans le blog
    3
    Par défaut
    Qu'est-ce qui va pas avec l'utilisation de for(;;) ?

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    if(condition)
        for(;true;) { throw "oups!"; }
    else
        doSomething();
    La différence c'est que sous VC++, ça passe sans problème alors que do {} while (false) ou if (false) else {} peuvent produire le warning 4127 (conditional expression is constant).

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    throwIf(sizeof(int) == 4, "oups"); // exemple pour générer le warning 4127
    Après, comme expliqué vite fait dans mon PS, c'est peut-être souhaitable dans le cas de ces macros de générer ce warning, alors ok pour faire comma la FAQ dit qu'il faut faire

    Mais ce qui m'intéresse c'est votre avis sur comment mieux gérer nos erreurs en C++.

  4. #4
    Expert éminent

    Femme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juin 2007
    Messages
    5 202
    Détails du profil
    Informations personnelles :
    Sexe : Femme
    Localisation : France, Essonne (Île de France)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Juin 2007
    Messages : 5 202
    Par défaut
    Comment gérer les erreurs?
    RAII.
    encore RAII.
    toujours RAII.
    utiliser le système de typage, avec des mots clés comme class et explicit.

    S'il n'est pas possible de compiler une situation erronée, alors la situation ne peut pas être erronée, et l'erreur n'est pas à controler.

    Il suffit alors de valider les entrées utilisateurs proprement, et de lui remonter l'erreur proprement.
    Pour une lecture de fichier, une exception à l'analyse.
    Nul besoin d'exception si on a une interface interactive.

  5. #5
    Membre émérite

    Profil pro
    Inscrit en
    Décembre 2013
    Messages
    403
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Décembre 2013
    Messages : 403
    Par défaut
    @ternel

    Comment tu gères un disque qui était accessible lorsque l'utilisateur a entré le nom du fichier, mais devient indisponible avant ou pendant la lecture ?

    Il faut remettre les choses a leur place quand même :

    - RAII n'est pas un mécanisme de gestion des erreurs. C'est un mécanisme de gestion automatique des ressources. Qui est effectivement très pratique en cas d'erreur ou d'exception, mais pas uniquement.

    - les exceptions ne sont pas un mécanisme des gestion des erreurs. C'est un mécanisme de gestion des choses qui arrivent (ou qui devrait arriver) exceptionnellement. Cela peut être des erreurs, mais peut on considérer qu'un disque qui n'est plus accessible est une "erreur" ? (Difficile de donner une definition claire de "erreur", mais on peut trouver "faute , méprise" sur wiki. Utiliser un ofstream invalide serait une erreur, mais qu'un disque ne soit plus accessible est une situation qui peut arriver et qui doit être prise en compte par le logiciel).

    Pas sur de comprendre la remarque sur "class" et "explicit".

    @Aurelien

    Je pense que for est ok. C'est pareil qu'un do-while

  6. #6
    Expert éminent
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 644
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 53
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 644
    Par défaut
    Salut,
    Citation Envoyé par ternel Voir le message
    Comment gérer les erreurs?
    RAII.
    encore RAII.
    toujours RAII.
    utiliser le système de typage, avec des mots clés comme class et explicit.

    S'il n'est pas possible de compiler une situation erronée, alors la situation ne peut pas être erronée, et l'erreur n'est pas à controler.

    Il suffit alors de valider les entrées utilisateurs proprement, et de lui remonter l'erreur proprement.
    Pour une lecture de fichier, une exception à l'analyse.
    Nul besoin d'exception si on a une interface interactive.
    Attention ! RAII n'a trait qu'aux ressources, et nous permet d'offrir la garantie que ces ressources seront correctement libérée, y compris dans les situations exceptionnelles / non envisagées.

    D'une certaine manière, RAII permet d'offrir la garantie que, si un problème exceptionnel devait survenir, mais qu'il est possible de corriger ce problème au sein de l'application afin qu'elle puisse continuer à fonctionner, les conséquences de ce problème "se limitent" à l'application elle-même (à cause d'éventuelles pertes de données) et ne s'étendent pas au système sur lequel l'application s'exécute (à cause, essentiellement, de la perte de ressources système).

    En outre, il y a deux catégories de problèmes qui peuvent survenir à l'exécution : Ceux qui sont dus à "une erreur de la part du développeur", et ceux contre lesquels le développeur ne peut rien.

    Les premiers sont des erreurs de logique : un pointeur nul là où on s'attendait à disposer d'un pointeur sur un objet polymorphe existant, un indice occasionnant un accès hors limites, le fait de laisser passer un diviseur dont la valeur est égale à 0, ...

    Ce genre d'erreur, c'est au développeur de s'assurer qu'elles ne seront pas commises en testant les préconditions à l'aide d'assertion (c'est idéal, car, une fois que la logique n'a plus d'erreur, les tests associés aux assertions deviennent inutiles, vu que les situations d'échec auront été bannies).

    La deuxième catégorie est beaucoup plus embêtante, car "tout ce que le développeur peut faire", c'est ... subir l'erreur : si un psychopathe coupe le cable réseau alors qu'une connexion à un serveur distant a été établie, si un disque dur flanche sans crier gare parce que cela fait déjà 15 ans qu'il tourne 24/7, si un imbécile va supprimer un fichier indispensable, si une autre application utilise / perd tellement de mémoire que tout le système en devient instable,... La seule chose que le développeur puisse faire, c'est de constater "ben, voilà... c'est comme cela...", éventuellement écrire ce qui s'est passé dans un log, et, "de temps en temps", voir s'il ne peut pas essayer de récupérer le problème (par exemple, en essayant de recréer la connexion avec le serveur distant... ce qui risque d'être difficile tant que le cable n'aura pas été changé )

    Très souvent, la meilleure des choses à faire dans ce cas là, c'est de ... laisser s'arrêter purement et simplement l'application.

    Alors, bien sur, avant que l'application ne s'arrête, on peut essayer de limiter les dégâts en
    • sauvegardant ce qui peut l'être (pour éviter les pertes de données)
    • essayant de résoudre le problème (si c'est possible)
    • écrivant une entrée dans le fichier log
    • fournissant éventuellement un numéro d'erreur à l'utilisateur à transmettre au help desk

    Mais ce n'est que "la réaction de la dernière chance"
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

  7. #7
    Expert confirmé

    Homme Profil pro
    pdg
    Inscrit en
    Juin 2003
    Messages
    5 756
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 43
    Localisation : France, Hérault (Languedoc Roussillon)

    Informations professionnelles :
    Activité : pdg

    Informations forums :
    Inscription : Juin 2003
    Messages : 5 756
    Billets dans le blog
    3
    Par défaut
    Citation Envoyé par ternel Voir le message
    Et là encore, les macros ne me parraissent pas utile.
    quel est l'avantage de MACRO_THROW_IF(cond, arguments de l'erreur) sur if(cond) throw_exception(arguments de l'erreur);
    Note que suite à la remarque de Luc j'ai remplacé les "macros" par des templates variadiques (d'où les guillemets).

    L’intérêt d'encapsuler le test de condition dans une "macro" est simple : pouvoir faire échouer ce test dans le contexte de code de test afin de vérifier le bon traitement de l'erreur.

    Un truc du genre:

    Code cpp : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    // implémentation fournie si compilé en mode TU
    template <typename ...Args>
    void throwIf(bool cond, Args&&... args) {
        s_currentTestId += 1;
        if (s_currentTestId == s_failAtTestId || cond) {
            throwError(std::forward<Args>(args)...);
        }
    }

    ça s'utilise ainsi (implémentation naïve):

    Code cpp : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    void testExceptionSafety(std::function<void()> f) {
        // exécuter une première fois normalement (pour compter le nombre de tests effectués)
        s_currentTestId = 0;
        REQUIRE_NOTHROW( f() );
        const size_t nbIds = s_currentTestId;
     
        for (size_t id : make_range(1, nbIds + 1)) {
            s_currentTestId = 0;
            s_failAtTestId = id;
            REQUIRE_THROWS( f() );
            // TODO: tester ici qu'il n'y a pas de leak...
     
            // vérification que tout a bien été nettoyé
            REQUIRE_NOTHROW( f() );
        }
    }

    De cette manière tu peux simuler tous les cas d'erreur gérés par ton code... à condition qu'il utilise des "macros" qui encapsulent le if.

    Citation Envoyé par koala01 Voir le message
    Alors, bien sur, avant que l'application ne s'arrête, on peut essayer de limiter les dégâts en
    • écrivant une entrée dans le fichier log
    Et aussi flusher les logs sur disque avant que l'application ne meurt sinon on peut perdre les derniers messages bufferisés précisément quand ils sont super importants

  8. #8
    Membre Expert
    Avatar de Pyramidev
    Homme Profil pro
    Tech Lead
    Inscrit en
    Avril 2016
    Messages
    1 513
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Tech Lead

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 513
    Par défaut
    Citation Envoyé par Aurelien.Regat-Barrel Voir le message
    Qu'est-ce qui va pas avec l'utilisation de for(;;) ?

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    if(condition)
        for(;true;) { throw "oups!"; }
    else
        doSomething();
    Ce que je voulais pointer du doigt, c'était ça :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    if(condition)
        for(;true;) { throw "oups!"; }; // Le dernier point-virgule provoque une erreur de compilation.
    else
        doSomething();
    Le problème vient des crochets.

    Après réflexion, tu peux faire :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    #define throwIf(cond, ...)\
        for (;(cond);)\
            throwError(__VA_ARGS__)

  9. #9
    Expert confirmé

    Homme Profil pro
    pdg
    Inscrit en
    Juin 2003
    Messages
    5 756
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 43
    Localisation : France, Hérault (Languedoc Roussillon)

    Informations professionnelles :
    Activité : pdg

    Informations forums :
    Inscription : Juin 2003
    Messages : 5 756
    Billets dans le blog
    3
    Par défaut
    Citation Envoyé par ternel Voir le message
    Il suffit alors de valider les entrées utilisateurs proprement, et de lui remonter l'erreur proprement.
    C'est le but de ce thread : clarifier le "il suffit" et comment faire ça "proprement". Et je ne demande qu'à ce que tu me montres comment le RAII (qui est un idiome donc au niveau local et non global...) peut simplifier encore plus l'écriture et l'utilisation de ma fonction saveToFile()

    D'après mon expérience, la gestion propre des erreurs n'est pas du tout évidente car le plus imbriqué des appels est susceptible de devoir signaler un problème à l'utilisateur dans sa langue et dans son contexte (échouer à ouvrir le fichier qu'il a demandé n'est pas pareil qu'échouer à lire un fichier de conf interne). Sachant qu'on veut aussi disposer de détails techniques pour l'analyse côté dev (fichiers de logs...) et pouvoir documenter la liste des messages possible pour le support. Déjà ça, c'est coton à faire "proprement". Mais alors quand l'erreur se produit dans un autre thread ou une fonction distante appelée en RPC...

    En fait, quand je découvre un code source, la première chose que je regarde pour me faire une idée macro de sa qualité est sa gestion d'erreur. Pour moi c'est la colonne vertébrale autour de laquelle s'architecture tout le reste. Et bien souvent c'est à grands coups de if imbriqués. Autant dire que c'est très lourd à lire et écrire!

    Là, avec ces 3 petites macros, j'ai réussi à diviser par deux le nombre de lignes de code initiales et à rendre les erreurs plus verbeuses. Je me demande si on peut faire mieux.

    Citation Envoyé par Pyramidev Voir le message
    Ce que je voulais pointer du doigt, c'était ça :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    if(condition)
        for(;true;) { throw "oups!"; }; // Le dernier point-virgule provoque une erreur de compilation.
    else
        doSomething();
    Le problème vient des crochets.
    Ah ok. Merci.

  10. #10
    Expert confirmé
    Avatar de Luc Hermitte
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Août 2003
    Messages
    5 296
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : Aéronautique - Marine - Espace - Armement

    Informations forums :
    Inscription : Août 2003
    Messages : 5 296
    Par défaut
    Plutôt que des macros, pourquoi ne pas en faire des fonctions inlines template variadiques ?
    Blog|FAQ C++|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS|Bons livres sur le C++
    Les MP ne sont pas une hotline. Je ne réponds à aucune question technique par le biais de ce média. Et de toutes façons, ma BAL sur dvpz est pleine...

  11. #11
    Expert confirmé
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Février 2005
    Messages
    5 502
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 53
    Localisation : France, Val de Marne (Île de France)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : Conseil

    Informations forums :
    Inscription : Février 2005
    Messages : 5 502
    Par défaut
    Je vois pas trop la différence avec la MACRO VERIFY et le fait d'avoir un seul type d'exception est rédhibitoire.

    Je ne catche que les que je sais gérer, et n'avoir qu'un seule type d'exception, c'est la loose complète !!!

    Mais pour moi c'est plutôt une bonne chose car j'utilise les exceptions pour remonter des erreurs critiques qui ne nécessitent (ne peuvent) pas être corrigées, mais simplement signalées.
    Cela rend son utilisation impossible dans un projet qui utilise toutes les fonctionnalités des exceptions.

    simplifier au maximum le test et de remontée d'erreur (éliminer les if)
    VERIFY fait pareil.

    faciliter l'écriture de messages d'erreurs détaillés (inclure les code d'erreur...)
    Les exceptions ont déjà ce champ, et même un message d'erreur.

    permettre la recherche automatique des erreurs dans le source (au moyen d'un script python...) afin de les documenter
    Heu, pourquoi faire, vu que la stacktrace donne les numéros de ligne et les chemins vers les fichiers ?

    permettre la traduction des messages d'erreur si on le souhaite
    Traduire un message d'erreur qui n'est destiné qu'au développeur, est vraiment nécessaire ???

    permettre d'inclure l'emplacement dans le source (fichier, ligne...) où l'erreur a été émise
    La stacktrace serait bien plus utile est il y a déjà des trucs, même si c'est plateforme dépendant.
    http://stackoverflow.com/questions/3...tack-in-c-or-c

Discussions similaires

  1. Votre avis sur la gestion d'emails en entreprise ?
    Par tempd6 dans le forum Emploi
    Réponses: 7
    Dernier message: 07/01/2016, 09h42
  2. Réponses: 1
    Dernier message: 27/06/2014, 14h50
  3. Votre Avis sur l'article : Le clustering avec Glassfish
    Par millie dans le forum Glassfish et Payara
    Réponses: 7
    Dernier message: 26/03/2010, 16h58
  4. Réponses: 5
    Dernier message: 27/10/2009, 19h06
  5. Votre avis sur les outils de gestion qualité du codage
    Par leminipouce dans le forum Qualimétrie
    Réponses: 1
    Dernier message: 19/10/2006, 21h00

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo