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++

  1. #21
    Expert éminent sénior

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

    Informations professionnelles :
    Activité : pdg

    Informations forums :
    Inscription : Juin 2003
    Messages : 5 750
    Points : 10 667
    Points
    10 667
    Billets dans le blog
    3
    Par défaut
    Bon, de tous ces échanges (surtout avec Paul ) j'ai réalisé que j'avais un soucis de clarté au niveau de mon utilisation des exceptions et je comprends mieux la remarque sur leur typage. Alors je vais reformuler du mieux que je peux.

    Tout d'abord, je pense que préciser le contexte est important (j'aurais du commencer par là je crois!). Quand on fait une application IHM, il peut être utile de créer différents types d'exceptions (mais je reste très réservé dans le cas de bibliothèques - je détaillerai peut-être ensuite). Dans mon cas, il s'agit de programmes (micro-services) en ligne de commande destinés à tourner sur de l'embarqué ou en tant que serveur (gRPC).

    Du coup je distingue 3 familles d'erreurs:
    • erreurs courantes qui sont gérées par l'appelant (find() ne touve rien...) => code de retour classique / std::optional
    • les erreurs fatales qui entrainent la fermeture quasi immédiate du programme: pan t'es mort
    • les erreurs inattendues où on ne peut rien faire mais qui ne remettent pas en cause l'intégrité du programme => utilisation de mes "macros"


    C'est donc pour gérer des erreurs peu courantes mais non fatales que j'utilise ces "macros". Chaque erreur:
    • est susceptible d'être reportée et ignorée. Ex: le serveur échoue à traiter une requête, il catch et forward l'erreur depuis son handler ("traitement générique"), et il continue sa vie tranquillou pépère... à condition de tout bien nettoyer (RAII) => d'où mon besoin de tester automatiquement toutes les erreurs de ce type.
    • l'erreur est suceptible d'être transmise (sérialisée) par le réseau et donc d'être affichée auprès de l'utilisateur. Je gagne en simplicité à générer in-situ le message texte descriptif. On peut aussi me demander plus tard de les traduire.


    J'espère que c'est plus clair

    Par commodité j'ai choisi d'utiliser std::runtime_error pour gérer ces erreurs. Peut-être que c'est pas une bonne idée et que je devrais créer un type spécifique? A voir.

    Ce qui est sûr c'est que pour ASSERT_ALWAYS j'ai fait l'erreur de la mapper sur ces "macros" alors qu'elle est utilisée dans un contexte d'erreur fatale (par rapport à assert, ASSERT_ALWAYS reste en debug). Du coup je vais la conserver comme à l'origine : appel de std::abort : pan t'es mort!

    Au passage, rapellons que si y'a pas de catch{} qui englobe un throw, alors le dépilage de la pile est facultatif du point de vue de la norme et donc zappé par certains compilos (bye bye le RAII!). Faut donc bien savoir ce qu'on fait quand on ne met pas de catch dans son main()!

  2. #22
    Expert éminent sénior

    Femme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juin 2007
    Messages
    5 189
    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 189
    Points : 17 141
    Points
    17 141
    Par défaut
    avec un catch par référence, tu peux aussi sous classer std::runtime_error sans difficulté, pour une sémantique accrue.

    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);Dans les deux cas, tu écris le mot if, tu écris la condition. Et le code effectif contient bien une conditionnelle, que ce soit if, for/break ou while/break.
    Par contre, l'un des deux est une macro, donc ne vérifie pas le type de cond, et surtout pas ceux des arguments, provoque des messages d'erreurs moins explicite, ne tient pas compte des namespaces.
    Mes principes de bases du codeur qui veut pouvoir dormir:
    • Une variable de moins est une source d'erreur en moins.
    • Un pointeur de moins est une montagne d'erreurs en moins.
    • Un copier-coller, ça doit se justifier... Deux, c'est un de trop.
    • jamais signifie "sauf si j'ai passé trois jours à prouver que je peux".
    • La plus sotte des questions est celle qu'on ne pose pas.
    Pour faire des graphes, essayez yEd.
    le ter nel est le titre porté par un de mes personnages de jeu de rôle

  3. #23
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 614
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 614
    Points : 30 626
    Points
    30 626
    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

  4. #24
    Expert éminent sénior

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

    Informations professionnelles :
    Activité : pdg

    Informations forums :
    Inscription : Juin 2003
    Messages : 5 750
    Points : 10 667
    Points
    10 667
    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

  5. #25
    Expert éminent sénior

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

    Informations professionnelles :
    Activité : pdg

    Informations forums :
    Inscription : Juin 2003
    Messages : 5 750
    Points : 10 667
    Points
    10 667
    Billets dans le blog
    3
    Par défaut
    Citation Envoyé par ternel Voir le message
    avec un catch par référence, tu peux aussi sous classer std::runtime_error sans difficulté, pour une sémantique accrue.
    Au final c'est ce que j'ai fait :

    Code cpp : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    namespace core {
        class recoverable_error : public std::runtime_error {
        public:
            explicit recoverable_error(const std::string & message) : runtime_error(message) {
            }
        };
    }

    Citation Envoyé par Luc Hermitte Voir le message
    Plutôt que des macros, pourquoi ne pas en faire des fonctions inlines template variadiques ?
    Je percute seulement maintenant : inline n'est-il pas superflu dans le cas de template?

    Citation Envoyé par ternel Voir le message
    C'est qu'il suffirait d'un static_cast<bool>(file).
    Ta réponse m'a soufflé une solution simple et générique:

    Code cpp : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    template <typename T, typename ...Args>
    void throwIfNot(const T & t, Args&&... args) {
        throwIfNot(static_cast<bool>(t), std::forward<Args>(args)...);
    }


  6. #26
    Expert éminent
    Avatar de Pyramidev
    Homme Profil pro
    Développeur
    Inscrit en
    Avril 2016
    Messages
    1 470
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 470
    Points : 6 107
    Points
    6 107
    Par défaut
    Citation Envoyé par Aurelien.Regat-Barrel Voir le message
    Comment le code le plus externe peut-il gérer toutes les erreurs de tous les composants de façon simple et générique? Je pense que la multiplication des types d'exceptions (avec son râteliers de try...catch) va à l'encontre de cette possibilité et complexifie la gestion des erreurs. [...] Mais je veux bien voir du code qui gagne en élégance à faire ainsi.
    Après réflexion, je suis favorable à l'idée de la multiplication des types d'exception.
    Pour factoriser le code, un moyen est d'utiliser std::exception_ptr, apparu en C++11.
    Exemple :
    Code : 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
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    std::wstring messageForUser(std::exception_ptr eptr)
    {
    	assert(eptr);
    	if(!eptr) {
    		log(L"messageForUser: the parameter does not hold an exception.", Severity::Error);
    		return L"";
    	}
    	try {
    		std::rethrow_exception(eptr);
    	} catch(const my_program::fail_to_open_file_exception& e) {
    		return getMessageFromTranslationFile(cFAIL_TO_OPEN_FILE, e.filePath());
    	} catch(const std::bad_alloc& e) {
    		return getMessageFromTranslationFile(cID_OUT_OF_MEMORY_MESSAGE);
    	} catch(const external_lib::out_of_memory_exception& e) {
    		return getMessageFromTranslationFile(cID_OUT_OF_MEMORY_MESSAGE);
    	} catch(const external_lib::file_not_found_exception& e) {
    		return L"File not found: \"" + e.path() + L"\"."; // \todo Translate message "File not found".
    	// ...
    	// A lot of catch blocks
    	// ...
    	} catch(...) {
    		return L"Error!";
    	}
    }
     
    void logCaughtException(std::exception_ptr eptr)
    {
    	assert(eptr);
    	if(!eptr) {
    		log(L"logCaughtException: the parameter does not hold an exception.", Severity::Error);
    		return;
    	}
    	try {
    		std::rethrow_exception(eptr);
    	} catch(const std::exception& e) {
    		log(e.what(), Severity::Error);
    	} catch(const external_lib::exception& e) {
    		log(e.get_message(), Severity::Error);
    	} catch(...) {
    		log("Unkwown exception type!", Severity::Error);
    	}
    }
     
    void someTaskLaunchedByTheUser()
    {
    	try
    	{
    		// code
    	}
    	catch(...) // only one catch for all exceptions
    	{
    		std::exception_ptr eptr = std::current_exception();
    		logCaughtException(eptr);
    		const std::wstring msg = L"The task failed.\n\n" + messageForUser(eptr);
    		const int response = showMessageBox(msg);
    		if(response == some_lib::retry)
    			someTaskLaunchedByTheUser();
    	}
    }
    Qu'en pensez-vous ?

  7. #27
    Expert éminent sénior

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

    Informations professionnelles :
    Activité : pdg

    Informations forums :
    Inscription : Juin 2003
    Messages : 5 750
    Points : 10 667
    Points
    10 667
    Billets dans le blog
    3
    Par défaut
    Citation Envoyé par Pyramidev Voir le message
    Qu'en pensez-vous ?
    Merci pour cet exemple car c'est précisément ce que je cherche à éviter

    Mes reproches:
    - ça brise l'encapsulation (besoin de référencer toutes les external_lib : à titre d'info sur mon projet précédent il y en avait +100)
    - ça brise le principe Open/Closed (rajouter une nouvelle erreur oblige à mettre à jour le code à d'autres endroits)
    - ça scale pas (gros projet = des dizaines de types à créer, avec leurs doublons et ambiguïtés comme c'est déjà le cas dans ton exemple avec bad_alloc vs out_of_memory_exception et file_not_found_exception vs fail_to_open_file_exception)
    - ça oblige à maintenir une correspondance entre un type et une table de messages (anti DRY)
    - c'est fragile (le nouveau développeur qui débarque et qui rajoute son type d'exception ne sait pas qu'il doit mettre d'autres portions à jour)
    - y'a le fameux râtelier de try...catch, qui est en fait un switch...case déguisé

    Et tout ça est typique de ce que j'ai pu voir quand on multiplie les types d'exceptions. C'est pourquoi je suis curieux d'avoir un exemple concret de code sur comment on fait ça proprement.

    Je peux peut-être poser ma question autrement : quelle différence avec des codes de retour? (try...catch sur le type = switch case sur le code d'erreur). Car le discours théorique sur les types exceptions a tendance à m'évoquer le "faut remplacer les variables globales par des singletons parce que les design pattern c'est mieux". Certes y'a un petit plus... Mais dans le fond, ça change quoi?




    Il me semble que quand on crée un type d'exception c'est qu'on a l'intention de pouvoir traiter cette erreur spécifiquement. Sinon, à quoi bon? Dans ce cas, cela revient à utiliser un mécanisme parmi d'autres (la pile) pour associer une "callback" donnée (exception handler) à une erreur donnée (throw MonType). Sauf qu'avec les exceptions les deux portions de codes sont très éloignées, ce qui n'est pas le cas avec des approches plus modernes du genre programmation réactive (où l'erreur est propagée vers l'avant au lieu d'être remontée en arrière).

    Car le code ça bouge. Et alors : comment savoir si les catch que j'ai mis sont toujours d'actualité? Et où se trouve le code concerné? Je serais curieux de connaître le pourcentage de catch qui sont en fait du code mort... C'est à force d'avoir dû répondre (péniblement) à ces questions dans du code legacy que j'en ai conclu que la multiplication des types d'exception... sont forme de code spaghetti (ouai, carrément!)

    Les exceptions ont eu le vent en poupe dans les années 80/90. COM et les MFC on en mis partout, et dans ce contexte là, ben oui il faut faire avec vu que c'est architecturé autour de ça (d'où ma remarque sur la colonne vertébrale). Mais depuis cette époque on en est revenu il me semble (Rust et Go ont décidé de faire sans).

  8. #28
    Expert éminent sénior

    Femme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juin 2007
    Messages
    5 189
    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 189
    Points : 17 141
    Points
    17 141
    Par défaut
    Si à un endroit du code tu n'as pas pu attraper avant les erreurs de 100 lib externes, mais que tu penses pouvoir le faire, tu as un très gros problème d'architecture.
    Tu peux toujours remplacer un le contenu d'un try (pas des catchs correspondant) par l'appel d'une fonction faisant la même chose.

    Ca veut dire que tu as une fonction qui ne sait pas quoi faire d'exceptions de plus de 100 bibliothèques externes, mais dont l'appelant peut tout traiter.
    C'est étrange, non?

    Pire, ca veut dire que cette fonction fait appel directement à plus de 100 modules différents???
    Normalement, il devrait y avoir une (ou quelques) cascade de modules, chacun encapsulant dans ses erreurs les erreurs des ses modules internes.
    Si ma fonction fait une opération sur une image, c'est le module de chargement qui encapsule les exceptions de libPNG, libJPG ou libGIF.

    Un module ne devrait rien exposer d'interne, c'est la Loi de Demeter. C'est aussi vrai pour les exceptions.
    mon module de chargement d'image ne lève que des exceptions de chargement d'image. (toute en héritage d'une unique image_load_exception).
    Mon module de traitement d'image (qui utilise le précédent) encapsulera une référence sur cette dernière si elle doit être transmise plus haut.
    Mes principes de bases du codeur qui veut pouvoir dormir:
    • Une variable de moins est une source d'erreur en moins.
    • Un pointeur de moins est une montagne d'erreurs en moins.
    • Un copier-coller, ça doit se justifier... Deux, c'est un de trop.
    • jamais signifie "sauf si j'ai passé trois jours à prouver que je peux".
    • La plus sotte des questions est celle qu'on ne pose pas.
    Pour faire des graphes, essayez yEd.
    le ter nel est le titre porté par un de mes personnages de jeu de rôle

  9. #29
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 614
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 614
    Points : 30 626
    Points
    30 626
    Par défaut
    Je suis du même avis que ternel...

    A priori, j'aurais tendance à dire que le meilleur endroit pour être en mesure de traiter une exception, c'est... au niveau de la fonction appelante ou, au pire, un ou deux niveau maximum plus haut.

    Si on remonte d'avantage dans la pile d'appel, il y a neuf chances sur dix que l'on ait déjà perdu tout le contexte qui aurait pu nous permettre de résoudre le problème, et que la meilleur des choses à faire est alors... de laisser planter l'application.

    Allez, un petit exemple pour arriver à me faire comprendre :

    Imaginons que je veuilles communiquer avec un serveur distant. J'utilise une bibliothèque tierce pour tout ce qui a trait à la communication avec ce serveur distant, et je commence par... essayer de m'y connecter. Je vais donc mettre en place une classe Connection qui représentera le fait que je suis bel et bien connecté au serveur.

    Prévoyons deux cas de figures (il y en a peut être d'autres ) dans lesquels la connexion n'aurait pas pu s'effectuer :
    • time out : le serveur a été trouvé, mais n'a pas répondu dans les temps
    • not found : pour "une raison ou une autre", on n'a déjà pas pu trouver l'adresse IP du serveur.

    Comme il y a des chances pour que la bibliothèque tierce utilise son propre jeu d'exception, et que je veux m'en affranchir au maximum, mais que ce n'est pas au concept de connexion de décider de ce qu'il faut faire si la connexion échoue, je vais tout simplement mettre en place un système de "conversion" des exceptions de la bibliothèque tierce, sous une forme qui serait sans doute proche de
    Code : 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
    24
    class Connection{
    public:
        class TimeOutError : public std::runtime_error{
            /* y a sans doute des trucs sympa à rajouter ici */
       };
       class NotFoundError : public std::runtime_error{
            /* y a sans doute des trucs sympa à rajouter ici */
       }; 
        Connection(std::string const & serverUrl /*, autres paramètres éventuels */){
            try{
                /* tout ce qui peut foirer au niveau de la lib tierce */
            }
            catch(lalib::time_out & e){
                /* créer une exception de type TimeOut avec les infos recues de la lib tierce et l'envoyer
                 * (ce n'est pas au concept de connexion de s'occuper de gérer le problème !!!)
                 */
            }
            catch(lalib::not_foundt & e){
                /* créer une exception de type NotFound avec les infos recues de la lib tierce et l'envoyer
                 * (ce n'est pas au concept de connexion de s'occuper de gérer le problème !!!)
                 */
            }
        }
    };
    Là, on est au niveau le plus bas : on veut juste créer une connexion et lancer une exception qui exprime le problème si la création échoue.

    Cela signifie que l'on aura "quelque part" une fonction qui s'occupera de créer la connexion, en fournissant les paramètres "qui vont bien" pour y arriver (entre autre, l'URL du serveur).

    Cette fonction peut éventuellement gérer le cas "time out", par exemple, en attendant quelques secondes avant de retenter de créer la connexion, sous une forme qui serait proche de
    Code : 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
    24
    25
     
    /* il pourrait y avoir dans la classe qui expose cette fonction une exception "UnreachableError" indiquant que l'on n'a pas su joindre le serveur */
    Connexion UneClasseQuelconque::connectTo(std::string const & serverUrl /*, autres paramètres éventuels */){
        /* si, après 3 fois, on n'y est pas arrivé, il faut commencer à 
         * penser que le problème est ailleurs
         */ 
        static const size_t MAXTRIES{3};
        for( size_t count=0; count<MAXTRIES; count++){
           try{
              Connection conn(serverUrl /*, ... */);
              return conn;
            }
             catch(Connection::TimeOut & e){
                  /* on attend un peu, pui on peut laisser continuer la boucle */
             }
             catch(Connection::NotFound & e){
                 /* Y a pas à s'en faire : on lance une exception UnreachableError */
                 throw UnreachableError(serveurUrl);
             }
        }
        /* si on arrive ici, c'est que les trois essais sont passés  sans arriver à contacter le serveur...µ
         * on lance donc l'exception "UnreachableError"
         */
        throw UnreachableError(serverUrl);
    }
    Plus haut encore, on arrive aux éléments qui vont définir l'url du serveur et les différents paramètres nécessaires.

    Il y a principalement trois possibilités pour ce faire :
    1. ces données sont introduites par l'utilisateur
    2. ces données font partie d'une configuration "plus ou moins cachée" (dans un fichier de config ou, pourquoi pas, dans une clé de registre sous windows)

    Dans les trois cas, on peut envisager deux raisons principales (pour lesquelles il est possible de faire quelque chose) pour lesquelles le serveur n'est pas accessible :
    • il y a une erreur dans les paramètres fournis ou
    • il y a un problème "matériel" du coté du client (modem ou cable débranché, ADSL down, ...)

    Si les paramètres de connexion sont fournis par l'utilisateur, on garde toujours l'occasion d'insister : "veuillez vérifier les données de connexion, ainsi que le branchement correct de l'ordinateur à internet". Et on peut rentrer dans une boucle qui ne s'arrêtera que... quand l'utilisateur en aura vraiment marre de réessayer. Et nous pouvons d'ailleurs aussi envisager de fournir un numéro d'erreur à transmettre au help desk

    Si les paramètres de connexion sont fournis par "une configuration plus ou moins cachée", il ne servira sans doute pas à grand chose d'essayer de les relire et de réessayer de se connecter : on devra surement directement envisager un numéro d'erreur et le contact du helpdesk

    Au delà, si la connexion au serveur est indispensable, hé bien, il n'y a pas trente six solutions : l'application n'étant pas dans "un état cohérent", il faut la laisser planter.

    Et si, au bout du compte, on a le système d'exploitation qui nous affiche -- d'une manière ou d'une autre -- "L'application a planté avec l'exception UneClasseQuelconque::UnreachableError (contactez le revendeur)", ben, tant mieux... Mais, a priori, l'utilisateur aura déjà eu cette information d'une manière ou d'une autre

    Et le fait est que, si tu architecture correctement ton projet, tu devrais quasiment toujours arriver à quelque chose de semblable :
    • la fonction qui fait appel aux fonctions spécifiques d'une bibliothèque tierce sera rarement en mesure de corriger les problèmes survenus lors de l'utilisation de cette bibliothèque
    • la fonction qui fait appel à cette fonction "pont" peut -- peut-être -- prendre certaines décision concernant certains problèmes qui pourraient apparaitre.
    • Et, au pire, le traitement final, la "dernière chance" pour arriver à résoudre le problème sera tentée... dans la (les) fonction(s) qui fait (font) appel à la fonction qui appelle la fonction "pont"

    Si tu remonte plus haut dans la pile d'appels, il y a de fortes chances pour que tu aies perdu une partie beaucoup trop grande du contexte que pour arriver à apporter une solution raisonnable à ton problème

    Allez, si tu crées des modules clairement séparés, il se peut qu'il y ait une ou deux fonctions de plus dans la pile d'appel, qui pourraient être totalement incapables d'apporter le moindre début de solution au problème, mais, une fois que tu sors du module, la fonction qui te permet de "rentrer dans le module" sera la fonction de la "dernière chance" : dans l'exemple que je viens de prendre, si tu as un module "network" dans lequel se trouve la notion de connexion et un module "ihm" dans lequel se trouve le formulaire permettant à l'utilisateur d'introduire les donnée, la fonction de la dernière chance est la fonction qui... réagira à l'appui du bouton "connect".

    Toutes les fonctions du module IHM par lesquelles nous pourrions passer pour atteindre cette fonction "onConnectCklicked" ne pourront rien faire d'autre que ... de constater que la connexion n'a pas eu lieu. Et elle seront donc forcé de laisser "remonter la patate chaude" jusqu'à un point où... l'application plantera
    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

  10. #30
    Expert éminent sénior
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Février 2005
    Messages
    5 071
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : France, Val de Marne (Île de France)

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

    Informations forums :
    Inscription : Février 2005
    Messages : 5 071
    Points : 12 116
    Points
    12 116
    Par défaut
    Message à la bourre, désolé :

    Citation Envoyé par Aurelien.Regat-Barrel Voir le message
    Quand on fait une application IHM, il peut être utile de créer différents types d'exceptions (mais je reste très réservé dans le cas de bibliothèques - je détaillerai peut-être ensuite). Dans mon cas, il s'agit de programmes (micro-services) en ligne de commande destinés à tourner sur de l'embarqué ou en tant que serveur (gRPC).
    Je ne vois pas trop pourquoi les IHM ont besoin de plus de type d'exception.
    Pour les bibliothèques, soit c'est du chargement dynamique, donc plutôt une API C et pas C++, donc si une exception fuse, c'est qu'elle est "mortelle".
    Soit c'est du statique, et je ne vois pas pourquoi il devrait avoir un appauvrissement des informations envoyés par l'exception, et son type est une information capitale.
    Pour ce qui est de l'embarqué, c'est un domaine que je ne connais pas.

    Citation Envoyé par Aurelien.Regat-Barrel Voir le message
    Du coup je distingue 3 familles d'erreurs:
    • erreurs courantes qui sont gérées par l'appelant (find() ne touve rien...) => code de retour classique / std::optional
    • les erreurs fatales qui entrainent la fermeture quasi immédiate du programme: pan t'es mort
    • les erreurs inattendues où on ne peut rien faire mais qui ne remettent pas en cause l'intégrité du programme => utilisation de mes "macros"


    C'est donc pour gérer des erreurs peu courantes mais non fatales que j'utilise ces "macros". Chaque erreur:
    • est susceptible d'être reportée et ignorée. Ex: le serveur échoue à traiter une requête, il catch et forward l'erreur depuis son handler ("traitement générique"), et il continue sa vie tranquillou pépère... à condition de tout bien nettoyer (RAII) => d'où mon besoin de tester automatiquement toutes les erreurs de ce type.
    • l'erreur est suceptible d'être transmise (sérialisée) par le réseau et donc d'être affichée auprès de l'utilisateur. Je gagne en simplicité à générer in-situ le message texte descriptif. On peut aussi me demander plus tard de les traduire.


    J'espère que c'est plus clair
    Ok, mais j'ai toujours du mal à comprendre comment un code qui détecte une erreur peut savoir si elle est fatale ou "anecdotique".
    Pour moi, ce n'est pas à ce code de choisir, il ne connait pas les méthodes de reprise sur erreur mise en place par le code appelant.

    Citation Envoyé par Aurelien.Regat-Barrel Voir le message
    Par commodité j'ai choisi d'utiliser std::runtime_error pour gérer ces erreurs. Peut-être que c'est pas une bonne idée et que je devrais créer un type spécifique? A voir.
    Si c'est une "runtime_error", pourquoi pas, mais je ne pense pas qu'un fichier de configuration mal formaté soit une "runtime_error", par exemple.

    Citation Envoyé par Aurelien.Regat-Barrel Voir le message
    Ce qui est sûr c'est que pour ASSERT_ALWAYS j'ai fait l'erreur de la mapper sur ces "macros" alors qu'elle est utilisée dans un contexte d'erreur fatale (par rapport à assert, ASSERT_ALWAYS reste en debug). Du coup je vais la conserver comme à l'origine : appel de std::abort : pan t'es mort!
    Toujours pareil, pour moi, ce n'est pas à ce code de choisir, ...
    "assert", c'est pour des erreurs de codage, non-respect des invariants, etc..., là, oui le code les détectant sait, et il fait en sorte qu'on l'écoute.


    Citation Envoyé par Aurelien.Regat-Barrel Voir le message
    Au passage, rapellons que si y'a pas de catch{} qui englobe un throw, alors le dépilage de la pile est facultatif du point de vue de la norme et donc zappé par certains compilos (bye bye le RAII!). Faut donc bien savoir ce qu'on fait quand on ne met pas de catch dans son main()!
    Oui, et c'est ça qui est cool avec un coredump, on n'est pas pollué par des actions "parasites".

    Y a pas de coredump en embarqué ?

    Citation Envoyé par Aurelien.Regat-Barrel Voir le message
    Code cpp : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    namespace core {
        class recoverable_error : public std::runtime_error {
        public:
            explicit recoverable_error(const std::string & message) : runtime_error(message) {
            }
        };
    }
    Comme d'hab, le lanceur de l'exception ne sait pas si elle "recoverable" ou pas.

    EDIT : 100% avec @koala01 et @ternel, comme d'hab.

  11. #31
    Expert éminent sénior

    Femme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juin 2007
    Messages
    5 189
    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 189
    Points : 17 141
    Points
    17 141
    Par défaut
    Citation Envoyé par bacelar Voir le message
    EDIT : 100% avec @koala01 et @ternel, comme d'hab.
    Que veux-tu, j'ai aussi appris en lisant du Koala01, du Médinoc, du Bacelar, du Bktero et du Bousk... Forcément, ça marque
    Mes principes de bases du codeur qui veut pouvoir dormir:
    • Une variable de moins est une source d'erreur en moins.
    • Un pointeur de moins est une montagne d'erreurs en moins.
    • Un copier-coller, ça doit se justifier... Deux, c'est un de trop.
    • jamais signifie "sauf si j'ai passé trois jours à prouver que je peux".
    • La plus sotte des questions est celle qu'on ne pose pas.
    Pour faire des graphes, essayez yEd.
    le ter nel est le titre porté par un de mes personnages de jeu de rôle

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