Partage
  • Partager sur Facebook
  • Partager sur Twitter

débat sur Try Catch

    30 décembre 2022 à 11:04:14

    Bonjour !

    Pour ne pas polluer le topic d'en dessous qui parle d'un soucis de try catch, je lance un sujet propre.

    C'est juste une discussion sur l'utilisation du try catch. Sur vos styles de programmation. 

    Dans quel contexte utilisez vous cela ? 

    Pour ma part, je n'utilise quasiment jamais try-catch. Etant 90% du temps en debug (debuggueur activé), un try-catch ne me montre pas un plantage franc, m'amène hors du problème.

    Alors bien sûr j'apprécie les exceptions de la stl (un débordement de vector), parce que je sais que c'est un outil de confiance "sans bugs" et que si je me prends une exception, ça viendra de mon coté.

    Mais je suis resté très "C", a placer des assert pour faire des vérifications supplémentaires en debug, avoir du code protégé sous debug qui fait des garde fous, et retourner des codes d'erreur quand une fonction foire, mais parce que l'appelante lui a donné n'importe quoi, et non pas parce qu'elle est bugguée. 

    J'entends l'argument que je vais avoir, qu'au lieu de remonter des codes d'erreurs sur 10 fonctions, jeter une exception remonte tout d'un coup. Celui la oui. 

    bref, je pars un peu dans tous les sens, mais pour faire simple, êtes vous des adeptes du try-catch, ou faites vous autrement ?

    • Partager sur Facebook
    • Partager sur Twitter

    Recueil de code C et C++  http://fvirtman.free.fr/recueil/index.html

      30 décembre 2022 à 11:53:28

      Je n'utilise un try / catch que lorsqu'il y a quelque chose à faire en cas d'exception. Plus le traitement de l'erreur est éloignée de celui qui lance l'exception, moins cela est pertinent. De ce fait, j'ai très peu try/catch par rapport au nombre de ligne.

      > Pour ma part, je n'utilise quasiment jamais try-catch. Etant 90% du temps en debug (debuggueur activé), un try-catch ne me montre pas un plantage franc, m'amène hors du problème.

      Je pense que tu confonds programmation défensive et programmation par contrat: https://luchermitte.github.io/blog/2014/05/24/programmation-par-contrat-un-peu-de-theorie/

      > Alors bien sûr j'apprécie les exceptions de la stl (un débordement de vector)

      Si tu parles de vector::at(), c'est de la programmation défensive et de mon point de vu, c'est une mauvaise chose. En plus, le débug de la STL active les assertions sur l'opérateur [].

      -
      Edité par jo_link_noir 31 décembre 2022 à 12:36:41

      • Partager sur Facebook
      • Partager sur Twitter
        30 décembre 2022 à 12:15:22

        C'est ce que j'attends de la STL, qu'elle m'arrête immédiatement si j'ai lancé un débordement. 

        Mon idée n'est pas de dire" ok s'il y a une exception alors je vais faire autrement". C'est "arrête moi et je corrige, pas question d'aller plus loin c'est foireux".

        • Partager sur Facebook
        • Partager sur Twitter

        Recueil de code C et C++  http://fvirtman.free.fr/recueil/index.html

          30 décembre 2022 à 15:58:24

          Les assertions, c'est bien, mais ca ne fait pas tout.

          Les exceptions, pour ma part:
          - Soit on peut corriger le tir (brancher sur un fonctionnement dit dégradé) et on prend en charge.
          - Soit on ne peut pas, et on laisse l'exception remonter la pile des appels.

          Quand à savoir que faire des exceptions qui remontent jusqu'au système d'exploitation, c'est plus une question de cosmétique qu'autre chose (substituer le message d'erreur standard par quelque chose de plus joli) qui n'a de place qu'en toute fin de projet.

          • Partager sur Facebook
          • Partager sur Twitter
            31 décembre 2022 à 11:51:52

            Salut,

            Pour ma part, je crois que, avant même de parler de quand on utilise try ... catch, il faudrait parler de quand on utilise ce qui le rend nécessaire: les exceptions.

            En effet, les exceptions ne devraient être utilisées, de base, que lorsque tu t'attends à ce qu'une chose sur laquelle tu n'as absolument aucun contrôle en tant que développeur de ton projet risque de foirer, par exemple:

            • si l'initialisation d'une fonctionnalité "externe" (OpenGl, Vulkan, ou autre) échoue
            • si un fichier n'existe pas ou ne peut pas être ouvert lorsqu'on en a besoin
            • si le contenu d'un fichier ne correspond à ce que tu t'attends à trouver dedans
            • si une ressource "externe" (disponible sur un serveur quelconque) n'est pas accessible, quelle qu'en soit la raison
            • j'en passe, et sans doute de meilleures.

            Ce n'est qu'à partir de là que nous pourrons commencer à nous poser la question de quand nous allons utiliser le try...catch, car il y a -- en gros -- trois raisons majeures pour le faire, à savoir:

            • Parce que tu veux relancer du processus qui a foiré "depuis le début" (depuis la fonction qui a appelé la fonction qui a appelé ... la fonction où le problème est survenu), peut-être après avoir laissé passer un certain délais
            • parce que tu veux donner à l'utilisateur du projet la possibilité d'abandonner le processus fautif et de partir sur autre chose sans forcément perdre tout le travail effectué depuis la dernière sauvegarde
            • parce que tu veux afficher un message "compréhensible" sur la raison pour laquelle le programme plante à l'utilisateur.

            A savoir que, dans le cas de la troisième raison indiquée, ce que tu feras sera sans doute de ... relancer l'exception une fois que tu te sera assuré que l'utilisateur a bel et bien pris connaissance du message (mais au fait, est ce que cela change quelque chose????)

            J'expliquerai un peu plus loin la raison pour laquelle je réduis si fortement les raisons pour utiliser les exceptions, pour ceux qui ne connaitraient pas encore mon avis sur le sujet, mais, en attendant, je tiens à attirer l'attention sur la manière d'utiliser try catch "correctement".

            Car la première manière dont on apprend à utiliser try catch est souvent ... la manière la plus inutile et sans doute la plus dangereuse qui soit, à savoir d'avoir un code proche de

            try{
               // le code qui risque de lancer une exception
            }
            catch ( ... ){
                std::cout<<"oups, quelque chose s'est mal passé\n";
            }

            Si je la qualifie d'inutile, c'est parce que cette manière ne nous permet absolument pas de déterminer ce qui quelle partie du programme a effectivement foiré, vu que cela peut être ... n'importe quelle partie appelée de manière directe ou indirecte par les fonction se trouvant dans le bloc try.  Autant dire, à peu près tout et n'importe quoi, surtout si le bloc try catch se trouve dans la fonction main.

            Et si je qualifie cette manière de dangereuse, c'est parce que, tel qu'il apparait ici, ce code ne traite absolument pas l'exceptions: il n'essaye pas de savoir pourquoi le problème est survenu, et encore moins d'y apporter une solution.  Tout ce qu'il fait, c'est ... d'afficher un joli message à l'utilisateur qui ... au mieux, ne saura absolument pas comment y réagir, et, au pire, s'en foutra royalement.

            Autrement dit, le problème qui a provoqué le lancement d'une exception est purement et simplement ignoré, en mode "on l'oublie et on passe à autre chose".

            Bien sur, si la seule instruction qui suit a pour résultat de rendre la main au système d'exploitation, cela n'est peut être "pas bien grave", mais, si ce n'est pas le cas -- et les raisons pour qu'il en aille autrement sont nombreuses -- la dangerosité de la manoeuvre vient du fait que, après que l'exception soit survenue, le programme risque de continuer à fonctionner en "mode dégradé" avec des conséquences impossibles à prévoir.

            Il y a donc, à mon sens, deux grandes situations à prendre en compte:

            • Soit l'exception provoquera le plantage irrémédiable du programme, et dans ce cas, on peut effectivement se contenter d'afficher un message à l'utilisateur
            • Soit le programme est censé continuer à fonctionner après que l'exception ait été lancée, et, dans ce cas, l'exception doit être gérée correctement dans le but d'y apporter une solution qui n'impliquerait pas que le programme continue à fonctionner en "mode dégradé"

            Si on peut, dans le premier cas, s'en foutre et laisser planter le programme (il faudra cependant peut-être relancer l'exception pour que le programme plante effectivement), dans le second cas, il faut -- ad minima -- essayer d'apporter une solution au problème et, si on n'arrive pas à le résoudre, veiller à relancer l'exception pour qu'une autre partie de solution puisse être tentée "ailleurs" ou pour ... que le programme finisse par planter parce qu'aucune solution complète n'a pu être apportée.

            Au final, la réponse à la question de savoir où utiliser try... catch, la réponse est sans doute "à l'endroit où il y a moyen d'apporter (ne serait-ce qu'une partie de) la solution au problème qui a provoqué le lancement de l'exception".

            Mais cette réponse est bien moins importante que celle à la deuxième question qui est "comment utiliser try ... catch", car la réponse est "en veillant à récupérer les exceptions d'une manière la plus précise que possible de manière à être en mesure d'apporter une (partie de la) solution au problème qui a provoqué le lancement de l'exception"

            L'idéal sera donc d'avoir une batterie d'exceptions assez importante que pour permettre de distinguer l'ensemble des situations sur lesquels nous n'avons en tant que développeur de notre projet, aucun contrôle afin d'être en mesure d'y apporter la "solution adéquate".

            Et, dans tous les cas, l'idéal est de relancer l'exception si aucune solution complète au problème n'a pu être apportée, de manière à ce que le reste de la solution puisse venir de "plus loin" par rapport à l'endroit où l'exception est lancée, quitte à ce que cela fasse planter le programme parce que la solution n'est, justement, pas encore complète au niveau de la fonction principale.

            Après, nous sommes bien d'accord qu'il y a forcément des projets pour lesquels il soit parfaitement inconcevable de laisser planter le programme et que tout ce que j'écris ici n'est pas forcément adapté à ce genre de situations ;)

            Quant à savoir comment traiter les "autres problèmes", ceux qui n'entre justement pas dans cette catégorie de "problèmes contre lesquels il est impossible de se prémunir, que l'on espère qu'ils ne surviendront jamais mais qu'on ne peut ignorer pour la cause" et qui justifie le lancement d'une exception, il faut partir du principe que, si problème il y a, l'origine du problème se trouve systématiquement dans le code que nous (ou notre équipe) avons écrit, et donc, qu'il s'agit -- en gros -- toujours d'un problème de logique.

            En effet, on peut partir du principe que tout le code que l'on écrit a forcément pour but de fournir un résultat "prédictible" et "reproductible":

            • prédictible voulant dans ce cas dire que, si on donne une série de donnée particulière à notre fonctionnalité, nous devrions -- si nous avions peut-être un peu de temps à perdre pour le faire -- être en mesure de déterminer très précisément le résultat qui obtenu "par nous même" ... "avant même" que le programme ne nous fournisse le résultat
            • reproductible voulant dire que, si on exécute deux (ou plusieurs) fois le même comportement avec exactement les mêmes données, nous devrions obtenir à chaque fois exactement le même résultat.

            Cest ce qui nous permet de définir les règles de la programmation dite "par contrat", à savoir (en simplifié) : "Si tu me donnes des données valides et cohérentes, tu obtiendras exactement le résultat que tu espérais (par contre, dans le cas contraire, tout et n'importe quoi peut arriver)".

            Et si je parle d'un problème de logique exclusivement, c'est parce que, en gros, il y a deux grosses raisons qui font qu'un comportement que l'on développe puisse ne pas fournir le résultat escompté:

            • soit parce que l'on se trompe, simplement, dans la mise en place de la logique spécifique du comportement en question, que ce soit parce que l'on intervertit deux instructions, dont une au moins modifiera le jeu de données manipulées ou parce que l'on fasse des "erreurs idiotes" comme multiplier une valeur au lieu de la diviser ou d'assigner une valeur alors que nous voulions en tester l'égalité avec une autre (par exemple, car il y en a d'autres :D )
            • soit parce que la personne qui fait appel à ce comportement fournis des données "invalides" ou "incohérentes" par rapport aux données que le comportement s'attend à manipuler, par exemple, en "laissant passer" une valeur négative à une fonction qui s'attend à ce qu'elle soit exclusivement positive (ex: une fonction de calcul de la racine carrée) ou un pointeur nul à une fonction qui s'attend à ce qu'il pointe effectivement sur une donnée valide.

            Dans les deux cas, nous nous trouvons face à un problème qui devrait en toute logique nous "sauter à la figure" en période de tests (à condition qu'il y en ait une, bien sur) et qui devrait être résolu "bien avant" que notre projet finisse effectivement en production.

            L'énorme avantage de la chose est que les vérifications qui permettent de mettre les problèmes en évidence n'ont réellement de l'intérêt que durant la période pendant laquelle le code est modifié, car, une fois que l'on sait que le problème n'a aucune raison de survenir (parce que la logique qui mène au comportement concerné est correcte), il n'y a simplement plus de raisons pour que le problème survienne.

            On peut donc se contenter de détecter les problèmes potentiels à l'aide d'assertions, qu'elles surviennent à la compilation ou à l'exécution "en mode Debug" en faisant en sorte que cette détection de problèmes soit purement et simplement ignorée "en production" (en mode "Release").

            Ainsi, nous pourrions envisager une fonction qui calcule la racine carrée d'un nombre sous une forme proche de

            double racine_carree(double value){
                 assert(value 0); /* (1) */
                double result = /* ce qu'il faut faire */
                assert(result * result == valueà; /* (2) */
                return result;
            }

            Le (1) va s'assurer que celui qui fait appel à la fonction nous fournira une valeur supérieure à zéro, et l'obligera si ce n'est pas le cas à "corriger le tir".

            Le (2) va s'assurer que la logique que nous mettons en oeuvre pour calculer la racine carrée est correcte, car, au final, en multipliant le résultat par lui-même, nous devons récupérer la valeur de départ.  Si ce n'est pas le cas, c'est ... que nous nous sommes trompé quelque part entre (1) et (2).

            Dans les deux cas, nous pouvons garantir que la fonction fournira un résultat correct si et seulement si l'appelant de la fonction met en place une logique "suffisante" que pour s'assurer que les valeurs transmises soient effectivement exclusivement positives, et donc, les vérifications n'ont "aucune raison" d'être faites à chaque exécution une fois que le programme est en production ;)

            • Partager sur Facebook
            • Partager sur Twitter
            Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs  à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
              31 décembre 2022 à 12:53:12

              Mais où trouves tu le temps d'écrire tout ça à chaque fois :lol:

              Ayant programmé longtemps en C, ton exemple sur le fichier qui ne s'ouvre pas, je le teste à l'ouverture, le code de retour d'un fopen ou un test sur le résultat d'une construction de ifstream. Pareil, une ressource externe, le m'attends à un code de retour, ou un getlasterror. Après bien sur, si la doc de la lib externe me dit que ça jette une exception, je ferai le try catch qui va bien. 

              Après, dans les programme que je fais, si ça foire, on sort. Donc on peut envisager un try catch global (que je désactive en debug) pour le client, pour que lui se prenne l'exception que ça a foiré... Et on corrige ensuite...

              • Partager sur Facebook
              • Partager sur Twitter

              Recueil de code C et C++  http://fvirtman.free.fr/recueil/index.html

                31 décembre 2022 à 15:08:01

                Fvirtman a écrit:

                Mais où trouves tu le temps d'écrire tout ça à chaque fois :lol:

                Déformation professionnelle ?
                • Partager sur Facebook
                • Partager sur Twitter
                  31 décembre 2022 à 15:26:20

                  Fvirtman a écrit:

                  Mais où trouves tu le temps d'écrire tout ça à chaque fois :lol:

                  Ben, j'ai l'habitude de "prendre le temps qu'il faut" sans le compter lorsqu'il s'agit de faire les choses qui me tiennent à coeur.

                  Il se fait que tenter d'expliquer le mieux possible certains aspects d'ordre "conceptuel" est l'une des choses qui me tiennent à coeur :D

                  Fvirtman a écrit:

                  ton exemple sur le fichier qui ne s'ouvre pas, je le teste à l'ouverture, le code de retour d'un fopen ou un test sur le résultat d'une construction de ifstream.

                  Ben oui, et c'est normal ;)

                  Le point sur lequel je voulais insister à ce niveau là, c'est que, si l'ouverture du fichier échoue, ben, toi, en tant que développeur d'une fonction qui essaye juste d'y accéder, tu es dans une situation dans laquelle:

                  • tu ne peux qu'espérer avoir effectivement accès au fichier (comprends: que le fichier existe et que le système d'exploitation te donne le droit de l'ouvrir)
                  • tu as bien conscience qu'il y a "toute une série de raisons" qui pourraient t'empêcher d'accéder à ce fichier (parce qu'il n'existe pas, ou parce que le système d'exploitation t'en refuse l'accès)
                  • le code que tu écris n'est -- a priori -- en rien responsable du fait que tu n'as pas accès au fichier
                  • Tu ne sais -- a priori -- "pas faire grand chose" pour résoudre le problème, hormis le faire remonter à "la personne responsable" de la présence du fichier et des droits d'accès qui sont accordés dessus et attendre que le problème soit résolu.

                  Et le fait est que l'impossibilité d'accéder à ce fichier va mettre ton programme dans un état incohérent, ad minima au niveau de la fonction que tu écris.

                  Nous sommes donc bien d'accord sur le fait que tout ce qui dépend de l'accès au fichier n'aura absolument aucune chance de réussir, et donc qu'il faut ** forcément ** abandonner l'ensemble du processus qui dépend de cet accès.

                  Il est donc parfaitement cohérent de lancer une exception afin de sortir de ce processus car on ne sait rien faire pour empêcher le problème de se produire et, si solution au problème il y a, il ne m'appartient visiblement pas de l'apporter alors que je suis occupé à développer la fonctionnalité qui tente d'accéder au fichier.

                  Attention, je ne dis pas que le lancement d'une exception est "la solution idéale" face à ce genre de problème.  Je dis juste que c'est "une des possibilités" qui nous sont offertes de " prendre le problème en compte" et de "lui faire remonter la pile d'appels" tout aussi valable que l'importe quelle autre possibilité qui nous est offerte.

                  Comprends bien que, à ce moment précis, je ne parle même pas encore de faire un try... catch.  Je parle simplement de lancer une exception (potentiellement "de ton propre cru") lorsque tu te trouves effectivement dans une situation dans laquelle tu serais dans l'impossibilité d'accéder à un des fichiers dont tu as besoin ;)

                  Car le try... catch n'a absolument rien à faire dans l'histoire: s'il y a une bonne raison pour en placer un, ce sera le long du cheminement qui a, effectivement, mené l'application à faire appel à la fonction que tu développe ;)

                  Après, on peut se poser sans fin la question de savoir s'il vaut mieux laisser planter le programme lorsqu'une exception est lancée ou s'il est cohérent d'intercepter cette exception, ou même à quel endroit il sera le plus intéressant de l'intercepter, et, par conséquent, de ce qu'il y a lieu de faire lorsque l'exception est interceptée.

                  Et, bien sur, tout cela va dépendre du programme en lui-même et des circonstances.

                  Quoi qu'il en soit, si une exception est interceptée, on peut partir du principe que:

                  • si elle est interceptée, c'est que nous sommes en mesure d'apporter au moins "un début de solution" au problème qui se pose (ex: détruire correctement une connexion réseau qui ne répond plus)
                  • si l'ensemble du problème n'est pas résolu lorsqu'une exception est interceptée, elle doit être relancée
                  • dans "le pire des cas", si un problème ayant justifié le lancement d'une exception n'est pas résolu au niveau de la fonction principale, le programme mérite très clairement de planter.
                  • Partager sur Facebook
                  • Partager sur Twitter
                  Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs  à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
                    31 décembre 2022 à 15:56:33

                    Bien sur, nous sommes tout à fait d'accord sur le concept d'exception (si je reprends l'exemple de l'ouverture du fichier), de cas ou ça peut foirer, et il y a donc soit le retour par code d'erreur (ce qui est le cas ici et que je ne conteste absolument pas, au contraire !), ou alors passer par try catch. 

                    Et j'ai pour ma part l'habitude de retourner des codes d'erreur. 

                    Mais toi du coup, mets tu des throw dans des codes pour générer des exceptions, ou bien renvoies tu des messages d'erreur aussi ? 

                    • Partager sur Facebook
                    • Partager sur Twitter

                    Recueil de code C et C++  http://fvirtman.free.fr/recueil/index.html

                      31 décembre 2022 à 16:33:12

                      Fvirtman a écrit:

                      Mais toi du coup, mets tu des throw dans des codes pour générer des exceptions, ou bien renvoies tu des messages d'erreur aussi ? 


                      Pour ma part, c'est une question de cohérence.

                      Soit on travaille par code de retour, soit par exception (rien n'interdit de "convertir" un code de retour en exception et vice versa).
                      Mais un mix des 2 ..... c'est bordélique.

                      Exemple:
                      Je doit utiliser une API C, qui travaille par code de retour, mais je préfère les exceptions.
                      J'encapsule l'API dans des classes (ou fonctions) qui jettent des exceptions.

                      • Partager sur Facebook
                      • Partager sur Twitter
                        31 décembre 2022 à 16:58:50

                        Je comprends bien.

                        Et sans lib externe ? Un code from scratch, vous êtes plutôt try-catch ou codes de retour ?

                        • Partager sur Facebook
                        • Partager sur Twitter

                        Recueil de code C et C++  http://fvirtman.free.fr/recueil/index.html

                          31 décembre 2022 à 19:35:40

                          Fvirtman a écrit:

                          Mais toi du coup, mets tu des throw dans des codes pour générer des exceptions, ou bien renvoies tu des messages d'erreur aussi ? 

                          Oui, bien sur... Le but est "d'arrêter le massacre le plus tôt possible" ;)

                          Si je suis dans une situation dans laquelle le lancement d'une exception représente la solution la plus adaptée pour arrêter le massacre, il est clair que j'aurai un throw some_exception dans mon code dés le moment où j'ai la capacité de me rendre compte que la fonction n'est pas en mesure (pour une raison qui ne dépend pas du code que j'écris) de rendre le service que l'on attend de sa part.

                          Reprenons l'exemple de l'accès (en lecture) d'un fichier.

                          Nous allons, typiquement, avoir trois grandes étapes à suivre:

                          • être en mesure d'ouvrir le fichier: il faut pour cela qu'il existe à l'emplacement indiqué et que le système d'exploitation nous autorise à y accéder
                          • être en mesure d'extraire le contenu du fichier, quitte à le charger en mémoire sans essayer de l'analyser: il faut pour cela disposer d'assez de mémoire que pour pouvoir le faire
                          • analyser le contenu chargé en mémoire afin de générer les données que nous allons manipuler: il faut pour cela que le forma du fichier corresponde et que le système d'exploitation accepte de nous allouer de la mémoire en suffisance pour représenter l'ensemble des données.

                          Nous avons donc cinq situations de bases pour lesquelles nous savons que "quelque chose peu clocher" sans qu'il ne soit possible de faire quoi que ce soit pour l'éviter, vu quelles ne dépendent absolument pas de nous.

                          Et chacune de ces situations -- mieux: chacun des cas de mon code dans lesquels une de ces situations pourrait survenir -- justifie le lancement d'une exception ;)

                          Fvirtman a écrit:

                          Et sans lib externe ? Un code from scratch, vous êtes plutôt try-catch ou codes de retour ?

                          (bon, je sais... la question s'adressait d'avantage à Deedolith :D)

                          Ben, ca dépend de ce que je vais faire, de la logique que je prévois pour l'utilisation de ce que je développe.

                          Si j'estime que je suis dans une situation dans laquelle le comportement de la fonction appelante directe (comprends: sans remonter de plus d'un cran dans la pile d'appels) est susceptible d'être modifié par le résultat de ma fonction, je m'orienterai sans doute vers un code de retour, voir, pourquoi pas, vers le renvoi d'un simple booléen destiner à en indiquer la réussite ou l'échec.

                          De même, je choisirai le code de retour si j'estime que l'échec de ma fonction ne  risque a priori pas de placer l'application dans un état inconsistant.

                          Par exemple, si je venais (vas donc savoir pourquoi) à recoder une fonction comme printf, je pourrais partir du principe que, bien que cela pourrait être embêtant pour l'utilisateur, cela ne mettra pas le fonctionnement de mon projet en péril si, par malheur, l'affichage ne se faisait pas correctement.

                          Je partirais donc sans doute sur un code de retour dans ce cas, simplement parce que je m'en fous royalement de savoir si l'appelant de ma fonction le récupère ou non, et que je m'en fous royalement de savoir ce qu'il en fait une fois qu'il l'a récupéré: cela ne changera de toute facons rien au fonctionnement de l'application.

                          Par contre, si j'ai "de bonnes raisons de penser" que l'échec de ma fonction *** risque *** d'avoir des répercussions sur le fonctionnement globales de l'application (tu remarqueras que c'est une phrase où les "peut-être sont rois :D ), alors je choisirai plutôt de lancer une exception pour la simple et bonne raison que je n'ai "aucun contrôle" sur le chemin d'exécution qui a mené à l'exécution de la fonction et que je n'ai donc aucune idée ni de l'origine du problème, ni de l'endroit dans le chemin d'appels où le problème cesse ... de poser problème.

                          En gros, lorsque je développe une fonction je suis "dans la bulle de cette fonction", dans le sens où je vais partir du principe que la seule chose sur laquelle je puisse influer est... le code de la fonction elle-même.

                          Si ce code dépend de l'appel d'une fonction qui pose problème, ce n'est (sans doute)pas à moi d'y apporter une solution du moins si les données que je lui passe sont correctes, et, de même, si ma fonction reçoit des données "valides et cohérentes", je dois m'assurer qu'elle fonctionne correctement (c'est ce que l'on attends de ma part), mais ce n'est sans doute pas à moi de gérer les conséquences d'un mauvais fonctionnement de ma fonction ... pour autant que sa logique est correcte et que les données qu'elle a tenté de manipuler aient été "valides et cohérentes" ;)

                          Et, dans ce cas, ben, si le lancement d'une exception représente une solution viable et non contestée, je choisirai sans doute de lancer une exception ;)

                          Au final, comme le disait Deedolith plus haut, le tout est une question de cohérence, car il se peut très bien que certaines fonctions lancent des exceptions car l'idée est clairement que "on est sans doute mieux de laisser planter le programme si ca foire" alors que d'autres fonctions du même projet se contenteront d'un code de retour parce que leur échec ou leur réussite n'aura que des "conséquence très localisées" sur le comportement du programme ;)

                          • Partager sur Facebook
                          • Partager sur Twitter
                          Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs  à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
                            31 décembre 2022 à 20:45:21

                            J’utilise Qt, donc pas de try-catch du tout.

                            Les alternatives utilisees sont de retourner un type d’erreur (code entier, un type Error generique ou un type specifique BiduleError) ou d’avoir un objet invalide (par exemple le type Bidule qui a une fonction load pour lire un fichier va mettre l’etat de Bidule dans Invalid plutot que d’avoir load qui retourne une erreur).

                            Fvirtman a écrit:

                            Etant 90% du temps en debug (debuggueur activé), un try-catch ne me montre pas un plantage franc, m'amène hors du problème.

                            Tu peux stopper sur un throw. Ca permet parfois de remonter a la source d’une exception, quand c’est la gestion de l’exception qui est bugge.

                            koala01 a écrit:

                            Ainsi, nous pourrions envisager une fonction qui calcule la racine carrée d'un nombre sous une forme proche de

                            double racine_carree(double value){
                                 assert(value 0); /* (1) */
                                double result = /* ce qu'il faut faire */
                                assert(result * result == valueà; /* (2) */
                                return result;
                            }
                            En pratique, j’ai jamais vu ce type d’approche en systematique. Parce que Ca pollue pas mal le code (on prefere des tests externes) et c’est pas applicable des que le code est un peu plus complexe.
                            • Partager sur Facebook
                            • Partager sur Twitter
                              31 décembre 2022 à 20:54:13

                              Ca dépend du projet sur lequel je travail.

                              Sur un nouveau projet, je privilégierai les exceptions.

                              Sur un projet existant, je me plie aux contraintes de l'existant.

                              • Partager sur Facebook
                              • Partager sur Twitter
                                31 décembre 2022 à 22:03:17

                                gbdivers a écrit:

                                En pratique, j’ai jamais vu ce type d’approche en systematique. Parce que Ca pollue pas mal le code (on prefere des tests externes) et c’est pas applicable des que le code est un peu plus complexe.

                                Je sais bien ... le plus souvent, on limitera les assertions au paramètres de la fonction (et ceux qui ne le font pas devraient s'y mettre :D)

                                C'est bien la raison pour laquelle j'ai écrit "on pourrait envisager un code comme", car "conceptuellement parlant" (avec tout ce que cela peut vouloir dire), il semblerait "cohérent" de s'assurer du bon fonctionnement de la fonction à coup d'assertion vu que, si la fonction ne se comporte pas comme "il faut", c'est qu'il est nécessaire d'en corriger le code.

                                Maintenant, il est tout à fait vrai que c'est quelque chose qui ne se fait jamais dans le code de production.  Cependant, si tu y réfléchis un tout petit peu, le résultat obtenu avec tes tests unitaire est -- a priori -- très proche de celui que tu obtiendrais en parsemant ton code d'assertions ;)

                                Les trois seules grosse différences étant que

                                • tu pollues beaucoup moins ton code de prod, ce qui est une bonne chose
                                • les tests unitaires ne vont pas -- forcément -- s'arrêter sur la première erreur rencontrée, ce qui est un avantage certain (on peut corriger plusieurs problèmes après une seule exécution des tests)
                                • la séparation entre le code de prod et les assertions donne à boire et à manger, car cela implique:
                                  • que l'existence des assertion n'est pas forcément acquise (ben oui... le code de prod peut très bien fonctionner sans les tests unitaires, et, sans tests unitaires, pas d'assertions ...)
                                  • Si elles existent (nous pouvons partir du principe qu'il y ait "de grandes chances pour que ce soit le cas" :D ) il n'y a encore aucune garantie qu'elles seront effectivement évaluées (comprends: que les tests unitaires seront exécutés)

                                N'allez pas croire, à la lecture de ceci, que je suis un opposant farouche aux tests unitaires. Bien au contraire, je trouve le principe absolument génial, surtout lorsqu'il est correctement mis en oeuvre, Mon intégrité intellectuelle m'oblige simplement à reconnaitre que ... de grands avantages vont souvent de pair avec une série d'inconvénient presque aussi grands, ce qui est le cas des tests unitaires.

                                Je voulais juste pointer du doigt le fait que le principe peut être le même alors que les formes sont fondamentalement différentes ;)

                                • Partager sur Facebook
                                • Partager sur Twitter
                                Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs  à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
                                  1 janvier 2023 à 1:24:02

                                  koala01 a écrit:

                                  très proche de celui que tu obtiendrais en parsemant ton code d'assertions

                                  Le probleme est de savoir sur quelles valeurs vont etre applique les assert. Si tu veux repondre a la question (pour reprendre ton exemple de fonction qui calcule une racine carree) “est ce que ma fonction est valide pour 0, pour 1, pour -1, pour d’autres valeurs”, tu ne peux pas repondre a cela avec juste des assertions. Il faut ecrire des tests, d’une forme ou d’une autre.

                                  Dit autrement, on ne peut pas se passer des tests pour repondre a ces questions (mais oui, comme tu l’as dit, on peut se passer des tests. Mais ca veut juste dire qu’on n’apporte aucune garantie sur ces questions, pas que les assertions y repondent).

                                  J’ajoute que l’on peut se passer des assertions (pour les preconditions en faisant un wide contract, pour les post conditions par des tests). L’inverse n’est pas vrai.

                                  • Partager sur Facebook
                                  • Partager sur Twitter
                                    1 janvier 2023 à 11:41:58

                                    L

                                    gbdivers a écrit:

                                    Le probleme est de savoir sur quelles valeurs vont etre applique les assert. Si tu veux repondre a la question (pour reprendre ton exemple de fonction qui calcule une racine carree) “est ce que ma fonction est valide pour 0, pour 1, pour -1, pour d’autres valeurs”, tu ne peux pas repondre a cela avec juste des assertions. Il faut écrire des tests, d’une forme ou d’une autre.


                                    Au contraire, quand tu fais des tests, tu es censé donner des valeurs valides. Si tu donnes une valeur invalide, ton test n'est pas censé donné une réponse fausse, il est censé ... ne pas fonctionner du tout. Si tu donnes 0 ou -1 à la racine carrée, c'est comme si tu remplissait un réservoir de voiture d'eau: le moteur n'est pas censé démarrer avec ce genre de liquide.

                                    Le problème, c'est que la valeur de départ (ou le liquide mis dans le réservoir), elle est fournie par l'appelant. Et c'est donc au contrat de s'assurer des validités de valeurs en précondition.

                                    gbdivers a écrit:

                                    J’ajoute que l’on peut se passer des assertions (pour les preconditions en faisant un wide contract, pour les post conditions par des tests). L’inverse n’est pas vrai.

                                    Oui, tu pourrais utiliser un wide contract. Seulement, qui prendra la peine, après avoir fait appel à la fonction, de s'assurer que le résultat est une valeur valide?

                                    Pire encore: pourquoi devrait on payer en production le cout d'une vérification -- aussi minime soit-elle -- de la logique du programme alors qu'il est censé avoir été testé?

                                    Parce que, quand tu feras tes tests (en TDD, cela va de soi) pour la mise au point de ta fonction, tu vas sans doute commencer par les valeur invalides:

                                    • racine_carre(0) = NAN
                                    • racine_carre(-x) = NAN
                                    • racine_carree(NAN) = NAN

                                    puis tu vas continuer avec les valeurs "faciles"

                                    • racine_carree(1) = 1
                                    • racine_carree(4) = 2

                                    ensuite, ce sera le tour des valeurs "bien connues"

                                    • racine_carre(2) = 1,414213
                                    • racine_carre(3) = 1,73205

                                    enfin, tu terminera avec des valeurs "plus aléatoires", comme

                                    • racine_carree(3.1492) = 1,772453 (parce  que 1772453 * 1,772453 =  3.141589)

                                    et tu en déduira que ta fonction fait ce qu'on attend de sa part. 

                                    Tu oublies "simplement" que cette fonction est destinée à être appelée par quelqu'un dont on ignore tout, qui est, de base "un imbécile distrait", dont tu ne peux pas être sur qu'il testera la validité de la valeur renvoyée et donc que son erreur de logique risque de se répercuter sur tout ce qui dépend de la valeur renvoyée par racine_carree.

                                    Autrement dit, tu ne peux absolument pas garantir, avec un wide contract, que l'application ne finira pas par se retrouver dans un état incohérent.

                                    Par contre, si tu place une assertion sur les précondition, un simple assert(value >0) au début de ta fonction, tu obliges systématiquement tous ceux qui créent un chemin d'appel à la fonction s'ils ont l'audace de laisser passer une valeur invalide.

                                    Et comme cela fait planter l'application en debug, ben ils n'auront pas d'autre choix que de corriger leur logique, quelle que soit la manière dont la valeur transmise a été obtenue.

                                    Et comme la logique elle-même aura été corrigée afin d'écarter les valeurs invalides pour ce chemin d'appel particulier, il n'y a même plus la moindre raison de se payer le cout d'une vérification de validité en production. On gagne donc sur les deux tableaux ;)

                                    Bien sur, cela nous rajoute une (voire plusieurs) ligne(s) de code de vérification des préconditions.  Et encore, car, pour assurer ton wide contract, tu serais quand même obligé de prendre les cas "plus petit ou égal a 0" et "NAN" en compte.

                                    Tout ce que l'on fait donc, c'est de remplacer un test par un test identique, mais avec des conséquences différentes et largement plus intéressantes.

                                    Comprenons nous.  Je ne crache pas sur le wide contract, loin de là... Ce que je dis c'est que ce n'est simplement pas adapté lorsque l'on a besoin d'assurer une logique d'appel correcte.

                                    Et le fait est que les tests unitaires ne peuvent pas garantir que la logique d'appel est correct de par leur nature extérieure.

                                    • Partager sur Facebook
                                    • Partager sur Twitter
                                    Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs  à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
                                      1 janvier 2023 à 14:06:41

                                      koala01 a écrit:

                                      quand tu fais des tests, tu es censé donner des valeurs valides

                                      Non, un test peut tout a fait tester des valeurs invalides, pour voir le comportement de la fonction dans ces cas (par exemple qu’un assert, une exception, un code d’erreur, etc est correctement lancée)

                                      koala01 a écrit:

                                      Et c'est donc au contrat de s'assurer des validités de valeurs en précondition

                                      Le “contrat” est un concept generique, qui peut etre implicite ou explicite, par les types, des asserts, des tests, documentation, peu importe.

                                      Il ne faut pas juste penser que “contrat = assert”. Tout cela participe au contrat et chaque approche apporte des avantages et benefices.

                                      koala01 a écrit:

                                      Tu oublies "simplement"

                                      Je n’oublie rien du tout. Je sais que le design des APIs publiques, en particulier quand c’est des clients, est un sujet complexe (et qui peut faire intervenir des non devs, par exemple des product owner, des documenteurs, des testeurs, des managers, des gens du marketing, etc). Par exemple, nos libs au boulots sont build en release. Donc pas d’assert, c’est une option qu’on ne peut pas utiliser dans notre API.

                                      Ca fait des annees qu’on discute des contrats ici, les arguments que tu recites ont deja ete dit 1000 fois. Et je suis d’accord avec eux. Mais on doit faire des choix dans le monde reel, avec des contraintes qu’on ne maitrise pas forcement.

                                      • Partager sur Facebook
                                      • Partager sur Twitter
                                        1 janvier 2023 à 22:15:01

                                        Moi je suis pas très fan des exceptions, si une fonction peut échouer dans son traitement, je préfère renvoyer la raison de l'échec pour que l'appelant fasse ce qu'il veut pour gérer le problème, et on ne risque pas d'oublier de gérer le problème contrairement aux try/catch qu'on est libre de ne pas mettre. Les exceptions je trouve que c'est un peu une solution de "feignantise" quand on a pas vraiment réfléchi à quels sont les erreurs possibles, quand est-ce qu'elles peuvent se produire, etc, ce qui n'est pas forcément une mauvaise chose, par exemple si on est au début du projet et que une gestion d'erreur robuste n'est pas encore la priorité principale, ou alors si on veut juste faire un petit projet "vite fait". Aussi c'est bien pour les erreurs qu'on ne s'attend pas à pouvoir gérer, des sortes d'erreurs fatales, à ce moment là ça permet de stopper directement l'exécution. Par exemple si il n'y a pas plus de mémoire, je m'attend à me manger une std::bad_alloc et à ce que le programme n'aille pas plus loin, je vais pas m'amuser à tester individuellement le retour de chaque fonction qui alloue possiblement de la mémoire pour me demander si ça a marché.

                                        Mais sinon dans les projets plus "réfléchis" et "robustes" je préfère des retours explicites, par exemple pour une fonction qui charge le contenu d'un fichier, je préfère lui faire retourner soit le résultat si tout va bien, soit une erreur avec pourquoi pas la raison (c'est quand que tu arrive std::expected on t'attend !! (pun intended))

                                        Sinon comme a dis gbdivers il y a une autre solution que j'aime bien c'est le "error on handle" (https://twitter.com/cmuratori/status/1454293788705185795) en gros on a un handle de la ressource, par ex un fichier, et si au cours de son utilisation un problème survient, on passe le handle à "invalid", et on est sûr que tout le reste du code qui aurait voulu l'utiliser ne fera rien puisqu'il vérifie d'abord si le handle est valide. Et on peut aussi lui associer un stream d'erreurs comme ça on peut savoir ce qui s'est passé. Un exemple concret, imaginons qu'on ait un script SQL que l'utilisateur nous donne et qu'on veut l'exécuter. Ce qu'on peut faire c'est le parcourir, et dès qu'on rencontre une erreur, on l'indique dans le stream (donc pas en retournant un code d'erreur ni en lançant une exception), et au passage on marque le truc comme étant "invalide" et par la suite au moment de l'exécuter, la fonction qui se charge de ça va regarder si le truc est valide, si oui elle exécute le truc et sinon elle ne fait rien, et à la fin si on voit que le truc est invalide, on peut afficher les erreurs à l'utilisateur par exemple


                                        koala01 a écrit:

                                        Par exemple, si je venais (vas donc savoir pourquoi) à recoder une fonction comme printf

                                        Par rapport au "vas donc savoir pourquoi" : c'est pas si déconnant que ça de recoder une fonction comme printf, je suis bien contente que fmt l'ai fait :)

                                        -
                                        Edité par JadeSalina 1 janvier 2023 à 23:13:16

                                        • Partager sur Facebook
                                        • Partager sur Twitter
                                          2 janvier 2023 à 0:18:12

                                          JadeSalina a écrit:

                                          Moi je suis pas très fan des exceptions, si une fonction peut échouer dans son traitement, je préfère renvoyer la raison de l'échec pour que l'appelant fasse ce qu'il veut pour gérer le problème,

                                          Déjà, qui te dit que c'est à l'appelant direct de gérer le problème?

                                          As tu seulement conscience que certains problèmes trouvent parfois leur origine dans une fonction qui s'est correctement déroulée et qui a été appelée dix ou quinze fonction avant celle qui échoue?

                                          Comment veux tu que l'appelant direct résolve le problème, alors que tout ce dont il dispose est une donnée foireuse dont il ignore tout, que ce soit l'origine ou la raison pour laquelle elle est foireuse?

                                          JadeSalina a écrit:

                                          et on ne risque pas d'oublier de gérer le problème

                                          Ca, ca reste encore à voir... Dis moi, combien de fois as tu utilisé la fonction printf ces derniers temps? combien de fois as tu pensé à récupérer sa valeur de retour? Et combien de fois as tu pensé à vérifier si cette valeur était cohérente?

                                          Et ce qui est vrai pour printf est vrai pour une quantité phénoménale d'autres choses. Es tu vraiment prête à écrire un test qui vérifie le retour d'une fonction que de fonction que tu appelles? toi qui ne jure que par les performances, tu en arriveras vite à la conclusion que ca les plombe très sérieusement ;)

                                          Parce que contrairement aux retours d'une fonction, qu'on peut oublier de récupérer et de vérifier et qui peut donc laisser le système dans un état incohérent, une exception lancée devra forcément être rattrapée "quelque part" et s'assurera de remettre le système "en l'état" au passage.

                                          JadeSalina a écrit:

                                          Les exceptions je trouve que c'est un peu une solution de "feignantise" quand on a pas vraiment réfléchi à quels sont les erreurs possibles, quand est-ce qu'elles peuvent se produire,

                                          Ca, c'est TON point de vue (ou plutôt celui de ton gourou)...

                                          Notre point de vue à nous est plutôt de les utiliser sur les choses qui ne dépendent pas de nous, quand l'erreur produite va clairement laisser le système dans un état incohérent, et de la rattraper (avec un try cach) à l'endroit où il sera possible à la fois de récupérer la cohérence du système et d'apporter (ne serait-ce qu'une partie de) la solution au problème qui a fait échouer l'appel de la fonction.

                                          Il n'est absolument pas question ni de mettre un gros bloc try ... catch dans toutes les fonctions que l'on développe. Il n'est même pas question de laisser planter systématiquement le programme parce que l'exception sera sortie de la fonction principale.

                                          Par contre, le fait de laisser le programme planter reste une des possibilités qui peuvent être envisagées à partir du moment où les avantages qu'il y aurait à le faire planter sont plus importants que les risques qu'il y aurait à le laisser continuer son exécution

                                          JadeSalina a écrit:

                                          Sinon comme a dis gbdivers il y a une autre solution que j'aime bien c'est le "error on handle" (https://twitter.com/cmuratori/status/1454293788705185795) en gros on a un handle de la ressource, par ex un fichier, et si au cours de son utilisation un problème survient, on passe le handle à "invalid",

                                          Et tu te retrouve, de nouveau, à transmettre un truc "en plus" (ou peu s'en faut) dont il faudra tester la validité avant d'espérer pouvoir faire quelque chose avec la ressource qui t'intéresse.

                                          Ce n'est pas  ** forcément ** une mauvaise soltion, notes. C'est juste une solution qui, comme toutes les autres, ne présente un réel intérêt que dans un nombre limité de situations et qui apportera, dans les autres situations, sans doute plus d'inconvénients que de bénéfices.

                                          • Partager sur Facebook
                                          • Partager sur Twitter
                                          Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs  à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
                                            2 janvier 2023 à 7:19:31

                                            JadeSalina a écrit:

                                            Moi je suis pas très fan des exceptions, si une fonction peut échouer dans son traitement, je préfère renvoyer la raison de l'échec pour que l'appelant fasse ce qu'il veut pour gérer le problème, et on ne risque pas d'oublier de gérer le problème contrairement aux try/catch qu'on est libre de ne pas mettre. Les exceptions je trouve que c'est un peu une solution de "feignantise" quand on a pas vraiment réfléchi à quels sont les erreurs possibles, quand est-ce qu'elles peuvent se produire, etc, ce qui n'est pas forcément une mauvaise chose, par exemple si on est au début du projet et que une gestion d'erreur robuste n'est pas encore la priorité principale, ou alors si on veut juste faire un petit projet "vite fait". Aussi c'est bien pour les erreurs qu'on ne s'attend pas à pouvoir gérer, des sortes d'erreurs fatales, à ce moment là ça permet de stopper directement l'exécution. Par exemple si il n'y a pas plus de mémoire, je m'attend à me manger une std::bad_alloc et à ce que le programme n'aille pas plus loin, je vais pas m'amuser à tester individuellement le retour de chaque fonction qui alloue possiblement de la mémoire pour me demander si ça a marché.

                                            Tu n’as pas compris l’une des problematiques de la gestion des erreurs.

                                            Par exemple, si je reprend ton exemple d’un echec d’allocation memoire, peut etre que tu n’as pas envie que ton programme s’arrete brusquement, mais qu’il fasse une tentative d’enregistrement de l’etat courant, pour recuperer le travail en cours lors du prochain demarrage de l’application (comme dans Word et Excel). Ou qu’il envoie un rapport de crash pour que le bug puisse etre corrige. Ou se demerde pour nettoyer la memoire et quand meme continuer a tourner, parce que c’est un programme qui controle le moteur d’un avion avec 300 passagers.

                                            Dans de tres nombreux cas, il y a un nombre important de contextes (fonctions, classes, modules, peu importe) intermediaires entre le contexte dans lequel l’erreur apparait et le contexte qui doit traiter cette erreur. Dans des codes pas trop complexes (peu de contextes intermediaires), retourner un code d’erreur peut etre pas trop complexe a gerer. C’est ce qu’on faisait il y a 30 ans, quand les programmes etaient simples (ou quand on est dev solo sur un jeu pas fini au bout de 8 ans…).

                                            Mais plus les programmes sont devenus complexes, plus il y a des contextes intermediaires. Ce qui veut dire dans contextes qui doivent gerer ces codes d’erreur qui ne les concenents pas, ce qui allourdi fortement la gestion des erreurs (et donc on fait plus d’erreur dans le code qui sert a gerer les erreurs…). Il a fallut trouver des alternatives aux codes d’erreur pour les programmes modernes. L’un des interets des exceptions est de transmettre directement une information depuis le contexte de l’erreur vers le contexte de gestion de cette erreur, et cela peut importe le nombre d’intermediaire. (Dit autrement, avec les exceptions, tu peux ajouter un intermediaire sans devoir changer le code qui emet l’erreur ou celui qui la traite. Avec un code d’erreur, quand tu ajoutes un intermediaire, il faut potentiellement revalider l’ensemble des contextes de gestion des erreurs).

                                            Ce que tu appelles de la faineantise, c’est ce qu’on appelle de la productivite : faire les choses plus rapidement et de facon plus maintenable, pour etre efficace et pas se retrouver comme un con bloque sur un meme jeu pendant 8 ans sans le sortir.

                                            • Partager sur Facebook
                                            • Partager sur Twitter
                                              2 janvier 2023 à 13:07:56

                                              JadeSalina a écrit:

                                              Sinon comme a dis gbdivers il y a une autre solution que j'aime bien c'est le "error on handle" (https://twitter.com/cmuratori/status/1454293788705185795

                                              Malgré ton obsession de tout ramener à ce gars la, est ce que sa façon de faire n'est pas un peu comme celle de l'API Windows ?

                                              Les fonctions Windows renvoient juste un true ou false pour dire si ça a marché ou pas, puis ensuite si ce n'est pas le cas, tu appelles "getlasterror" qui t'en dit davantage. 

                                              Est ce que Casey lui ne préconise pas des fonctions qui ne renvoient.... juste rien, ou pas de code d'erreur, et tu dois lancer un getlasterror systématique pour voir si ça a foiré ? C'est pas un peu lourd comme façon de faire ? 

                                              Alors bien sur ça permet de récupérer une erreur 12 fonctions en amont et ne pas avoir a faire un if à chaque étage.... mais si tu laisses tout dérouler sans tester avant d'être en haut, tu risques de calculer plein de trucs pour rien...

                                              • Partager sur Facebook
                                              • Partager sur Twitter

                                              Recueil de code C et C++  http://fvirtman.free.fr/recueil/index.html

                                                2 janvier 2023 à 17:00:38

                                                koala01 a écrit:

                                                JadeSalina a écrit:

                                                et on ne risque pas d'oublier de gérer le problème

                                                Ca, ca reste encore à voir... Dis moi, combien de fois as tu utilisé la fonction printf ces derniers temps? combien de fois as tu pensé à récupérer sa valeur de retour? Et combien de fois as tu pensé à vérifier si cette valeur était cohérente?

                                                J'ai jamais vérifé sa valeur de retour car je m'en fiche :) Mais quand je disais qu'on ne peut pas oublier de gérer le problème c'était surtout pour les fonctions qui renvoient un std::expected ou un std::optional, on est obligé de "passer par dessus" pour accéder au résultat si il existe. Sinon on peut aussi mettre un [[nodiscard]]

                                                gbdivers a écrit:

                                                Dans des codes pas trop complexes (peu de contextes intermediaires), retourner un code d’erreur peut etre pas trop complexe a gerer. C’est ce qu’on faisait il y a 30 ans, quand les programmes etaient simples (ou quand on est dev solo sur un jeu pas fini au bout de 8 ans…).

                                                Oui voilà c'est ça que je préfère faire, c'est qu'il y ait pas trop de contextes intermédiaires pour garder les choses simples

                                                gbdivers a écrit:

                                                Par exemple, si je reprend ton exemple d’un echec d’allocation memoire, peut etre que tu n’as pas envie que ton programme s’arrete brusquement, mais qu’il fasse une tentative d’enregistrement de l’etat courant, pour recuperer le travail en cours lors du prochain demarrage de l’application (comme dans Word et Excel). Ou qu’il envoie un rapport de crash pour que le bug puisse etre corrige. Ou se demerde pour nettoyer la memoire et quand meme continuer a tourner, parce que c’est un programme qui controle le moteur d’un avion avec 300 passagers.

                                                Oui pour ce genre d'erreur qui peut se produire presque à n'importe quel moment derrière n'importe quel appel de fonction ça peut être intéressant de laisser la possibilité à l'appelant de faire ce qu'il veut avec un try/catch mais sinon pour les erreurs moins "fatales" je préfère renvoyer une erreur, par exemple si on veut lire un fichier, on peut renvoyer soit le contenu soit une erreur 

                                                gbdivers a écrit:

                                                Ce que tu appelles de la faineantise, c’est ce qu’on appelle de la productivite : faire les choses plus rapidement et de facon plus maintenable, pour etre efficace et pas se retrouver comme un con bloque sur un meme jeu pendant 8 ans sans le sortir.

                                                Mais c'est pas parce qu'on met du temps à sortir un jeu qu'on est nul, si le jeu en question c'est un MMORPG 3D open world ça se comprend que ça prenne du temps, bah je pense que lui il fait ce genre de choses hautement complexe qui nécessiterait des centaines de programmeurs donc on verra bien quand il sortira :)(HH c'est un peu hors course car c'est plus un projet éducatif et il fait ça juste en live le soir de temps en temps, c'est pas à temps plein)

                                                Fvirtman a écrit:

                                                Malgré ton obsession de tout ramener à ce gars la, est ce que sa façon de faire n'est pas un peu comme celle de l'API Windows ?

                                                Les fonctions Windows renvoient juste un true ou false pour dire si ça a marché ou pas, puis ensuite si ce n'est pas le cas, tu appelles "getlasterror" qui t'en dit davantage. 

                                                Est ce que Casey lui ne préconise pas des fonctions qui ne renvoient.... juste rien, ou pas de code d'erreur, et tu dois lancer un getlasterror systématique pour voir si ça a foiré ? C'est pas un peu lourd comme façon de faire ? 

                                                Alors bien sur ça permet de récupérer une erreur 12 fonctions en amont et ne pas avoir a faire un if à chaque étage.... mais si tu laisses tout dérouler sans tester avant d'être en haut, tu risques de calculer plein de trucs pour rien...

                                                Je sais pas exactement ce que Casey proposerait comme solution mais après c'est juste des idées comme ça il faut pas se dire que c'est ça qu'il faut faire à chaque fois, ça dépend des situations, des fois ça peut être intéressant et d'autres fois il peut y avoir une solution plus adaptée. Par exemple petite question, comment vous faites pour arrêter la visite d'une structure récursive (par exemple quand on a trouvé l'élément qui nous intéressait) ? On pourrait renvoyer vrai/faux pour savoir si on continue la visite, mais du coup au moment d'appeler chaque fils, il faut vérifier le retour et sortir si besoin, ça fait plein de tests à faire. Peut être que dans ce cas on peut juste lancer une exception qui nous fait directement remonter à la surface, ça ferait un code plus "simple" et peut être que c'est plus performant de se manger un stack unwinding occasionnellement que de faire plein de tests, pour chaque fils

                                                -
                                                Edité par JadeSalina 2 janvier 2023 à 17:01:25

                                                • Partager sur Facebook
                                                • Partager sur Twitter
                                                  2 janvier 2023 à 17:09:25

                                                  JadeSalina a écrit:

                                                  Je sais pas exactement ce que Casey proposerait comme solution mais après c'est juste des idées comme ça il faut pas se dire que c'est ça qu'il faut faire à chaque fois, ça dépend des situations, des fois ça peut être intéressant et d'autres fois il peut y avoir une solution plus adaptée. Par exemple petite question, comment vous faites pour arrêter la visite d'une structure récursive (par exemple quand on a trouvé l'élément qui nous intéressait) ? On pourrait renvoyer vrai/faux pour savoir si on continue la visite, mais du coup au moment d'appeler chaque fils, il faut vérifier le retour et sortir si besoin, ça fait plein de tests à faire. Peut être que dans ce cas on peut juste lancer une exception qui nous fait directement remonter à la surface, ça ferait un code plus "simple" et peut être que c'est plus performant de se manger un stack unwinding occasionnellement que de faire plein de tests, pour chaque fils

                                                  -
                                                  Edité par JadeSalina il y a 4 minutes


                                                  Tu fais bien de dire que l'idée existe, oui. Après tout c'est un sujet sur try/catch, donc ça parle de codes de retour, donc effectivement le concept de l'API Windows qui se gère par getlasterror, ça existe oui.

                                                  Pour le coup de la fonction récursive, je le fais assez souvent. En général c'est une méthode, je suis dans une classe, donc je peux avoir une variable de la classe "found". Et c'est juste au niveau de l'appel récursif (si le code est bien fait, ce n'est pas à plein d'endroit mais à un seul) que je test si found avant de lancer de nouveau une récursion. 

                                                  • Partager sur Facebook
                                                  • Partager sur Twitter

                                                  Recueil de code C et C++  http://fvirtman.free.fr/recueil/index.html

                                                    2 janvier 2023 à 18:00:37

                                                    JadeSalina a écrit:

                                                    J'ai jamais vérifé sa valeur de retour car je m'en fiche :)

                                                    Et c'est bien là tout le problème : une valeur de retour, on peut s'en fiche... Il a fallu attendre C++17 et l'attribut [[nodiscard]] pour que le compilateur soit capable d'émettre un avertissement si tu ne récupérais pas la valeur de retour d'une fonction qui est censée renvoyer quelque chose...

                                                    Mais, encore une fois, cela ne sert strictement à rien, n'est-ce pas?

                                                    Il est vrai que la valeur de retour de printf n'a que très peu d'intérêt (hormis pour l'utilisateur du programme, qui ne sera peut-être pas ravi que l'affichage ait foiré).

                                                    Mais il faut te dire qu'il existe une quantité phénoménale de fonction qui renvoient des valeurs qui ne sont même pas récupérées lors de l'appel.  Et le problème, c'est que pour beaucoup de ces fonctions, leur résultat -- si la fonction échoue -- risque fort de laisser le programme dans un état incohérent, avec, comme conséquence, que tout ce qui sera fait après cet appel "foireux" risque lui aussi de partir en sucette.

                                                    JadeSalina a écrit:

                                                    c'était surtout pour les fonctions qui renvoient un std::expected ou un std::optional, on est obligé de "passer par dessus" pour accéder au résultat si il existe. Sinon on peut aussi mettre un [[nodiscard]]

                                                    Mais toutes les fonctions n'ont pas forcément de bonnes raisons de renvoyer un std::expected ou un std::optional...

                                                    Pire, il y a des cas dans lesquels std::optional ne fonctionnera pas (ou très difficilement), comme le renvoi d'une référence.

                                                    Quant à [[nodiscard]] son usage n'a aucune raison d'être systématique, car il y a -- malgré tout -- aussi des fonctions dont on peut se fiche pas mal du code de retour.

                                                    JadeSalina a écrit:

                                                    Oui voilà c'est ça que je préfère faire, c'est qu'il y ait pas trop de contextes intermédiaires pour garder les choses simples

                                                    Ce que tu n'arrives pas à comprendre, c'est que tu n'as bien souvent pas le choix du nombre de contextes intermédiaires car il dépend directement de la complexité intrinsèque (la complexité que l'on ne peut pas éviter) du projet. Et, dans une certaine mesure, de la complexité ajoutée (celle que l'on aurait pu éviter en prenant d'autres décision) au projet

                                                    Le truc, c'est que tous les projets ne sont pas forcément "bouclés" en moins de cinq milles lignes de code...

                                                    • Partager sur Facebook
                                                    • Partager sur Twitter
                                                    Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs  à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
                                                      2 janvier 2023 à 18:25:36

                                                      Bonne année.

                                                      Je vois que vous vous êtes lâchés... Je vais essayé de répondre eau fil de l'eau (et en filtrant)

                                                      Premier truc, tu voudras voir l'intervention de Sean Parent de cette année. Je ne suis pas forcément d'accord avec tout. Mais il faut que je la revois, beaucoup de choses en rapport avec le débat sont abordées.

                                                      > Dans quel contexte utilisez vous cela ?

                                                      Uniquement pour traiter les anomalies extérieures et ... "prévisibles". Cela n'a pas de sens pour une erreur de programmation. Comme d'autres (Sean Parent, Joe Duffy, Herb Sutter à priori, etc), j'estime qu'une erreur de programmation n'est pas rattrapable.

                                                      Même une out-of-bounds ne peut pas l'être, car qu'est-ce qui garantit que le mauvais index passé vient bien d'une erreur idiote et pas d'une corruption de la pile? En C et en C++, rien!

                                                      >un try-catch ne me montre pas un plantage franc, m'amène hors du problème.

                                                      Grosse confusion de problématiques pour moi. Les exceptions ce n'est pas fait pour les erreurs de programmation. C'est le boulot des assertions. Jo_link_noir avait donné un lien vers mon billet de blog.

                                                      > [Code de retour & exceptions] Mais un mix des 2 ..... c'est bordélique.

                                                      Hum... Je privilégie les codes de retours/booléens/et autres monades quand je sais que le chemin à remonter jusqu'au "que fait-on si échec?" est court. Exceptions sinon.

                                                      > Non, un test peut tout a fait tester des valeurs invalides, pour voir le comportement de la fonction dans ces cas (par exemple qu’un assert, une exception, un code d’erreur, etc est correctement lancée)

                                                      Je ne comprends pas l'intérêt des death tests sur des narrow contracts. J'assimile un tel contrat à de l'UB. Et tester que l'on ne sait pas ce qui va se passer... A l'inverse, si on sait ce qui est attendu, ce n'est plus du narrow, mais du wide, car le comportement est défini.

                                                      > si une fonction peut échouer dans son traitement, je préfère renvoyer la raison de l'échec pour que l'appelant fasse

                                                      C'est pour cela que l'on préfère les exceptions aux codes de retours, et autres errno à la noix. Il n'y a guère que les variants/monades qui permettent de remonter plus d'infos.

                                                      > Les exceptions je trouve que c'est un peu une solution de "feignantise"

                                                      La vraie fainéantise, c'est les codes de retours que l'on ignore. Car un code correct à base de codes de retours a grosso modo un if toutes les 2 lignes. Si ce n'est pas le cas, il est fort probablement incorrect.

                                                      Et honnêtement, on n'a pas forcément besoin d'avoir tous les détails tout du long pour remonter que l'on ne peut pas accomplir une tâche. Ce n'est qu'aux points de reprise de la main que cela devient important. Et ces points ne sont pas si nombreux. Au milieu, on s'en moque royalement.

                                                      > on passe le handle à "invalid"

                                                      Mon expérience est que ces saletés sont des m*rd*s sans nom. Au lieu d'avoir des invariants bien propres et simples: "si ça existe je peux m'en servir", on se retrouve à la place avec des tests dans tous les sens et des machines à états pas toujours triviales.

                                                      -
                                                      Edité par lmghs 2 janvier 2023 à 21:33:41

                                                      • Partager sur Facebook
                                                      • Partager sur Twitter
                                                      C++: Blog|FAQ C++ dvpz|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS| Bons livres sur le C++| PS: Je ne réponds pas aux questions techniques par MP.

                                                      débat sur Try Catch

                                                      × Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
                                                      × Attention, ce sujet est très ancien. Le déterrer n'est pas forcément approprié. Nous te conseillons de créer un nouveau sujet pour poser ta question.
                                                      • Editeur
                                                      • Markdown