Journal Gestion de l'erreur - C++ - std::optional

Posté par (page perso) . Licence CC by-sa
Tags :
39
3
sept.
2016

Sommaire

Introduction

Ce journal discute de la gestion d'erreur lors des appels de fonction. Dans la première partie, nous discuterons plusieurs solutions de gestion d'erreur classiques rencontrée dans de nombreux languages comme C, C++, Python, etc. Dans la seconde partie, nous présenterons std::optional<T>, une nouvelle classe de la librairie standard C++17 et nous discuterons comment celle-ci change la gestion d'erreur. Pour finir je donnerai un avis personnel sur l'API de cette classe que je n'aime pas, et je proposerai une solution alternative inspirée de ce qui se fait dans d'autres langages, comme swift, rust, ocaml, haskell, etc.

Ce journal s'adresse au développeur débutant dans sa première partie. Le développeur débrouillé pourra être intéressé par la présentation de std::optional de la seconde partie. Enfin la partie finale sera sûrement le point de départ de nombreux débats (constructifs) et pourra intéresser tout le monde, mais surtout le développeur confirmé.

Ce journal est clairement MON avis sur la question, et de nombreux points sont largement subjectifs, ils sont issus de mon expérience personnelle et je vous invite à m'incendier dans les commentaires si vous n'êtes pas d'accord ;)

Note : je m'intéresse uniquement au traitement des optionals, donc j'ignore certains détails de bonnes pratiques de programmation ou d'optimisation ou de cas particuliers, ainsi inutile de troller sur le fait que certains de mes exemples n'exploitent pas la move sémantique et copient inutilement des chaînes, je sais ;)

Fonctions partielles et fonction totales

Il existe de nombreuses fonctions qui peuvent échouer, on appelle cela des fonctions partielles. Quelques exemples que nous utiliserons au cours de ce document.

  • Une fonction maximum qui donne la valeur maximum d'une liste. Que se passe-t-il si la liste est vide ?
  • Une fonction recherche qui cherche un élément dans un tableau et retourne l'indice où celui-ci se trouve. Que se passe-t-il si l'élément n'est pas dans le tableau ?
  • Une fonction lireFichier qui retourne le contenu lue dans un fichier. Que se passe-t-il si le fichier n'est pas lisible ou n'existe pas ?

Pour toutes ces fonctions, il convient de mettre en place une politique de gestion d'erreur adaptée. Nous en discuterons plusieurs :

  • La politique du contrat.
  • La méthode des valeurs sentinelles
  • La méthode du flag de réussite.
  • La méthode par exception
  • La méthode par optional.

Méthode par contrat

Cette méthode est de loin la plus simple, il suffit de placer un contrat avec l'utilisateur de la fonction disant qu'il ne doit appeler la fonction que dans un contexte où elle doit réussir.

Cette approche est utilisé par exemple en C++ pour l'opérateur operator[] d'accès aux cases d'un tableau sur un std::vector. Dans le cas où on essaye d'accéder à une mauvaise case, le comportement du programme est indéfini.

Cette méthode est très simple, mais force le développeur à s'assurer que le contrat est respecté avant l'appel de fonction. C'est souvent contre productif voir impossible. Dans le cas de la fonction recherche il faudrait parcourir une première fois la structure pour vérifier que l'élément est dedans avant d'appeler la fonction recherche pour savoir où il est. Dans le cas de la fonction lireFichier, c'est carrément impossible puisque pour savoir si on peut lire un fichier, il faut le lire, et qu'il n'y a aucune garantie qu'un fichier lue à un instant t sera lisible à l'instant t+1.

Je n'apprécie pas cette méthode car elle est source de nombreux bugs difficiles à trouver.

Méthode par valeur sentinelle

L'idée ici étant d'utiliser une valeur de retour particulière pour matérialiser l'erreur.

C'est une approche très souvent utilisée dans de nombreuses librairies. Par exemple, en C, nous avons la fonction fopen FILE *fopen(const char *path, const char *mode); chargée d'ouvrir un fichier. En cas de réussite, la fonction retourne un pointeur vers un objet utilisé par la suite pour traiter le fichier. En cas d'échec, elle retourne un pointeur NULL.

Dans le cas de la méthode recherche qui renvoie l'indice dans un tableau d'un élément recherché, on pourrait renvoyer une valeur négative, puisque les indices dans un tableau sont toujours positifs. L'usage se ferait ainsi de la manière suivante :

// prototype de la fonction
int rechercher(const Collection &c, const Item item);

//...

int offset = rechercher(maCollection, monItem);

if(offset >= 0)
{
    std::cout << "Item trouvé à la position " << offset << std::endl;
    std::cout << "La valeur de l'item est " << maCollection[offset] << std::endl;
}
else
{
    std::cout << "Item non trouvé".
}

Cette méthode ne peut cependant pas s'appliquer à tous les cas de figure. Quelle valeur sentinelle pourrait renvoyer la fonction maximum ? Celle-ci devra être garantie de ne pas pouvoir être confondue avec une valeur qui serait le vrai résultat de la fonction.

Cette méthode rend l'erreur facile, en effet, il est aisé d'oublier de tester la réussite, ainsi le code suivant, qui semble anodin, est faux :

int offset = rechercher(maCollection, monItem);

std::cout << "Item trouvé à la position " << offset << std::endl;
std::cout << "La valeur de l'item est " << maCollection[offset] << std::endl;

En effet, si l'élément n'est pas trouvé, offset vaut -1 et maCollection[offset] n'a pas de sens.

Méthode par flag de réussite

Ici la fonction vas renvoyer un flag, souvent un booléen pour matérialiser la réussite. La valeur de retour étant en fait passée par référence et modifiée.

// prototype de la fonction
bool rechercher(const Collection &c, const Item item, int &offset);

//...

int offset;
bool res = rechercher(maCollection, monItem, offset);

if(res)
{
    std::cout << "Item trouvé à la position " << offset << std::endl;
    std::cout << "La valeur de l'item est " << maCollection[offset] << std::endl;
}
else
{
    std::cout << "Item non trouvé".
}

Cette approche corrige une des limitations de la méthode par valeur sentinelle, elle peut fonctionner pour n'importe quelle fonction, puisque il n'est pas nécessaire de trouver une valeur sentinelle adaptée, la réussite étant matérialisée par le booléan. Cependant on peut toujours oublier de tester le booléan de résultat et ainsi utiliser la valeur de offset qui serait non initialisée (ou initialisée par défaut avec une valeur fausse).

Méthode par exception

Cette méthode est plus souvent utilisée dans des langages comme Python. Par exemple, la fonction de recherche d'un élément dans une liste vas soit renvoyer l'indice de l'élément, soit va lever une exception qui va remonter la pile d'appel jusqu'à être interceptée ou jusqu'à terminer le programme.

>>> # cas qui fonctionne
>>> [1,2,3].index(2)
1

>>> # cas qui renvoie une exception
>>> [1,2,3].index(4)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: 4 is not in list

La méthode par exception a de nombreux avantages :

  • Si on ne traite pas l'exception, elle finira le programme, souvent avec un message d'erreur explicite, ce qui évite les bugs dissimulés où la valeur sentinelle ou non initialisée est utilisée.
  • Le code n'est pas complexifié par une gestion d'erreur avec de nombreux if
  • La gestion d'erreur peut être repoussée à plus tard, dans une fonction appelante.

La gestion de l'exception se fait, en Python, avec un bloc try / except :

try:
   offset = maCollection.index(monItem)
   print("l'element a été trouvé, il est en position", offset, "et c'est", maCollection[offset]
except: # Normalement il ne faut pas attraper TOUTES les exceptions, mais c'est un tutoriel simplifié
   print("Élement non trouvé")

Le C++ gère aussi les exceptions, mais la librairie standard ne s'en sert pas. Pour différentes raisons que je n'aborderais pas, la communauté du C++ n'aime pas les exceptions et peu de librairie les utilisent correctement.

Cependant, rien ne force le développeur à gérer les exceptions et rien n'indique à celui-ci qu'une fonction peut lancer une exception, si ce n'est une lecture scrupuleuse de la documentation. Certains langages, comme Java, proposent un système d'exception qui force le développeur à gérer celles-ci ou à les propager explicitement à la fonction appelante.

C'est donc à mon sens une solution intéressante, mais il est trop facile d'oublier de traiter un cas exceptionnel. Heureusement, celui-ci se traduira par une exception qui finalisera le programme, mais en tant que développeur j'aurais aimé que mon compilateur m'assure lors du développement que je n'avais pas raté ce cas.

Bilan

Dans l'état actuel des choses, nous avons des méthodes qui laissent beaucoup de place à l'erreur potentielle et qui forcent à bien lire la documentation de chaque fonction pour distinguer les valeurs sentinelles, comprendre le fonctionnement du flag de réussite, ou connaître la liste des exceptions pouvant apparaître.

La méthode par exception est sans doute la plus propre de toutes, mais il est difficile dans les langages qui ne gèrent pas les exceptions vérifiées (Comme C++, Python, etc.) d'être certain d'avoir traité toutes les exceptions possibles.

En dernier point, touts ces méthodes sont très implicites et il n'est pas évidant de repérer les oublis de gestion d'erreur lors d'une lecture de code.

C++17 et std::optional

std::optional vas apparaître avec C++17 et une implementation était disponible dans Boost.Optional. Il s'agit d'une classe qui peut contenir ou pas une valeur. L'objet peut être testé avec une méthode has_value et la valeur peut être récupérée le cas échéant avec la méthode value. Vous pouvez dès à présent tester std::optional dans le namespace std::experimental dans de nombreux compilateurs. Reprenons notre exemple de la fonction rechercher :

// prototype de la fonction
std::optional<int> rechercher(const Collection &, const Item &);

// ...

std::optional<int> offsetO = rechercher(maCollection, monItem);

if(offsetO.has_value()) // ou tout simplement if(offset)
{
    int offset = offsetO.value();
    std::cout << "Item trouvé à la position " << offset << std::endl;
    std::cout << "La valeur de l'item est " << maCollection[offset] << std::endl;
}
else
{
    std::cout << "Item non trouvé".

}

Cette approche est très proche de l'approche par flag de réussite, sauf que le flag de réussite est contenu dans l'optional en même temps que la valeur de retour. Je trouve personnellement que cela rend l'API de la fonction plus lisible car on distingue bien en argument les entrées et en valeur de retour les sorties pouvant ne pas réussir. Comparons nos anciens prototypes :

// Méthode par valeur sentinelle, le prototype ne donne aucune
// information sur les erreurs possibles, il faudra lire la documentation
int rechercher(const Collection &, const Item &);

// Méthode par flag de réussite, le prototype nous laisse penser qu'il
// y a quelque chose à faire, d'autant que la valeur passée par référence
// n'est pas const, ce qui devrait nous alarmer, mais c'est tout de même
// flou.
bool rechercher(const Collection &, const Item &, int &);

// Méthode par exception, le prototype ne liste pas les exceptions.
int rechercher(const Collection &, const Item &);

// Méthode par optional, le prototype met en évidence la présence d'un
// résultat qui peut ne pas être là.
std::optional<int> rechercher(const Collection &, const Item &);

De plus, on ne peut pas utiliser directement l'optional<T> en lieu
et place de la valeur contenu, il faut explicitement aller le chercher
avec la méthode value.

Cette méthode est donc attrayante à mes yeux, mais on peut encore se tromper en allant directement chercher la valeur avec value sans tester sa présence :

int offset = rechercher(maCollection, monItem).value();

Heureusement, la fonction value lève une exception dans ce cas là, donc tout n'est pas perdu.

Une autre limitation concerne la soupe de code qui apparaît lorsque on travaille avec de nombreuses méthodes retournant des optional. Imaginons que nous avons les méthodes f, g et h tel que :

std::optional<B> f(Arg);
std::optional<C> g(B);
D h(C);

et que nous voulions construire la méthode std::optional<D> foo(Arg). Notre code risque de ressembler à ceci :

std::optional<D> foo(Arg arg)
{
    auto fResO = f(arg);
    if(!fResO)
    {
        return std::optional<D>{}; // un optional vide
    }

    auto fRes = fResO.value();

    auto gResO = g(fRes);
    if(!gResO)
    {
        return std::optional<D>{}; // un optional vide
    }

    auto gRes = gResO.value();

    return std::optional<D>{h(gRes));
}

Et encore, je vous ai épargné la cascade de if imbriqué. Ce code est lourd et fastidieux et potentiellement source d'erreur. Pourrait-on faire mieux ?

Une nouvelle API pour std::optional

Je reproche deux choses à std::optional dans son état actuel :

a) La libraire est source d'erreur car on peut appeler la fonction value alors que l'optional ne contient rien. Il faut en théorie s'assurer qu'il contient bien une valeur avant, et rien ne force cette vérification.
b) Il est difficilement composable, en effet, l'utilisation de plusieurs std::optional est souvent source d'une cascade de if imbriqués qui font mal aux crane et qui sont sources d'erreurs.

Sécurité

Nous allons nous attaquer au premier point. Ce que je veux c'est une fonction qui, connaissant un optional, fasse un traitement si celui-ci contient une valeur et pas de traitement sinon.

Dit autrement, je veux cette fonction, que j'appelle arbitrairement optionalApply :

template<typename T1, typename F>
void optionalApply(std::optional<T1> o, const F &f)
{
    if(o)
    {
        f(o.value());
    }
}

Simple non ? Elle effectue l'action f si l'optional est bon, et pas d'action sinon. F est un type templaté qui peut contenir au choix des fonctions, des lambdas, ou des objets fonction. On pourra donc écrire le code de recherche d'un élément dans un tableau de la façon suivante :

optionalApply(rechercher(maCollection, monItem), [&](int offset)
{
    std::cout << "Item trouvé à la position " << offset << std::endl;
    std::cout << "La valeur de l'item est " << maCollection[offset] << std::endl;
});

En se servant des fonctions lambda de C++11. Je l'avoue, la syntaxe est particulière du fait des lambdas, mais ici on obtient un code ou il est impossible de se tromper. Par contre je n'ai pas géré le cas o ùon veuille faire quelque chose si l'optional ne contient rien. Autant faire une nouvelle fonction, optionalCase

template<typename T1, typename FOk, typename FNotOk>
void optionalCase(std::optional<T1> o, const FOk &fOk, const FNotOk &fNotOk)
{
    if(o)
    {
        fOk(o.value());
    }
    else
    {
        fNotOk();
    }
}

Ce qui nous donne maintenant :

    optionalCase(rechercher(maCollection, monItem),
                 [&] (int offset)
                  {
                      std::cout << maCollection[offset] << std::endl;
                  },
                 [] ()
                 {
                     std::cout << "Item non trouvé" << std::endl;
                 }
                 );

On pourra débattre de la lisibilité du truc, mais maintenant, en tout cas, on ne peut plus se tromper.

Composabilité avec map

Maintenant la question c'est comment chaîner plusieurs fonctions qui peuvent renvoyer des optionals et ce facilement, en limitant la présence de variables intermédiaires (les optionals et leur valeur) et en limitant l'erreur au maximum.

Pour cela on va écrire la fonction map, variante de optionalApply qui contient un type de retour :

template<typename T1, typename T2, typename F>
std::optional<T2> map(std::optional<T1> o, const F &f)
{
    if(o)
    {
        return std::optional<T2>(f(o.value()));
    }

    return std::optional<T2>(); // un optional vide
}

Cette fonction nous permet donc d'appliquer n'importe quelle fonction de transformation de A vers B sur un std::optional<A> vers un std::optional<B>. Exemple d'utilisation, on veut calculer la longueur du contenu d'un fichier, sachant que on possède une fonction std::optional<std::string> readFileContent(const std::string &path).

// Solution sans map
std::optional<size_t> getFileLength(const std::string &path)
{
     auto contentO = readFileContent(path);
     if(contentO)
     {
         auto content = contentO.value();
         return std::optional<size_t>(content.size());
     }
     else
     {
         return std::optional<size_t>(); // optional vide
     }
}

// Solution avec map

std::optional<size_t> getFileLength(const std::string &path)
{
     return map(readFileContent(path), [] (std::string content)
     {
         return content.size();
     });
}

La solution avec map est plus simple. Notons que on pourrait faire encore plus simple et plus lisible en exploitant C++14 et C++17 pour la détection automatique des type des lambdas et si map était une méthode de optional, on pourrait avoir quelque chose du genre :

std::optional<size_t> getFileLength(const std::string &path)
{
     return readFileContent(path).map([] (auto content)
     {
         return content.size();
     });
}

Notons aussi que le lambda n'est pas obligatoire si la fonction à chaîner existe déjà.

Composabilité avec bind

Nous ne sommes toujours pas capables de traiter le cas présenté initialement avec les fonctions f, g et h car celles-ci ne sont pas des fonctions de A dans B, mais de A dans std::optional<B>, ce qui complexifie un peu les choses, nous allons introduire une nouvelle fonction, bind qui gère ce cas.

template<typename T1, typename T2, typename F>
std::optional<T2> bind(std::optional<T1> o, const F &f)
{
    if(o)
    {
        return f(o.value());
    }

    return std::optional<T2>(); // un optional vide
}

On peut donc maintenant traiter notre cas initial, je vous rappelle :

// sans map ni bind

std::optional<D> foo(Arg arg)
{
    auto fResO = f(arg);
    if(!fResO)
    {
        return std::optional<D>{}; // un optional vide
    }

    auto fRes = fResO.value();

    auto gResO = g(fRes);
    if(!gResO)
    {
        return std::optional<D>{}; // un optional vide
    }

    auto gRes = gResO.value();

    return std::optional<D>{h(gRes));
}

// avec map et bind

std::optional<D> foo(Arg arg)
{
    return map(bind(f(arg), g), h)
}

// avec map et bind en fonction membre des optionals

std::optional<D> foo(Arg arg)
{
    return f(arg).bind(g).map(h);
}

Voila, c'est plus simple et moins source d'erreur.

Pour finir, que faire si en bout de chaîne on veux une valeur par défaut si il n'y a pas de valeur dans l'optional, et bien il existe value_or qui est déjà dans le standard c++17 qui permet de récupérer la valeur ou une valeur par défaut, ce qui permet d'écrire :

std::optional<D> fooWithDefault(Arg arg, D def)
{
    return map(bind(f(arg), g), h).value_or(def);
}

Conclusion

Je vous ai présenté différentes méthodes de gestion des fonctions partielles. Pour ma part, mon coeur balance entre les optionals (avec map et bind) et les exceptions. Les optionals assurent une sécurité importante du code, une grande composabilité et assurent que l'erreur est traitée. Les exceptions ont moins de garantie, mais il existe des cas où on ne veut pas traiter l'erreur et où il est préférable d'avoir une exception.

Ces deux approches sont utilisables en C++, en Python (mais avec moins de garantie à la compilation) et dans de nombreux langages comme Haskell (mon préféré), Rust, Swift, Ocaml, j'en oublie des milliers. L'approche par optional a une place de premier ordre dans certains langages. Le C++ propose depuis son standard C++17 une API d'optional, mais je la trouve limitée et je vous ai proposé des extensions.

Les fonctions map, bind, optionalCase et optionalApply que je vous ai proposées sont utilisables comme cela, mais pour en faire une vraie librairie il faudrait prendre en compte de nombreux cas particuliers, notamment pour traiter le cas où les objets contenus dans l'optional ne peuvent pas être copiés.

  • # Ouai bof

    Posté par . Évalué à 5.

    Je ne fais pas de C++ mais vu ce que tu écris je suis d'accord avec ton constat. Un optional avec une méthode de test et de déréférencement n'apporte pas grand chose à retourner possiblement null. Les deux sont vérifiables avec une analyseur statique de code et les deux permettent de faire des erreurs… Et au passage si tu es dans un langage ou le compilo/runti,e ne peut pas virer l'optional et bien tu viens de prendre un accès mémoire en plus…

    D'ailleurs bien que l'Optional de Java soit loin d'être puissant ou génial, c'est un des premier truc que j'explique aux gens. Si tu utilises isPresent() suivi d'un get() alors tu fais certainement une erreur car tu n'as rien gagné du tout. Les API fonctionnelles elles empêchent toute erreur.

    Sur le sujet des différents types de gestions d'erreur et les approches des différents langages, je me souviens de cet excellent article: The error model

    • [^] # Re: Ouai bof

      Posté par . Évalué à 4.

      Un optional avec une méthode de test et de déréférencement n'apporte pas grand chose à retourner possiblement null.

      L'interet de optional en c++ c'est justement d'eviter de renvoyer un pointeur (mais sinon ca apporte pas plus).
      Avec optional l'ownership est explicite, et ca fonctionne avec les move et RVO.

      Perso je suis assez fan, et j'utilise pas mal (avec StatusOr pour la même chose en plus "lourd" https://github.com/google/certificate-transparency/blob/master/cpp/util/statusor.h).

    • [^] # Re: Ouai bof

      Posté par . Évalué à 9.

      Yep. Et en java en plus, ton optional peut techniquement être null, ce qui doit être assez cocasse de se prendre un npe sur un optional.

      Les,optional, c'est utile quand c'est intégré au cœur du langage (cf Swift, kotlin, et j'imagine d'autres aussi), et que t'as des constructions idiomatiques pour les gérer (genre le guard let de Swift).
      Si c'est pour remplacer if foo != null par sa version object if foo.isPresent(), se taper des generics de partout et alourdir tout le reste du code avec des foo.get(), ca améliore les choses que marginalement.

      Linuxfr, le portail francais du logiciel libre et du neo nazisme.

      • [^] # Re: Ouai bof

        Posté par (page perso) . Évalué à 2.

        Tout à fait d'accord sur l'histoire du pointeur null, en java cela aidera pas. En C++ tu peux éviter de mettre des pointeurs dans des optionnels et dans ce cas là tu auras moins de problème.

        Personnellement mon optional maison lève une exception si on lui met un pointeur null dedans.

    • [^] # Re: Ouai bof

      Posté par . Évalué à 1.

      Beaucoup des langages (tous?) qui gère les union types à paramètre gèrent celaen natif. On pense en premier lieu aux langages fonctionnels (Haskell, OCaml …), mais on peut aussi penser à Rust qui intègre le type Option dans sa librairie standard. Rust reprend beaucoup des principe du fonctionnel en son cœur même si il ne l'est pas.

      Dans ces langages, lorsque l'on fait appel à une fonction qui retourne un tel type, on est obligé de gérer tous les cas (le compilateur nous y force). Par ailleurs un sucre syntaxique (pattern matching), nous permet de faire cela de manière concise. Ces langages fournissent aussi en natif des fonctions map, bind pour ces types.

      En gros ces types ont été transposés dans Java et C++, mais les compilateurs de ces derniers ne présentant pas les mêmes garanties, l'effet de l'utilisation de Optional est moins prégnant surtout en terme de possibilité de bug. Cependant, ça me parait tout de même utile ne serait-ce que pour la concision que ça apporte.

      La mode étant au pensons fonctionnel, beaucoup de ses principes sont transposés dans les langages populaires. Cependant, cela s'effectue toujours avec un perte de garantie et de sécurité car ces derniers n'ont pas été pensés pour faire du fonctionnel initialement.

      Quant à savoir pourquoi tout le monde s'obstine avec ces langages bâtards pour leur nouveaux projets n’ayant pas besoin de performances extrêmes, c'est un autre histoire…

  • # C++ et exceptions

    Posté par (page perso) . Évalué à 4.

    Le C++ gère aussi les exceptions, mais la librairie standard ne s'en sert pas. Pour différentes raisons que je n'aborderais pas, la communauté du C++ n'aime pas les exceptions et peu de librairie les utilisent correctement.

    Je ne fais pas du tout de C++ et du coup ce passage m'intrigue profondément. Je ne veux pas lancer de troll hein, mais ça m'aurait vraiment intéressé de connaître ces raisons.

    Sinon super article, très intéressant ;)

    • [^] # Re: C++ et exceptions

      Posté par (page perso) . Évalué à 5.

      Je ne fais pas du tout de C++ et du coup ce passage m'intrigue profondément. Je ne veux pas lancer de troll hein, mais ça m'aurait vraiment intéressé de connaître ces raisons.

      Alors, en premier lieu, je dois corriger un point faux (imprécis) de mon article. La librairie standard c++ utilise les exceptions, mais dans certains cas limités, comme une allocation ratée. Mais l'ouverture d'un fichier, la recherche d'un élément qui n'existe pas, …, sont gérées avec des fonctions de test is_open, ou des valeurs sentinelles conteneur.end().

      Concernant l'usage, certains se plaignent des performances ou de l'augmentation de la taille du code, mais c'est très controversé et discutable en fonction du contexte de travail.

      Un autre problème, soulevé dans mon journal, est que les exceptions ne sont pas du tout listées dans le prototypes des fonctions, et donc qu'il est difficile de savoir, sans lire la documentation, qu'une fonction peut lancer une exception, et les choses peuvent évoluer sans prévenir.

      Un autre point important est la difficulté d'écrire du code qui résiste bien aux exceptions, l'idée étant que si une exception est lancée, tu dois pouvoir te retrouver dans un état stable et prévisible après récupération de l'exception, sinon il ne sert à rien de survire à l'erreur si c'est pour se retrouver dans un état indéterminé.

      • [^] # Re: C++ et exceptions

        Posté par . Évalué à 5.

        La librairie standard c++ utilise les exceptions, mais dans certains cas limités, comme une allocation ratée.

        Elle l'utilise aussi dans vector::at (et d'autres du même genre comme string::at) qui est l'équivalent de l'accès indicé (celui avec les accolades) mais avec vérification du domaine. Et il y a les fonctions de conversion genre std::stoi qui peuvent renvoyer des exceptions. Il y a aussi des fonctions liées au système comme std::thead::detach. Et puis, il y a quasiment toutes les fonctions de std::filesystem qui ont deux versions : une avec exception et l'autre avec un code d'erreur.

        Bref, affirmer que la bibliothèque standard utilise les exceptions dans des cas limités est erroné. Elle utilise les exceptions pour les cas exceptionnels, ce qui est la raison d'être des exceptions.

        • [^] # Re: C++ et exceptions

          Posté par (page perso) . Évalué à 5.

          Merci pour la précision des librairies qui utilisent des exceptions.

          Bref, affirmer que la bibliothèque standard utilise les exceptions dans des cas limités est erroné. Elle utilise les exceptions pour les cas exceptionnels, ce qui est la raison d'être des exceptions.

          La notion de cas exceptionnels est vraiment dépendante du point de vue. C'est toujours un grand moment de cassage de tête de savoir ce que on fait passer en exception et ce que on fait passer en std::optional ou autre méthode.

        • [^] # Re: C++ et exceptions

          Posté par . Évalué à 3. Dernière modification le 05/09/16 à 10:04.

          tu peux aussi ajouter les dynamic_cast T& qui te jettent une exception std::bad_cast, alors que les dynamic_cast T* vont te renvoyer un pointeur null dans le cas où ce n'est pas possible.

          En c++ on a le choix et c'est bien. Je déteste devoir parsemer mon code de try/catch pour une gestion qui peut s'agrémenter de if ou ?;

          Il ne faut pas décorner les boeufs avant d'avoir semé le vent

  • # contrats

    Posté par . Évalué à 2.

    Les contrats n'impliquent pas intrinsèquement un comportement non-défini (au sens du C avec apparition de démons naseaux) lorsqu'ils sont violés.

    Si l'on prend le cas d'une fonction mathématique (cad "pure" en info) on pourrait dire que le contrat est que les paramètres sont dans le domaine de définition. Ça n'implique pas en soit l'apparition de démons naseaux si l'appelant est en dehors, et il est tout à fait convenable (et même préférable) de réagir par exemple en levant une exception dans une implémentation.

    • [^] # Re: contrats

      Posté par (page perso) . Évalué à 2.

      apparition de démons naseaux

      Belle traduction, j’apprécie ;)

      Si l'on prend le cas d'une fonction mathématique (cad "pure" en info) on pourrait dire que le contrat est que les paramètres sont dans le domaine de définition. Ça n'implique pas en soit l'apparition de démons naseaux si l'appelant est en dehors, et il est tout à fait convenable (et même préférable) de réagir par exemple en levant une exception dans une implémentation.

      Quand une fonction n'est pas définie pour tout son domaine, il y a plusieurs solutions :

      • On réduit le domaine de définition, on rendant les types en entrée plus contraints, par exemple, on pourrait imaginer une fonction division avec le prototype suivant int division(int a, nonZeroInt b), où nonZeroInt est un type int qui ne peut pas contenir zéro. Mais cela force l'utilisateur à convertir et finalement cela repousse le problème plus haut dans le code.
      • On augmente le domaine de sortie, en mettant par exemple un std::optional en valeur de retour, ainsi division deviendrait std::optional<int> division(int a, int b). Mais on repousse le problème plus bas dans le code.
      • On lève une exception. C'est ce que tu proposes et dans de nombreux cas c'est une bonne solution, mais je ne peux pas m’empêcher de penser qu'un jour cela va péter au moment où on ne s'y attend pas.

      C'est un vrai problème et à chaque cas de figure il y a une solution qui est plus ou moins adaptée.

      Une solution que j’apprécie vraiment, mais qui est peu utilisé dans la vraie vie c'est l'utilisation de type rafinés. Tu peux ajouter des contraintes sur les types de ta fonction et laisser le compilateur prouver que ces contraintes ne seront jamais violée. Sinon il peut te monter dans quel cas ton code est faux ou simplement te laisser dans le doute en t’annonçant qu'il n'a pas réussis à faire la preuve. En Haskell il y a Liquid Haskell qui est très amusant à utiliser, mais je n'ai pas de retour sur l'impact que cela peut avoir sur un gros projet.

      • [^] # Re: contrats

        Posté par . Évalué à 2.

        Des langages comme Smalltack utilisent des contrats qui sont simplement des tests en début et fin de fonctions. Ces tests s’héritent avec l'interface objet.

        L’intérêt est de faire confiance à la lib de base, ce qui évite de programmer de façon défensive, ce qui a un coût en perf énorme. Les contrats permettent ici de récupérer les erreurs.

        Le problème des types complexes, c'est qu'un humain a du mal à s'en sortir avec. Comprendre simplement les type GADT de Ocaml, n'est pas simple du tout. Et pourtant les possibilités sont limités.

        "La première sécurité est la liberté"

  • # Liberté de l'utilisateur

    Posté par . Évalué à 2.

    Merci pour cette présentation claire de l'état de l'art sur la gestion des erreurs. En fait, j'ai quand même l'impression que ce domaine est peut-être l'un de ceux où les langages de programmation ont le plus de marge de progression. En particulier, il s'agit toujours d'un compromis entre la souplesse et la robustesse, entre une approche quasiment mathématique (obligation de gérer toutes les causes d'échec) et une approche pragmatique.

    Par exemple, ce qui me gène pas mal avec std::optional, c'est qu'on impose à l'utilisateur une syntaxe particulière (en gros, on reporte sur l'utilisateur le traitement des erreurs). L'autre problème, c'est qu'il ne semble pas facile de distinguer les "warnings" (du style "attention, il y a quelque chose qui ne se passe pas comme prévu") et les erreurs ("la valeur de retour n'a pas de sens").

    Honnêtement, le meilleur système que j'ai trouvé pour les projets de taille moyenne (code scientifique principalement destiné à tourner en "interne", ne doit pas faire n'importe quoi mais pas d'aspect critique—en particulier, mieux vaut planter que de rester dans un état douteux), c'est la logique de certains langages interprétés, où les fonctions peuvent retourner quelque chose comme "NA" et/ou émettre des warnings, ce qui fait que le code n'est jamais dans un état réelement indéterminé (au pire, f(NA) renvoie NA). Du coup, on peut développer sans se prendre la tête avec le traitement des erreurs, et on peut ratrapper le coup à n'importe quel niveau. Le côté négatif, évidemment, c'est que l'erreur n'est pas identifiée clairement par le code de retour, et que les détails sont dans stderr, donc plus ou moins inaccessibles au programme.

    • [^] # Re: Liberté de l'utilisateur

      Posté par . Évalué à 2.

      Crée ton optional avec levée d'exception si tu y accède alors qu'il n'existe pas, ou utilise value_or pour avoir une valeur avec du sens; je ne vois pas vraiment ce que l'on peut faire de mieux.

      Il vaut mieux lire l'interface exposée ici pour se faire une idée.

      http://en.cppreference.com/w/cpp/experimental/optional

      pour son exemple

      std::optional<D> foo(Arg arg)
      {
          auto fResO = f(arg);
          if(!fResO)
          {
              return std::optional<D>{}; // un optional vide
          }
      
          auto fRes = fResO.value();
      
          auto gResO = g(fRes);
          if(!gResO)
          {
              return std::optional<D>{}; // un optional vide
          }
      
          auto gRes = gResO.value();
      
          return std::optional<D>{h(gRes));
      }

      Je me contenterai de

      std::optional<D> foo(Arg arg)
      {
        try{
          return make_optional(h( g( f(arg).value() ).value())) ;
         }
         catch(Machin e)
         {
          return std::optional<D>();
         }
      }

      Il ne faut pas décorner les boeufs avant d'avoir semé le vent

      • [^] # Re: Liberté de l'utilisateur

        Posté par . Évalué à 3.

        je ne vois pas vraiment ce que l'on peut faire de mieux.

        Ça rajoute une couche de complexité qui n'est pas forcément nécessaire. Je sais bien que le C++ est un langage verbeux et qu'on peut changer de langage si on aime les trucs concis, mais personnellement ça me gêne de devoir appeler ".value()" pour accéder à la valeur cherchée. Je comprendrais le mécanisme des optional s'il y avait une conversion implicite de optional vers T, ce qui permettrait, au choix, d'utiliser la fonction de manière transparente, ou de gérer les erreurs si le contexte le demande.

        C++ est devenu un truc tellement gros que j'ai l'impression que la plupart des projets en utilisent un sous-ensemble, du style sans STL, sans exceptions, sans templates, sans héritage multiple, sans pointeurs, sans lambdas, etc. Si tu publies une bibliothèque dont la gestion d'erreur repose sur des optional, tu forces les utilisateurs à adopter cette méthode, même si ça ne correspond pas du tout à la manière dont, en interne, ils gèrent les erreurs. Je ne sais pas si le jeu en vaut la chandelle.

        • [^] # Re: Liberté de l'utilisateur

          Posté par (page perso) . Évalué à 3.

          Je comprendrais le mécanisme des optional s'il y avait une conversion implicite de optional vers T, ce qui permettrait, au choix, d'utiliser la fonction de manière transparente, ou de gérer les erreurs si le contexte le demande.

          Personnellement je déteste les conversions implicites, c'est subjectif, mais je sais que je vais me faire avoir à un moment donné.

          Si tu publies une bibliothèque dont la gestion d'erreur repose sur des optional, tu forces les utilisateurs à adopter cette méthode, même si ça ne correspond pas du tout à la manière dont, en interne, ils gèrent les erreurs. Je ne sais pas si le jeu en vaut la chandelle.

          En raisonnant de cette manière on ne fait aucune évolution et on reste à faire du C avec classes comme l'était les premières versions de C++.

          • [^] # Re: Liberté de l'utilisateur

            Posté par . Évalué à 2.

            En raisonnant de cette manière on ne fait aucune évolution et on reste à faire du C avec classes comme l'était les premières versions de C++.

            Ça serait vrai si la gestion des erreurs par les optional était une solution universelle et consensuelle. Personnellement, j'ai l'impression que ça ressemble à une fausse bonne idée, qui complexifie l'interface et qui impose à l'utilisateur un paradigme de gestion des erreurs qu'il ne maîtrise peut-être pas, qu'il n'apprécie peut-être pas, ou qui n'est peut-être pas pertinent dans le cadre de son projet.

            Je ne suis pas Norstradamus et j'ai peut-être tort, mais ma prédiction, c'est que si ce type de gestion d'erreur se généralise, 99% du C++ développé en 2020 sera quelque chose comme

            float distance(float x1, float x2, float y1, float y2) {
               return sqrt(square(x2-x1).value() + square(y2-y1).value()).value();
            }

            Du coup, j'ai du mal à voir l'avantage…

            • [^] # Re: Liberté de l'utilisateur

              Posté par (page perso) . Évalué à 4.

              Ça serait vrai si la gestion des erreurs par les optional était une solution universelle et consensuelle. Personnellement, j'ai l'impression que ça ressemble à une fausse bonne idée, qui complexifie l'interface et qui impose à l'utilisateur un paradigme de gestion des erreurs qu'il ne maîtrise peut-être pas, qu'il n'apprécie peut-être pas, ou qui n'est peut-être pas pertinent dans le cadre de son projet.

              Je ne vois pas en quoi cela complexifie l'interface ? D'un coté nous avons une interface implicite où les erreurs sont gérées par des valeurs sentinelles ou des flags, où il faut lire la documentation d'une fonction en profondeur pour comprendre son comportement et avoir de la rigueur lors de l'utilisation pour ne pas se tromper et pour laquelle tout le monde réinvente de la gestion d'erreur tous les jours. Un jour le flag de retour est un bool qui vaut true pour ok, false sinon, l'autre jour c'est l'inverse, le troisième jour c'est un int. De l'autre nous avons une fonction dont le prototype te renseigne beaucoup sur son comportement et pour laquelle tu ne peut pas te tromper en l'utilisant. Et la seule complexification de l'interface c'est l'utilisation d'une classe optional contenant deux méthodes, map et bind (non fournies en standard je te l'accorde). On peut aussi utiliser l'approche par exception sur le .value() comme proposée par d'autres.

              Pour le coté consensus, c'est tout de même une approche prise dans de nombreux langages : Rust, Swift, OCaml, Haskell, entre autre… Avec une approche plus ou moins poussée.

              float distance(float x1, float x2, float y1, float y2) {
                 return sqrt(square(x2-x1).value() + square(y2-y1).value()).value();
              }

              Pourquoi n'as tu pas poussé le vice de ton exemple jusqu'à mettre un optional sur le + pour montrer à quel point les optionals sont complexes ?

              La gestion d'optional n’apparaît que dans les fonctions qui peuvent générer des erreurs, et dans ces fonctions, il faut traiter les erreurs et soit c'est fait avec des optionals, soit avec autre chose, quoi qu'il en soit, cela apparaitra. Soit il n'y a pas d'erreur à traiter, et dans ce cas là pas de problème. Ainsi ta fonction distance qui est toujours définie, sauf en cas de nombre infinis, peut être écrite sans optional. Après si les NotANumber t’embêtes, alors tu pourras mettre des optionals pour protéger.

              • [^] # Re: Liberté de l'utilisateur

                Posté par . Évalué à 3.

                Pour moi, ce qui est intéressante dans ce genre de classe, c’est de surcharger tous les opérateurs, comme ça tu écris ta/tes formule/s et à la fin, quand tu as besoin de savoir le résultat, tu vérifies que ton résultat est correct avant de l’utiliser. J’avais écris un truc équivalent pour des données arrivant sur un BUS.

            • [^] # Re: Liberté de l'utilisateur

              Posté par . Évalué à 3. Dernière modification le 07/09/16 à 15:50.

              En Haskell, on fait comme ça :

              distance :: Maybe Float -> Maybe Float -> Maybe Float -> Maybe Float -> Maybe Float
              distance = liftA4 (\x1 x2 y1 y2 -> sqrt (square (x2 - x1) + square (y2 - y1))

              liftA4 n'existe pas en pratique, mais c'est l'idée, pour toute fonction de type a -> b -> c -> d… on calcule une fonction de type f a -> f b -> f c -> f d. Ça s'appelle un foncteur applicatif, ça permet de faire de la gestion d'erreur, mais pas que. On peut simuler du calcul non déterministe, faire des traces, du code pure qui travaille sur des IO, etc.

              C'est beau.

              • [^] # Re: Liberté de l'utilisateur

                Posté par (page perso) . Évalué à 3.

                Moué, en pratique cela me choque la fonction distance qui prend des Maybe en paramètre… Je ne sais pas si j'ai quelque part dans mon code des fonctions qui prennent des Maybe en paramètre (si ce n'est des combinateurs).

                Alors, si on suppose que square :: Float -> Maybe Float et que sqrt :: Float -> Maybe Float, mais bon, parce que on est pervers, je ferais :

                distance :: Float -> Float -> Float -> Float -> Maybe Float
                distance x1 x2 y1 y2 = do
                    d1 <- square(x2 - x1)
                    d2 <- square(y2 - y1)
                    sqrt (d1 + d2)

                distance :: Float -> Float -> Float -> Float -> Maybe Float
                distance x1 x2 y1 y2 = sqrt =<< ((+) <$> square (x2 - x1) <*> square (y2 - y1))

                Mais là on nous prendrais pour des malades ;)

                Si cela intéresse quelqu'un, je peux expliquer ce qu'est ce bordel. Cela fait peur, mais les trois opérateurs =<<, <$> et <*> sont tellement utilisés tous le temps en haskell qu'il sont un peu comme le =, les [] et le -> du C.

      • [^] # Re: Liberté de l'utilisateur

        Posté par (page perso) . Évalué à 3.

        std::optional<D> foo(Arg arg)
        {
          try{
            return make_optional(h( g( f(arg).value() ).value())) ;
           }
           catch(Machin e)
           {
            return std::optional<D>();
           }
        }

        C'est pas mal en effet. J'aime moins car je trouve que c'est plus verbeux / complexe que l'approche à base de map et bind et que on peut facilement oublier de faire le traitement. Cependant j'admet que c'est une solution intéressante avec ce qui est fournit par défaut.

        • [^] # Re: Liberté de l'utilisateur

          Posté par . Évalué à 3.

          à noter que l'exemple n'est vraiment pas top,

          si on fait if( machinOptional ), on peut ensuite utiliser *machinOptional, le value() fait une verif supplémentaire qui n'est pas nécessaire.

          Il ne faut pas décorner les boeufs avant d'avoir semé le vent

          • [^] # Re: Liberté de l'utilisateur

            Posté par (page perso) . Évalué à 2.

            si on fait if( machinOptional ), on peut ensuite utiliser *machinOptional, le value() fait une verif supplémentaire qui n'est pas nécessaire.

            Oui. C'est un exemple pédagogique pour montrer que cette approche est, à mon sens, lourde. On est pas à un test dans un exemple pédagogique, mais promis, la prochaine fois, je le ferais mieux ;)

  • # Mauvaise connaissance du c++

    Posté par . Évalué à 5.

    Cependant, rien ne force le développeur à gérer les exceptions et rien n'indique à celui-ci qu'une fonction peut lancer une exception, si ce n'est une lecture scrupuleuse de la documentation. Certains langages, comme Java, proposent un système d'exception qui force le développeur à gérer celles-ci ou à les propager explicitement à la fonction appelante.

    Pour la stl, les exceptions sont rare uniquement si tu le réclame, optional à un option de construction qui lui permet de jeter une exception lorsque tu utilisas values() si tu lui réclame.

    Tu as aussi le value_or() qui permet d'avoir une valeur par défaut si on a rien. C'est pareil avec les ios, les dynamic_cast, les conteurs (at() au lieu de operator[]… )

    Le fait d'avoir le choix est une bonne chose, je ne compte pas le nombre de

    Machin ma = null ; 
    try{
      ma=pouet() ;
    }catch(ExceptionMachin ignored){}
    }catch(ExceptionBidule ignored){}
    }catch(ExceptionTruc ignored){}

    que je croise en java pour devoir gérer le fait que l'on sait qu'on a pas d'exception dans ce cas précis, mais qu'on doit faire comme si. Avec en prime le fait que le compilo pense que ma peut être null, ce qui peut faire chier au niveau de certaines annotations, donc faut encore en rajouter…

    Heureusement depuis java 7 on peut cumuler les catch (catch ExecptionMachin|ExceptionTruc|ExceptionBidule|ExcetionCasseCouille|ExcetptionJeDepasseLaLigneDe120Caractere|ExceptionJenAiEncore e)

    Le code java est pollué par de la gestion de truc qui n'arrivent jamais, et le jour où ça arrive, c'est tellement ignoré que ça passe à la trappe; j'ai une préférence pour faire un printstacktrace, au cas où.

    Il ne faut pas décorner les boeufs avant d'avoir semé le vent

    • [^] # Re: Mauvaise connaissance du c++

      Posté par . Évalué à 1.

      Le code java est pollué par de la gestion de truc qui n'arrivent jamais, et le jour où ça arrive, c'est tellement ignoré que ça passe à la trappe; j'ai une préférence pour faire un printstacktrace, au cas où.

      Hum ignoré ou printStracktrace(), dans les deux cas tu as merdé ton design quelque part. À priori même à deux endroits: définition de l'API et gestion des erreurs à l'utilisation.

      Normalement au pire tu utilise une API qui utilise des checked exceptions pour signaler des cas que tu considères être des faults plutôt que des contingencies (ie. tu ne peux rien faire localement sinon remonter le problème à un fault handler de plus haut niveau qui lui décidera de l'action adéquat)

      Ça reste verbeux mais il n'y a pas de miracle. Les gens qui utilisent des unchecked se plaignent du côté loterie, de la gestion de compatibilité impossible etc. mais ne sont pas embêtés pour écrire du code de porc. Et les gens qui utilisent des checked se plaignent d'avoir à gérer ce qui est a gérer. Comme la flemmardise et le vite fait mal fait sont la règle dans le métier, on a tendance à plus entendre les gens qui ne veulent gérer aucun cas d'erreur.

      Et comme les faults des uns sont souvent les contingencies des autres, on arrivera rarement a faire une séparation propre des deux niveaux API.

      Rien de nouveau sous le soleil: http://www.oracle.com/technetwork/java/effective-exceptions-092345.html

      c'est tellement ignoré que ça passe à la trappe; j'ai une préférence pour faire un printstacktrace, au cas où.

      Et depuis la nuit des temps, ce genre de code bases sont des tas de boue.

      Même quand le langage est pas coopératif, c'est pas bien difficile de mettre en place des principes simples qui empêche d'ignorer ce genre d'erreurs. Au minimum tu imposes que les trucs "impossibles" soient remontés au fault handler le plus proche ou racine, plutôt que de continuer son petit bonhomme de chemin.

      A choisir, pouvoir raisonner sur une base de code est beaucoup plus important que sauver deux lignes de code.

      • [^] # Re: Mauvaise connaissance du c++

        Posté par . Évalué à 2. Dernière modification le 05/09/16 à 14:33.

        Et les gens qui utilisent des checked se plaignent d'avoir à gérer ce qui est a gérer.

        Le problème des checked exceptions, c'est que un changement dans une méthode des types d’exceptions retournés, on doit changer tous les appelants (sinon ça compile pas). Alors que les appelants en ont rien à faire, la plupart du temps.

        ça pollue le code appelant, et j'ose pas imaginer le bordel si tu mets ça dans une bibliothèque partagée publiquement, pour tous ses utilisateurs…

        Source : Kent Beck. ;-)

        A choisir, pouvoir raisonner sur une base de code est beaucoup plus important que sauver deux lignes de code.

        Je crois que la gestion des cas d'exceptions et des erreurs, c'est plus compliqué que deux lignes de code. Sinon, on en parlerait pas encore aujourd'hui.

        "Quand certains râlent contre systemd, d'autres s'attaquent aux vrais problèmes." (merci Sinma !)

        • [^] # Re: Mauvaise connaissance du c++

          Posté par . Évalué à 2.

          Le problème des checked exceptions, c'est que un changement dans une méthode des types d’exceptions retournés, on doit changer tous les appelants (sinon ça compile pas). Alors que les appelants en ont rien à faire, la plupart du temps.

          Donc tu n'as aucun problème dans le fait que n'importe quelle API que tu utilises change sa signature sans t'en informer et sans aucune information à la compilation ? Par ce que c'est exactement ce dont il s'agit. On parle d'un bon vieux goto non local qui peut apparaitre ou disparaitre n'importe où n'importe quand sans aucun moyen de contrôle.

          En fait ce qui dérange les gens c'est d'un côté de gérer correctement une API, son évolution et de l'autre la robustesse et exploitabilité d'une application.

          Vu qu'on n'est déjà pas capable de le faire correctement quand un système de type est la pour nous aider, on va tout faire à runtime histoire au moins on est plus embêté par ce foutu système de type qui nous dit qu'on fait n'importe quoi en changeant la signature / fonctionnement d'une API publiée ! Et puis c'est vrai enfin quoi puisque la plupart des utilisateurs font n'importe quoi, pourquoi s'embêter ?

          Tu as une analyse du problème bien plus intéressante que la mienne dans l'article que j'ai déjà cité: The error model.

          Source : Kent Beck. ;-)

          Puisque tout le monde dit des conneries, Kent Beck et la maitresse inclus, l'auteur est en général bien moins intéressant que l'argumentaire.

          • [^] # Re: Mauvaise connaissance du c++

            Posté par . Évalué à 2. Dernière modification le 05/09/16 à 15:52.

            Donc tu n'as aucun problème dans le fait que n'importe quelle API que tu utilises change sa signature sans t'en informer et sans aucune information à la compilation ?

            En quoi je devrais gérer les exceptions de tous ceux que j'appelle ? Quand pourrais-je m'occuper de mes exceptions, plutôt que de faire du support pour le code d'autrui ? A quel moment ça s'arrête ?

            Vu qu'on n'est déjà pas capable de le faire correctement quand un système de type est la pour nous aider, on va tout faire à runtime histoire au moins on est plus embêté par ce foutu système de type

            Critiquer le bordel que les Checked Exceptions mettent dans les responsabilités entre appelé et appelant, ce n'est pas critiquer le système de types… Faut se calmer, là…

            "Quand certains râlent contre systemd, d'autres s'attaquent aux vrais problèmes." (merci Sinma !)

            • [^] # Re: Mauvaise connaissance du c++

              Posté par . Évalué à 1.

              Critiquer le bordel que les Checked Exceptions mettent dans les responsabilités entre appelé et appelant, ce n'est pas critiquer le système de types… Faut se calmer, là…

              Tu peux parfaitement critiquer.

              Maintenant ce que tu critiques ce n'est pas uniquement les checked exceptions c'est le principe que les erreurs fassent partie de la signature d'une méthode.

              • [^] # Re: Mauvaise connaissance du c++

                Posté par . Évalué à 2. Dernière modification le 05/09/16 à 21:22.

                Ben non. Avec les exceptions normales, le changement du type/nombre d'exceptions retournables par une méthode :
                1. Ne t'empêchent pas de compiler
                2. Ne te forcent pas à changer (à minima) tes signatures et ton code (= t'occuper du boulot des autres).
                3. Et ce, pour tous les appelants

                Je ne dis pas d'ignorer les exceptions, mais au moins avec les exceptions normales on peut garder l'indépendance entre les couches du code.

                Parce que la entre les checked Exceptions et (par exemple) la classe de base qui touchent les classes filles => ça donne quelque chose de très monolithique très vite.

                Et surtout, les exceptions normales ne te forcent pas à violer SRP. ;-)

                "Quand certains râlent contre systemd, d'autres s'attaquent aux vrais problèmes." (merci Sinma !)

                • [^] # Re: Mauvaise connaissance du c++

                  Posté par (page perso) . Évalué à 3.

                  C'est dingue comme on peut avoir des avis si divergent, c'est interessant.

                  Dans mon cas de figure je veux être au courant que l'API a changée, et pour moi la possibilité d'une exception en plus ou en moins cela fait partie de l'API, c'est tout aussi important que si un argument est ajouté à une méthode et je veux que cela m’empêche de compiler.

                  Après si tu laisses les exceptions remonter une pile d'appel de 12 fonctions, c'est qu'il y a aussi sans doute un problème de conception quelque part et il serait intéressant de revoir l'encapsulation.

        • [^] # Re: Mauvaise connaissance du c++

          Posté par . Évalué à 3.

          ça pollue le code appelant

          C'est de la gestion d'erreur, si tu considère que c'est de la pollution c'est dommage. De plus si elles n'en ont rien c'est de loin ce qu'il y a de plus concis.

          Mais surtout, là où tu traite ton exception tu as forcément un couplage avec l'exception. Il doit la comprendre et savoir quoi en faire. Faire remonter aussi haut que tu semble le dire ton exception, pousserait à penser que tu expose du fonctionnement un peu trop interne à trop haut. Ensuite pour toutes les méthodes passe plat, c'est plutôt sympa pour avoir une meilleure visibilité des chemins que ton exception peut prendre (et les refacto peuvent facilement être automatisés).

          et j'ose pas imaginer le bordel si tu mets ça dans une bibliothèque partagée publiquement, pour tous ses utilisateurs…

          Le type de tes exceptions est exactement comme le type de tes paramètres ou de ton retour. Si tu casse ton API c'est dommage pour toi.

          Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

          • [^] # Re: Mauvaise connaissance du c++

            Posté par . Évalué à 1. Dernière modification le 06/09/16 à 15:21.

            La gestion des erreurs : OK;

            Les checked exceptions qui impactent absolument tout le monde au moindre changement/ajout : non merci.

            Après, si ça te gêne pas de violer SRP, c'est dommage.

            "Quand certains râlent contre systemd, d'autres s'attaquent aux vrais problèmes." (merci Sinma !)

            • [^] # Re: Mauvaise connaissance du c++

              Posté par . Évalué à 3.

              Les checked exceptions qui impactent absolument tout le monde au moindre changement/ajout : non merci.

              Ce n'est pas possible. Vraiment. Si tu as ce genre de situation c'est probablement que le design est à revoir. Les exceptions ce n'est pas fait pour traverser toutes ta pile d’exécution. Si tu as vraiment ce genre d'impact (ou que tu en as l'impression) c'est que tu ne maîtrise pas le cheminement de tes erreurs. De plus ça peut vouloir dire que tu expose trop de choses aux appelants.

              Ce n'est pas simple, mais la gestion d'erreur n'est pas simple. Devoir traiter l'erreur à tous les étages via des retours de méthodes n'est pas particulièrement plus simple, ni agréable.

              Après, si ça te gêne pas de violer SRP, c'est dommage.

              Probablement parce que ça n'a rien avoir. Si tu change une exception, même si tu fais passe plat de cette exception, tu change ton contrat (à moins que ton contrat soit déjà large et qu'il annoncé une exception mère). Donc c'est ça responsabilité de l'annoncer.

              Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

              • [^] # Re: Mauvaise connaissance du c++

                Posté par . Évalué à 0.

                Les Checked Exceptions != Exceptions habituelles… Dans le cas des exceptions normales, je suis tout à fait d'accord.

                "Quand certains râlent contre systemd, d'autres s'attaquent aux vrais problèmes." (merci Sinma !)

                • [^] # Re: Mauvaise connaissance du c++

                  Posté par . Évalué à 2.

                  Je ne comprends pas. Les checkeds exceptions sont là pour t'empêcher de lancer des erreurs de manière implicite (comme je le dis ça fait parti du contrat de ta méthode) et sont donc de la responsabilité de toute la pile d'appel traversante. Les exceptions non vérifiées sont pratiques pour justement traverser 80% (ou 100% qui sait ?) de ta pile sans avoir à te demander ce qui se passe.

                  Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

                  • [^] # Re: Mauvaise connaissance du c++

                    Posté par . Évalué à 1.

                    sont donc de la responsabilité de toute la pile d'appel traversante.

                    Et c'est bien ce qui coince. De part leur nature, tu changes le type de Checked Exceptions retourné ou tu en rajoutes une : ça impact tout la pile.

                    "Quand certains râlent contre systemd, d'autres s'attaquent aux vrais problèmes." (merci Sinma !)

                    • [^] # Re: Mauvaise connaissance du c++

                      Posté par (page perso) . Évalué à 2.

                      De part leur nature, tu changes le type de Checked Exceptions retourné ou tu en rajoutes une : ça impact tout la pile.

                      C'est là que le design est mal fichu. Cela impacte toute la pile le temps que tu règles le problème en gérant l'exception à l'endroit qui te semble pertinent dans la pile, si tu veux le gérer localement, et bien fait le localement (et le reste de la pile ne sera pas impactée). Si tu veux le faire 10 étapes plus haut, alors il est normal que cela impact les 10 couches intermédiaires.

                    • [^] # Re: Mauvaise connaissance du c++

                      Posté par . Évalué à 3.

                      ça impact tout la pile

                      Non, ça n'impacte que ceux qui décident de ne pas se charger de ce problème.

                      Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

                      • [^] # Re: Mauvaise connaissance du c++

                        Posté par . Évalué à -1.

                        Bon, va te documenter sur la différence entre les Checked Exceptions et les Exceptions normales, et pourquoi seul Java les implémente, parce que là tu parles dans le vide…

                        "Quand certains râlent contre systemd, d'autres s'attaquent aux vrais problèmes." (merci Sinma !)

                        • [^] # Re: Mauvaise connaissance du c++

                          Posté par . Évalué à 4.

                          Bon, va te documenter sur la différence entre les Checked Exceptions et les Exceptions normales

                          Je suis aller voir si je ratais quelque chose et ils semble que non, c'est assez simple. Les exceptions non vérifiées peuvent être ignorée silencieusement alors que les autres doivent être géré (soit par un try/catch soit par un throws) et c'est vérifié statiquement.

                          et pourquoi seul Java les implémente

                          Pas trouvé grand chose là dessus. Le seul truc qui parle d'une controverse que j'ai trouvé c'est ça : https://docs.oracle.com/javase/tutorial/essential/exceptions/runtime.html
                          En cherchant un peu différemment j'ai trouvé ça d'intéressant : http://stackoverflow.com/a/18472120. Globalement il y a quelques arguments :

                          • la distance entre l'erreur et sa gestion. J'en ai déjà parlé si c'est si long que ça c'est que tu as des modules bien trop gros (il devient difficile d'avoir une compréhension simple de « j'ai une erreur ici, ça se répercute là »)
                            • il y a un argument sur l'encapsulation que je ne comprends pas du tout. L'encapsulation consiste basiquement à cacher la représentation interne pour n'exposer qu'une API (pour moi les erreurs font partie de l'API, mais bon). Plus tu écarte cette gestion d'erreur de l'endroit où elle est levée, plus tu expose de ton fonctionnement interne. Pire tu complexifie l'usage de ton API en ayant des cas implicites. Au final on viol la loi de Demeter.
                          • l'existence d'unchecked exception empêche de faire de l'exception-free. L'exception-free est un mythe, ça ne peux fonctionner que sur du code sans allocation par exemple. Ce n'est pas du tout ou rien. Tu as toutes les erreurs métier qui peuvent se gérer comme ça et c'est déjà énorme
                          • des questions de performance. Ok mais c'est au détriment de la qualité du code (on perd l'analyse statique de la gestion d'erreur)

                          parce que là tu parles dans le vide…

                          Il semble en effet. C'est dommage, mais j'aurais au moins essayé de communiqué.

                          Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

                          • [^] # Re: Mauvaise connaissance du c++

                            Posté par . Évalué à 0.

                            Je suis aller voir si je ratais quelque chose et ils semble que non, c'est assez simple.

                            Ah mais l'intention derrière les Checked Exceptions est noble. Mais en pratique, c'est du caca.

                            If we throw an IOException in UserRepository.getContent(Path) and want to handle them in ContextMenu.menuClicked(), we have to change all method signatures up to this point. What happens, if we later want to add a new exception, change the exception or remove them completely? Yes, we have to change all signatures.

                            Supeeeer.

                            "Quand certains râlent contre systemd, d'autres s'attaquent aux vrais problèmes." (merci Sinma !)

                            • [^] # Re: Mauvaise connaissance du c++

                              Posté par . Évalué à 3.

                              Tu peux répéter la même chose à l'envi ça ne rendra pas les choses plus vraies pour autant.

                              Si tu change le comportement extérieur de UserRepository.getContent(Path) ça signature change que ce soit explicite ou implicite les cas d'erreurs changent (c'est pas un problème de java ou autre). C'est factuel. Le fait que tu ne veux pas le voir apparaître dans ton code sans autre raison que « ça fait des choses à changer » (alors que ça ne fait que mettre en évidence ce qui a changé), n'est pas plus intéressant que ça. C'est comme si tu me disais que le typage statique est contraignant parce que si tu change le type d'une variable, tu dois le reporter partout où tu t'en sert.

                              Surtout que c'est dans des cas rare que tu fais ça. Selon ton niveau de design tu va présenter un type d'exception et tu va encapsuler toutes exceptions spécifiques comme un sous type de se dernier. Dans l'exemple de l'article, tu expose ton implémentation donc oui chaque modification va tout bazarder. Mais le design de l'exemple est à revoir AMHA.

                              D'ailleurs (c'est assez marrant) l'auteur met le doigt sur le problème :

                              Moreover, if you use an interface of a library (e.g. Action.execute()) you are not able to change the signature at all.

                              C'est bien que lever une IOException dans une méthode AddUserAction.execute(Object) n'est probablement pas une bonne idée.

                              Pour moi l'erreur consiste à avoir une approche bottom-up plutôt que top-down. Ici la question qu'il faut se poser c'est est-ce que ContextMenu.menuClicked() a quelque chose à faire d'une IOException ? Ce qui l'intéresse c'est peut être plutôt de savoir que l'Action ai échouée. Que ça vienne d'une erreur d'IO, d'une connexion à la base raté ou autre, ne change probablement pas vraiment son comportement, non ? Si son job c'est d'afficher l'erreur qui va bien à l'utilisateur alors ça peut carrément être traité dans une exception générique à tous les cas.

                              Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

                              • [^] # Re: Mauvaise connaissance du c++

                                Posté par . Évalué à 1. Dernière modification le 08/09/16 à 14:30.

                                Tu peux répéter la même chose à l'envi ça ne rendra pas les choses plus vraies pour autant.

                                Tu peux répéter à l'envie que les Checked Exceptions n'apportent aucun problème, ce ne sera pas vrai pour autant. Surtout que de mon côté j'ai des exemples et articles qui démontrent leurs problèmes par A + B.

                                Mais il n'y a pas pire aveugle que celui qui ne veut pas voir.

                                "L'UI qui s'occupe des SQLExceptions du Data Layer ? Je vois aucun problème avec ça. SRP n'est aucunement violé !"
                                - barmic , 2016

                                Mettez au moins un chapeau en alu sur la tête, comme ça on pourra vous éviter dans la rue. Parce que là, un tel niveau d'incompétence de votre part c'est criminel.

                                "Quand certains râlent contre systemd, d'autres s'attaquent aux vrais problèmes." (merci Sinma !)

                                • [^] # Re: Mauvaise connaissance du c++

                                  Posté par . Évalué à 3.

                                  "L'UI qui s'occupe des SQLExceptions du Data Layer ? Je vois aucun problème avec ça. SRP n'est aucunement violé !"
                                  - barmic , 2016

                                  C'est justement ce que je dis. Tu ne devrais avoir ni SQLException, ni IOException au niveau de l'UI (que ce soit dans les signatures utilisées ou dans le code). À la place tu devrait avoir une abstraction. C'est bien le fait de propager très loin ton exception qui viole la SRP.

                                  Si dans l'exemple de l'article que tu présente, l'Action ou le Repository (la responsabilité de chacun n'est pas très clair avec juste le nom) aurait encapsuler l'exception dans un type comme ActionException (nom peut être mal choisi). Tu aurais :

                                  • pas de viol de la SRP
                                  • meilleur respect de Demeter
                                  • plus de robustesse aux évolutions

                                  Ça fait combien de fois que je dis qu'il ne faut pas propager comme ça les exceptions ? Tiens je le dis juste dans le commentaire où tu répond bizarrement…

                                  Tu peux répéter à l'envie que les Checked Exceptions n'apportent aucun problème […]

                                  Non je dis que ton argument est mauvais pas que les checked exceptions sont exempte de problème. Les API fluent (très en vogues en ce moment) ne marchent pas très bien avec. Tu as des cas où il est plus agréable de faire autrement (code de retour, Optional, pour les API fluent ou de promesse avec des onException()).

                                  Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

                                  • [^] # Re: Mauvaise connaissance du c++

                                    Posté par . Évalué à 3.

                                    Moui enfin catcher une exception pour la renommer en cours de route pour en faire une qui dit exactement la même chose qui est relancé ensuite… Comment dire… Beurk!

                                    Il ne faut pas décorner les boeufs avant d'avoir semé le vent

                                    • [^] # Re: Mauvaise connaissance du c++

                                      Posté par . Évalué à 1.

                                      Et ça sert absolument à rien, surtout.

                                      "Quand certains râlent contre systemd, d'autres s'attaquent aux vrais problèmes." (merci Sinma !)

                                      • [^] # Re: Mauvaise connaissance du c++

                                        Posté par . Évalué à 4.

                                        Je vois pas comment m'expliquer plus clairement. Les choses sont liées entre elles. Si tu as une abstraction, elle doit bien avoir un sens, non ? Tu est sensé faire le même boulot d'abstraction avec tes exceptions qu'avec le reste de ton code. Ça donne un comportement cohérent à ton API.

                                        Il ne s'agit en aucun cas de faire un rethrow de l'exception tout juste encapsulée dans une nouvelle exception, mais de faire un travail de normalisation à fin de permettre à l’appelant d'utiliser ton abstraction de manière facilité et de ne pas avoir à se préoccuper de tous les cas, mais uniquement à faire un travail de présentation (pour l'exemple du dessus).

                                        Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

                                    • [^] # Re: Mauvaise connaissance du c++

                                      Posté par . Évalué à 3.

                                      qui dit exactement la même chose

                                      Ben non. Elle fait l'abstraction. Une exception comme IOException est inutilisable pour une interface graphique. Elle n'a qu'un message (voir : https://docs.oracle.com/javase/8/docs/api/java/io/IOException.html). Si tu veux avoir un message un minimum propre dans ton interface il va falloir avoir une description de l'erreur plus intelligente que ça. Par exemple un code d'erreur et une série de paramètres (le chemin ici par exemple). Le code peut te permettre d'avoir un message d'erreur localisé qui sera formaté avec les paramètres contenu dans la nouvelle exception. Tu peux aussi catégoriser l'erreur pour pour pouvoir faire des renvoies aux documentation de ton logiciel qui concernent cette partie.

                                      Exactement la même chose dis-tu ? ^^

                                      Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

                                      • [^] # Re: Mauvaise connaissance du c++

                                        Posté par . Évalué à 4.

                                        Si tu veux avoir un message un minimum propre dans ton interface il va falloir avoir une description de l'erreur plus intelligente que ça.

                                        Typiquement la localisation de l'erreur se fait au niveau de l'interface (dernier item de la chaine avant l'utilisateur), et c'est à ce moment là qu'elle est catché; devoir rajouter tout le long des couches le throws IOException, ou une encapsulation de l'erreur dans une autre n'apporte rien.

                                        Il ne faut pas décorner les boeufs avant d'avoir semé le vent

                                        • [^] # Re: Mauvaise connaissance du c++

                                          Posté par . Évalué à 3.

                                          Je ne suis pas d'accord. L'UI n'a pas à savoir qu'il peut y avoir une IOException, une SQLException ou je ne sais quoi d'autres, c'est de la logique interne de l'implémentation de ton Action que tu leak.

                                          Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

                                          • [^] # Re: Mauvaise connaissance du c++

                                            Posté par . Évalué à 3.

                                            L'ui doit remonter une information à l'utilisateur pour lui indiquer que quelque chose s'est mal passée. Tu peux te contenter d'un catch all laissant un message laconique, ou essayer d'être plus fin et donner des information utile.

                                            Enfin on parle de IOException, ça peut surtout être un FileNotFoundException.

                                            Et ça typiquement, c'est le truc que tu peux filer à l’utilisateur qui vient de sélectionner le fichier, avec une localisation qui va bien; à la rigueur avec un petit message explicatif que le fichier à été déplacé ou supprimé depuis qu'il a cliqué.

                                            Pareil si c'est un soucis d'encodage du fichier ou du format qui n'est pas le bon, c'est l'utilisateur qui à la main sur ce qu'il donne. Si y a un problème de verrou sur le fichier c'est encore lui qui sait quel fichier il peut fermer.

                                            Alors on peut lui filer un ApplicationException, avec le code du message qui va bien pour le traduire au moment de l'afficher, mais si tu avais prévu de faire des actions différente en fonction de l'erreur tu dois recréer une exception par type que tu veux traiter différemment.

                                            Et puis la fois où tu veux réutiliser des fonctions qui ont encapsulé les erreurs, mais que tu te rends compte qu'il faut en fait en spécialiser une tu te retrouves à toucher ton ancien bout de code qui n'a rien à voir avec ton évol.

                                            Alors évidemment tu peux dire que toutes tes fonctions te renvoient une GeneralException, mais au final ça ne donne aucune information.

                                            Il ne faut pas décorner les boeufs avant d'avoir semé le vent

                                            • [^] # Re: Mauvaise connaissance du c++

                                              Posté par (page perso) . Évalué à 5.

                                              Alors évidemment tu peux dire que toutes tes fonctions te renvoient une GeneralException, mais au final ça ne donne aucune information.

                                              Je pense que ce qu'essaie de dire barmic, c'est que certaines exceptions n'ont pas le droit d'être propagées en dehors de leur domaine et qu'on doit mettre en place des “superviseurs” qui montent la garde à la frontière d'un domaine et qui traduisent les erreurs d'un domaine en erreurs génériques, qui peuvent mentionner tous les détails utiles à l'utilisateur (sous forme d'un texte) ou bien proposer un option de récupération. Pour le cas particulier de l'UI d'une application habituelle, tout ce qui compte c'est de savoir qu'il y a eu une erreur, quel contexte afficher et si on peut récupérer l'erreur. Et c'est uniquement des erreurs de ce type que doit voir le code de l'UI.

                                              • [^] # Re: Mauvaise connaissance du c++

                                                Posté par . Évalué à 2.

                                                Merci :)

                                                Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

                                            • [^] # Re: Mauvaise connaissance du c++

                                              Posté par . Évalué à 2.

                                              Pareil si c'est un soucis d'encodage du fichier ou du format qui n'est pas le bon, c'est l'utilisateur qui à la main sur ce qu'il donne. Si y a un problème de verrou sur le fichier c'est encore lui qui sait quel fichier il peut fermer.

                                              Donc l'UI doit savoir faire tout ça ? Si par configuration tu as le choix entre une action qui va accéder au disque, à une base de données ou à un webservice, ton ui va devoir gérer chacun de ses cas dans un enchaînement de if/else/if ? Les exceptions te permettent de gérer chacun de ses cas dans des classes différentes et soit d'être génériques soit par polymorphisme effectuer le bon traitement.

                                              Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

                                              • [^] # Re: Mauvaise connaissance du c++

                                                Posté par . Évalué à 3.

                                                ton ui va devoir gérer chacun de ses cas dans un enchaînement de if/else/if

                                                Plutôt la couche en dessous avec des catch que des if/else if

                                                Certaines doivent être transformée avant car n'intéresse pas le niveau plus haut, mais si tu choppes ton exception pour juste la renommer "parce que", je m'insurge.

                                                Typiquement si ton parseur de nombre dans le xml te pête à la gueule te disant juste NumberFormatException, ça à du sens de la chopper au dessus disant que c'est le parsing xsd qui vient de foirer en ajoutant ligne+colonne de l'erreur. Par contre chopper toutes les Exception lors de la lecture d'un fichier pour les transformer en JYarrivePasException, non. Les actions à entreprendre peuvent dépendre du type de l’exception; donc pas au niveau de L'UI qui ne devrait faire que de l'affichage, mais juste en dessous. (par exemple ouvrir une fenêtre avec un gros point rouge sur la ligne qui a planté le parsing).

                                                Il ne faut pas décorner les boeufs avant d'avoir semé le vent

                                                • [^] # Re: Mauvaise connaissance du c++

                                                  Posté par . Évalué à 4.

                                                  Plutôt la couche en dessous

                                                  Donc dans l'Action ?

                                                  avec des catch que des if/else if

                                                  Il n'y a pas de différence, tu as un bloque de code de traitement dans ton UI par possibilité d'erreur dans l'Action et potentiellement pour la combinatoire des différentes Actions. Mieux tu peux avoir des exception identiques pour des Actions différentes qui demandent donc des réactions différentes en terme d'affichage utilisateur. Tu peux avoir une IOException pour pleins de raisons différentes et dans des cas d'accès disques ou réseau.

                                                  Certaines doivent être transformée avant car n'intéresse pas le niveau plus haut, mais si tu choppes ton exception pour juste la renommer "parce que", je m'insurge.

                                                  On va pas boucler là dessus ?

                                                  Typiquement si ton parseur de nombre dans le xml te pête à la gueule te disant juste NumberFormatException, ça à du sens de la chopper au dessus disant que c'est le parsing xsd qui vient de foirer en ajoutant ligne+colonne de l'erreur. Par contre chopper toutes les Exception lors de la lecture d'un fichier pour les transformer en JYarrivePasException, non. Les actions à entreprendre peuvent dépendre du type de l’exception; donc pas au niveau de L'UI qui ne devrait faire que de l'affichage, mais juste en dessous. (par exemple ouvrir une fenêtre avec un gros point rouge sur la ligne qui a planté le parsing).

                                                  Rien empêche d'avoir ça dans ton exception plutôt que dans le code de l'UI. Je l'ai déjà dis plus haut, mais bon. Imagine tu ta gestion d'erreur soit relativement simple et consiste à afficher un message à l'utilisateur. Si tu sors une exception qui contient :

                                                  • un identifiant d'erreur
                                                  • une liste de variables nommées qui donnent un contexte à ton erreur (pour le cas d'un xml que tu n'arrive pas à parser : ligne, colonne, nom du fichier, nom de la balise si possible, code d'erreur de ton parseur, identifiant de l'erreur,…)

                                                  Ton UI va devoir :

                                                  • attraper l'exception
                                                  • récupérer le format du message d'erreur à partir de l'identifiant de l'erreur et de la locale
                                                  • générer le texte utilisateur à partir du format qu'il vient de récupérer et des variables contenues dans l'exception

                                                  et ce bout de code sera identique à pas mal de cas. xcomcmdr sera content car la SRP sera vraiment respectée l'UI ne s'intéresse pas à savoir comment est-ce que l'Action se débrouille pour rendre son service !

                                                  Bien sûr ça n'est pas aussi simple, par exemple les parseurs XML te fournissent généralement une liste d'erreur, c'est au développeur de voir comment il veut gérer ces cas pour l'utilisateur.


                                                  Les checkeds exceptions devraient être utilisées comme ça. Elles ne sont pas parfaites, il y a pleins de cas où elles sont sous-optimales, voir carrément embêtante, mais affirmer qu'elles sont mauvaises parce que quand on les utilise mal elles sont chiantes à gérer ne me semble pas plus intéressant que ça. Il faut savoir comment elles devraient être utiliser pour pouvoir ensuite savoir quand les utiliser ou pas.

                                                  Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

          • [^] # Re: Mauvaise connaissance du c++

            Posté par . Évalué à 3.

            Sur une API donnée, j’utilise une classe d’exception dont les erreurs particulières héritent. Et je catch toujours la classe parent là où je souhaite récupérer les erreurs inattendues.

            Ex :

            class Excep;
            
            class ErrReseau : public Excep;
            
            class ErrFpu : public Excep;
            
            class ErrMem : public Excep;
            
            // Erreur ajoutée depuis l'écriture du code qui suit.
            class ErrNouvelle : public Excep;
            
            
            void fct()
            {
              try {
                 // ...
                 // On attend potentiellement une erreur réseau ou de mémoire.
              } catch (ErrReseau err)
              {
                 // Traitement erreur réseau
              } catch (ErrMem err)
              {
                 // Traitement erreur mémoire
              } catch (Excep err)
              {
                 // Traitement des erreurs inattendues.
                 // La classe Excep à une méthode virtuelle pure : std::string getMsg();
                 // Au minimum :
                 std::cerr << "Unexpected error : " << err.getMsg() << std::endl;
              }
            }
            • [^] # Re: Mauvaise connaissance du c++

              Posté par . Évalué à 3.

              Et c'est bien. Tu peux même te servir de cet arborescence pour jouer sur la granularité des erreurs et ne pas exposer cette précision dans ton API.

              Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

      • [^] # Re: Mauvaise connaissance du c++

        Posté par . Évalué à 3.

        Hum ignoré ou printStracktrace(), dans les deux cas tu as merdé ton design quelque part.

        Je sais pas, je récupère un XML, généré par l'appli, que je parse. Les chance que ce xml ne valide pas le schéma, qu'il soit mal formé ou n'ait pas la balise attendu est nulle, je dois vraiment polluer le code? Et je met quoi ? serr ("Oups si on est la c'est qu'on a vraiment merdé" );

        Là j'ai pris un exemple tangent, mais imagine que c'est une regex en dure dans le code, faut aussi se farcir de la gestion d'exception pour dire qu'elle est mal rédigée?

        Bref lorsque tu codes, tu peux faire des assertions sur ce que tu as en entrée, et tu peux être certains que là où tu passes tu n'auras jamais l'exception; devoir alourdir le code et provoquer des changement de scope (et donc de visibilité) pour des cas qui n'arrivent jamais n'est vraiment pas un gain.

        Il ne faut pas décorner les boeufs avant d'avoir semé le vent

        • [^] # Re: Mauvaise connaissance du c++

          Posté par . Évalué à 2.

          Là j'ai pris un exemple tangent, mais imagine que c'est une regex en dure dans le code, faut aussi se farcir de la gestion d'exception pour dire qu'elle est mal rédigée?

          Des idées:

          • Tu utilises, ou fait, une API adaptée a ton besoin
          • Plutôt qu'un serr tu plante ton processus par ce que tu viens de te retrouver dans un état impossible. (ça aidera aussi le prochain couillon qui après avoir oublié de mettre à jour le schema prendra une erreur propre plutôt qu'un truc qui fait semblant de marcher avec un serr ou qui plante 3kms après pour une raison inconnue)
          • [^] # Re: Mauvaise connaissance du c++

            Posté par . Évalué à 3.

            Plutôt qu'un serr tu plante ton processus par ce que tu viens de te retrouver dans un état impossible.

            C'est pour cela que j'ai tendance à préférer le stacktrace qu'on retrouvera dans les logs; ça évite de planter toute l'appli et de déconnecter sauvagement tous les utilisateurs du serveur métier.

            Tu utilises, ou fait, une API adaptée a ton besoin

            Merci, mais je ne vais pas recoder la roue; je fais avec les briques que j'ai; je ne vais pas coder une api par état non plus (telle api à utiliser dans ce cas là, telle api dans celui ci…

            Il ne faut pas décorner les boeufs avant d'avoir semé le vent

            • [^] # Re: Mauvaise connaissance du c++

              Posté par . Évalué à 2.

              C'est pour cela que j'ai tendance à préférer le stacktrace qu'on retrouvera dans les logs; ça évite de planter toute l'appli et de déconnecter sauvagement tous les utilisateurs du serveur métier.

              Le stacktrace local n'est pas une solution. Ce que tu veux c'est que la fault remonte vers le fault handler adéquat qui fera alors la reprise sur erreur attendu pour l'unité en cours d'exécution (fonctionnel et non fonctionnel):

              • Logguer / publier des métriques / créer une entrée dans le ticketing
              • Re-essayer l'unité logique
              • Planter le processus / Servir un message
              • Etc.

              Souvent quand tu as une base de code ou 90% des catch sont printstacktrace() / serr / log, tu vas aussi trouver des choses comme ça:

              long value;
              try {
                value = Long.parseLong(str);
              } catch (NumberFormatException e) {
                e.printStackTrace();
              }
              
              return value + 2;
              
              • [^] # Re: Mauvaise connaissance du c++

                Posté par . Évalué à 2.

                Alors là c'est qu'on ne passe pas sur les mêmes bases de codes; si on a un e.printStackTrace(), c'est que c'est normalement un cas qui ne doit JAMAIS arriver; si cela arrive, les unités de parse de log te remontent le machin directe chez le dev pour qu'il regarde le cas.

                Par contre comme les procédures pour installer un patch sont assez compliqué à mettre en oeuvre, on préfère garder le système en état de fonctionnement.

                J'ajouterai que ton code, ça risque de ne pas compiler (value peut être non initialisé), il faudrait au moins le mettre à 111, 421 ou 666 ou encore, ajouter un throw :)

                tiens un truc amusant pour chopper un constructor que l'on sait exister avec ensuite utilisation de ce constructor j'ai ça dans mon catch : InstantiationException | InvocationTargetException | NoSuchMethodException | IllegalAccessException

                ça en fait pas mal pour rien…

                Il ne faut pas décorner les boeufs avant d'avoir semé le vent

                • [^] # Re: Mauvaise connaissance du c++

                  Posté par . Évalué à 3.

                  c'est que c'est normalement un cas qui ne doit JAMAIS arriver; si cela arrive, les unités de parse de log te remontent le machin directe chez le dev pour qu'il regarde le cas

                  Donc le cas qui ne doit JAMAIS arriver, tu mets quand même un système pour vérifier qu'il n'arrive jamais en fait.

                  Et quand ce qui n'arrive jamais arrive, le système va tout de même avoir un comportement et puisque que tu ne veux pas que ton système plante, offre 10M$ au client ou tue un petit chaton tu vas forcément avoir un chemin d'exécution qui prend le truc en compte tu ne peux pas juste continuer après ton serr comme si de rien n'était.

                  tiens un truc amusant pour chopper un constructor que l'on sait exister avec ensuite utilisation de ce constructor j'ai ça dans mon catch : InstantiationException | InvocationTargetException | NoSuchMethodException | IllegalAccessException
                  ça en fait pas mal pour rien…

                  Effectivement => catch (ReflectiveOperationException e) ;)

                  Après non, on ne va pas défendre la plupart des API du JDK non plus faut pas pousser.

  • # moche ?

    Posté par . Évalué à 3.

    C'est assez moche comme code. En Ocaml, le type optionnel est un type somme, ce qui oblige à le décomposer, donc, c'est impossible de l'oublier ou de tomber sur un pointeur nul. Il manque ce filtrage à C++ pour avoir toutes la puissance du truc.

    Ensuite, niveau exception je déteste ça. Ne pas trouver quelques choses dans une recherche, peut être un bug ou un cas attendu, j'ai déjà eu des gros problèmes à démêler les 2 cas. Dans le cas de Ocaml, il faudra que les fonctions inclus leur exceptions dans leur signature, cela permettra au moins d'éviter des oublies.

    "La première sécurité est la liberté"

    • [^] # Re: moche ?

      Posté par (page perso) . Évalué à 2.

      C'est assez moche comme code. En Ocaml, le type optionnel est un type somme, ce qui oblige à le décomposer, donc, c'est impossible de l'oublier ou de tomber sur un pointeur nul. Il manque ce filtrage à C++ pour avoir toutes la puissance du truc.

      C'est exactement ce que je propose à la fin de l'article non ? Sauf que comme on ne peut pas "pattern matché" en C++, je propose deux fonctions utilitaires, map et bind qui font les tests et qui appellent les lambdas associés.

      • [^] # Re: moche ?

        Posté par . Évalué à 2.

        non, bind ne permet pas de vérifier que le cas "vide" est géré.

        "La première sécurité est la liberté"

        • [^] # Re: moche ?

          Posté par (page perso) . Évalué à 4.

          Ba si…

          Si tu as un std::optional<A> nommé value et une fonction de A dans std::optional<B> nommée f. Alos bind(value, f) te renvoie un std::optional<B>. Soit il y avait quelque chose dans value et il a appliqué f sur ce quelque chose, soit il n'y avait rien, et il renvoie un std::optional<B>.

          Le cas vide est donc géré et repoussé à la prochaine étape. Un jour, en fin de chaîne, une décision sera prise sur quoi faire du optional vide, une valeur par défaut, avec value_or, ou un comportement different, avec optionalCase.

  • # Remarques diverses et ... tardives

    Posté par (page perso) . Évalué à 2.

    (Je doute que ce message ait beaucoup de visibilité vu mon temps de découverte de l'article original, mais voici tout de même quelques remarques).

    De la programmation par contrat

    Tout d'abord, la Programmation par Contrat est totalement méprise—ou très mal présentée. Son objectif n'est pas de gérer des problèmes plausibles et liés à l'environnement tel qu'un fichier illisible, corrompu, ou encore des sockets plantées.
    Son objectif est de traiter les erreurs de programmation. C'est pour cela que l'on dit que c'est à l'utilisateur de vérifier le contrat d'appel (pré-conditions avant d'appeler la fonction). Appeler pop() sur une pile vide est idiot. De même que d'accéder à un élément hors bornes, ou d'exécuter sqrt(sin(x)-1) sur tout x. Ce sont autant d'erreurs dont la prévention est de la responsabilité de l'appelant.

    Je n'apprécie pas cette méthode car elle est source de nombreux bugs difficiles à trouver.

    A ce sujet, je préfère 100 fois une assertion là pour détecter une précondition non remplie qu'une exception pour analyser ce qu'il se passe. En terme d'investigation, c'est un vrai bonheur à contrario de toutes les alternatives dynamiques (pour repérer et tordre le coup aux erreurs de programmation en C&C++, donc). Je suis d'accord accessoirement sur le fait que les types opaques c'est encore plus mieux. Mais cela complexifie les choses car un strictlypositive<> * strictlynegative<> donne un strictlynegative<>, mais quid de l'addition ? (Ou alors, il faut faire comme avec boost.unit et avoir une arithmétique sur des range<min, max>. Hum… C'est tordu, mais ça peu m'amuser à investiguer.)

    Bref, je me suis déjà longuement étendu sur le sujet par ici: http://luchermitte.github.io/blog/2014/05/24/programmation-par-contrat-un-peu-de-theorie/ (série de 3 billets)

    Des exceptions dans la SL

    Il y a effectivement peu d'endroits où la SL choisit de lancer des std::logic_error, qui sont des exceptions qui signifient "erreur de programmation". En général, une approche pure contrat, mais pas toujours instrumentée est employée. Cf les "STL checkées" sur le sujet. Et j'espère qu'après le C++17 si les évolutions sur les contrats sont validées, toute la SL sera annotée pour spécifier les contrats de toutes les fonctions qui en ont.

    De mon avis, std::vector<>::at() est une hérésie qui n'aurait jamais du exister.

    Ailleurs, s'il y a des choses qui peuvent vraiment échouer, des exceptions de runtime (dans la terminologie C++, je sais que runtime error veut dire le contraire dans d'autres langages) seront lancées—d'autres exemples ont été donnés. J'ai envie de dire que quelque part, c'est plus nos métiers qui vont vraiment détecter des situations exceptionnelles (et plausibles) et à ne pas confondre avec des erreurs de programmation.

    A propos d'optional et des erreurs

    Si dans le contexte d'une fonction de recherche, un retour optionnel (qui ne signifie pas forcément "erreur") a du sens, dans le contexte de retour d'erreurs de runtime (au sens C++ donc), optional n'est pas un bon outil car il ne porte en lui aucun contexte. Et tu nous montres ici à quel point les enchainements ne sont pas propres/simples.

    Je vous invite plutôt à vous tourner vers des types comme expected. Il y a eu un article sur le sujet en 2012 et de l'encre électronique a coulé (pour encore plus de monadification de la bête) après ça :
    - la vidéo de la conf: http://channel9.msdn.com/Shows/Going+Deep/C-and-Beyond-2012-Andrei-Alexandrescu-Systematic-Error-Handling-in-C
    - les slides: https://onedrive.live.com/?cid=F1B8FF18A2AEC5C5&id=F1B8FF18A2AEC5C5%211158&parId=root&o=OneUp

    Pour le coup, c'est fait pour. Et c'est plus proche du genre de type que cet article recherche.

    • [^] # Re: Remarques diverses et ... tardives

      Posté par (page perso) . Évalué à 1.

      Il me semblait bien que j'avais vu passer des choses en plus de la conf d'Andrei Alexandrescu: une proposition d'évolution pour disposer d'un type std::expected<> est en cours: http://open-std.org/Jtc1/sc22/wg21/docs/papers/2016/p0323r1.pdf

      Il est présenté comme une généralisation de std::optional.

      • [^] # Re: Remarques diverses et ... tardives

        Posté par . Évalué à 1.

        Effectivement c'est une meilleure approche si on veut plus d'information sur la raison de l'échec.

        Dans le même genre, à la place des Maybe a, en Haskell on peut utiliser les data Either a b = Left a | Right b.

        Et en Ocaml, il y a soit le type 'a option = None | Some of 'a soit le type ('a, 'b) result = Ok of 'a | Error of 'b.

        Ce qui correspond à la classe std::expected<T,E> proposée pour le C++ :

        Class template expected<T,E> proposed here is a type that may contain a value of type T or a value of type E in its storage space. T represents the expected value, E represents the reason explaining why it doesn’t contains a value of type T. The interface and the rational are based on std::optional N3793. We can consider expected<T,E> as a generalization of optional<T>.

        Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.

  • # Faire des monades en C++

    Posté par . Évalué à 3.

    Comme le commentaire du dessus, je suis en retard par rapport à la date de publication du journal : je l'ai découvert via la dépêche sur les contenus primés de septembre.

    Je voudrais juste signaler un article sur les monades en C++ étant donné que c'est ce qui est présenté en fin d'article pour gérer les std::optional via une option monad.

    L'article compare les deux approches entre Haskell et C++, ce qui pourrait intéresser l'auteur du journal qui, je le sais, apprécie particulièrement le langage Haskell.

    Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.

Suivre le flux des commentaires

Note : les commentaires appartiennent à ceux qui les ont postés. Nous n'en sommes pas responsables.