Journal C++ Core Guidelines

Posté par . Licence CC by-sa
Tags :
32
2
oct.
2015

En ce moment a lieu la conférence CPPCon qui regroupe la crème des acteurs du C++ et de son comité de normalisation. Au cours de cette conférence, Herb Sutter (le responsable du comité de normalisation, travaillant à Microsoft) et Bjarne Stroustrup (inventeur du C++) ont donné deux exposés sur la manière de bien écrire du C++.

Ils en ont profité pour dévoiler un guide des règles de bonne conduite pour C++ accompagné d'une bibliothèque, GSL (Guideline Support Library) permettant de mettre en pratique de manière très concrète ces recommandations. Microsoft a fourni une première implémentation (sous licence MIT) mais d'autres implémentations ont déjà vu le jour.

Que penser de ces règles ? Il y a beaucoup de règles connues par tous ceux qui ont pratiqué le C++ suffisamment longtemps, mais c'est une bonne idée de les avoir regroupées dans un seul endroit. En revanche, je suis beaucoup plus circonspect sur la bibliothèque GSL. Beaucoup de choses dans cette bibliothèque sont quasiment déjà dans la bibliothèque standard ou vont y arriver sous peu. Un des arguments pour défendre cette vision est de dire qu'il faut utiliser le plus tôt possible. Or, dès le départ, les règles préconisent l'utilisation de C++14, alors que le compilateur de Microsoft n'est pas encore capable de compiler du C++11 standard pour des raisons complètement surréalistes (ils n'avaient jusqu'à présent même pas d'arbre syntaxique abstrait !). Bref, même s'il y a de bonnes idées dans cette bibliothèque, je préférerais nettement qu'elles soient intégrées au futur C++17 et donc discutées collectivement. La bonne nouvelle, c'est que ce document (et donc cette bibliothèque) est encore en discussion et donc des évolutions sont possibles.

C++ reste un langage très vivant qui évolue très bien avec son âge.

  • # À suivre?

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

    La chaine avec les vidéos des conférences sur Youtube : https://www.youtube.com/user/CppCon

    Je trouve intéressant l'outil dont Herb Sutter faisait la démonstration dans sa conférence "Writing good C++14… by default" (mais qui, il me semble, n'est pas encore disponible), et qui permet visiblement de s'assurer qu'il n'y a pas de soucis de dangling pointers. Ce qui est assez rigolo c'est que ça se rapproche assez de ce que Rust peut faire (avec des différences évidemment, et pas tout à fait les mêmes objectifs non plus je crois).

    Par contre je me demande ce que ça va vraiment donner en pratique : certes (par rapport à Rust notamment) il y a l'avantage que ça reste du C++, que tu peux continuer à utiliser le code existant ; d'un autre côté est-ce qu'il est vraiment possible de profiter des garanties de sécurité dans ce cas là ? Et la question bonus : est-ce que ça va vraiment être utilisé en pratique, ou est-ce que ça revient pas au final à créer un "dialecte" de C++ qui aura autant de mal à s'imposer qu'un nouveau langage ?

    • [^] # Re: À suivre?

      Posté par . Évalué à 2.

      Et la question bonus : est-ce que ça va vraiment être utilisé en pratique, ou est-ce que ça revient pas au final à créer un "dialecte" de C++ qui aura autant de mal à s'imposer qu'un nouveau langage ?

      Pour certains point du C++11/14, c'est clair qu'ils seront adoptés (lambda, auto, for(auto &obj, v)…) pour d'autre, je crains surtout une orgie de shared_ptr :(

      D'autres me semblent plus tendu à mettre en place (typiquement les move).

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

      • [^] # Re: À suivre?

        Posté par . Évalué à 2.

        D'autres me semblent plus tendu à mettre en place (typiquement les move).

        Au contraire, les move sont utilisés. Par exemple, si on utilise des retour de fonction par valeur plutôt que par argument, on va utiliser les move. Et il y a plein de cas où ça sert, comme les constructeurs. Petit exemple :

        struct Foo1 {
          Foo1(const std::string& bar) : m_bar(bar) { }
          std::string m_bar;
        }
        
        struct Foo2 {
          Foo2(std::string bar) : m_bar(std::move(bar)) { }
          std::string m_bar;
        }
        
        Foo1 f1("toto");
        Foo2 f2("toto");
        
        std::string qux("qux");
        Foo1 f3(qux);
        Foo2 f4(qux);

        Dans le cas de f1, tu crée un objet std::string et tu fais une copie dans m_bar (donc allocation et copie du contenu). Dans le cas de f2, tu crées un objet std::string et tu le déplaces donc pas d'allocation et pas de copie de contenu, juste un pointeur. Donc, dans ce cas là, tu es gagnant à utiliser un std::move. Et dans le cas où tu utilises une chaîne, ça ne change presque rien : dans f3 tu fais la copie dans le constructeur, dans f4 tu fais la copie au moment de l'appel.

        • [^] # Re: À suivre?

          Posté par . Évalué à 3.

          Je n'ai pas dit que c'était inutile, au contraire, mais écrire ses propre move operator, pour ses propres classes demande une certaine discipline que je ne vois pas trop chez mes collègues, déjà qu'on est en boost::shared_ptr partout, alors que la réponse à la question "Qui possède l'objet?" est évidente.

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

          • [^] # Re: À suivre?

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

            mais écrire ses propre move operator, pour ses propres classes demande une certaine discipline

            Foo1(Foo1&&) = default;

            Ça va, c’est assez facile.

            (Et si c’est une classe qui a des membres non-movables ou un peu complexes (disons que ça possède un raw pointer, pour une quelconque raison), oui faut l’écrire à la main, mais les classes de ce genre sont censées être petites et ne gérer qu’une seule chose, donc même dans ces cas c’est assez facile à écrire, et c’est surtout rare)

        • [^] # Re: À suivre?

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

          On peut pinailler en disant que ça n'est pas inconditionnellement meilleur: dans le cas où std::string utilise la small string optimization (cad que l'objet contient l'espace pour stocker une petite chaine, ce qui est le cas pour la std::string de g++ 5.1) , f3(qux) sera plus efficace que f4(qux) (si qux est suffisament courte)

        • [^] # Re: À suivre?

          Posté par . Évalué à 3.

          Dans le cas de f2, tu crées un objet std::string et tu le déplaces donc pas d'allocation et pas de copie de contenu, juste un pointeur.

          Donc ce n'est vraiment pas anodin ! Dans ton exemple si je modifie qux, je modifie aussi f4 ?

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

          • [^] # Re: À suivre?

            Posté par . Évalué à 2.

            Dans ton exemple si je modifie qux, je modifie aussi f4 ?

            Non, puisque tu as fait une copie, donc la copie n'est pas modifiée.

            • [^] # Re: À suivre?

              Posté par . Évalué à 3.

              Une fois tu dis qu'on ne copie pas et une fois qu'on copie.

              Si je crée un constructeur par copie, d'après toi je ne fais plus que de la manipulation de pointeur, donc les objets qui sont passés en paramètre du constructeur permettent d’accéder à l'état interne de tes objets.

              Je comprends l'idée de privilégier la vitesse à la sécurité, mais c'est un point qu'il faut garder en tête (tout comme dans le cas inverse il faut garder en tête qu'il y a une recopie de donnée).

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

              • [^] # Re: À suivre?

                Posté par . Évalué à 1.

                Si je dis pas de bêtise, à part rares exceptions, tu utilises le move sur un argument si il est de type right reference (&&). Donc effectivement son exemple semble un peu boiteu. Il sert a compléter l'utilisation par exemple dans un constructeur, de const &, qui permet d'eviter la copie d'un objet mais ne marche pas si c'est le retour d'une autre fonction.
                
      • [^] # Re: À suivre?

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

        Je parlais de GSL, et de ce que présente Herb Sutter dans sa conf https://www.youtube.com/watch?v=hEx5DNLWGgA où il y a quand même des trucs un peu spécifiques (comme les annotations sur les lifetimes) et des trucs qui m'ont l'air « un peu pareil mais pas avec le même nom ».

        Après j'avoue que je m'y connais pas des masses en C++ récent, mais j'ai l'impression que ça risque de « fractionner » un peu le langage entre différentes écoles sur la façon moderne d'écrire du C++.

        • [^] # Re: À suivre?

          Posté par . Évalué à 2.

          Après j'avoue que je m'y connais pas des masses en C++ récent, mais j'ai l'impression que ça risque de « fractionner » un peu le langage entre différentes écoles sur la façon moderne d'écrire du C++.

          Le c++ est déjà fractionné, selon les projets on utilise pas le même sous ensemble, je suis même tombé sur des projets où l'on n'avait pas le droit de créer des template (on pouvait quand même utiliser ceux de la STL); sur d'autres on autorise boost, sur d'autre on utilise Qt, bref un sous ensemble de plus ou de moins ne change pas grand choses à la donne.

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

      • [^] # Re: À suivre?

        Posté par . Évalué à 3.

        c'est clair qu'ils seront adoptés (lambda, auto, for(auto &obj, v)…)

        Rien à faire, les lambda je n'arrive pas à trouver ça mieux que de vrais fonctions.
        En terme de lisibilité, une fonction à un nom, une doc, je peut me faire une idée de ce qu'elle fait sans avoir besoin de déchiffrer son code.
        En terme de maintenabilité, de déboguage et d'évolution, je peux mettre un point d'arrêt dans une fonction, je peux en écrire 3 versions et choisir à la compilation, ou à l'exécution, voir dans mon débogueur laquelle va être utilisée, …
        C'est quoi les arguments en faveur des lambda ?

        • [^] # Re: À suivre?

          Posté par . Évalué à 4.

          C'est quoi les arguments en faveur des lambda ?

          Je ne sais pas ce qu'il en est pour le C++, mais en Java j'utilise surtout beaucoup les références de méthodes. Du coup j'utilise des méthodes comme des lambda.

          class A {
              public int foo() { /* ma méthode */ }
          
              public Collection<Integer> bar(Collection<A> as) {
                  return as.stream()
                           .map(A::foo) /* ici je donne une référence de méthode comme lambda */
                           .collect(Collectors.toList());
              }
          }

          D'ailleurs les lambda aussi peuvent avoir un nom :

          Function<A, B> myName = a -> new B(a);
          Function<A, B> myName2 = B::new; // identique à au dessus

          Pour ce qui est de ton debogage c'est un problème de ton debogueur.

          Pour la lecture, le fait d'avoir au même endroit permet de ne pas avoir à mettre ceinture et bretelles. Si tu a validé tes données dans le code appelant inutile de le faire dans le code appelé. Au lieu de fonctionner en boite noire avec des méthodes qui s’enchaînent un peu trop, tu regroupe un peu ton code utile ensemble.

          Les lambdas permettent des choses intéressantes comme l'évaluation paresseuse de paramètre ou l'utilisation d'API de plus au niveau pour manipuler les conteneurs (avec des map(), filter(), find(), etc).

          Mais sinon oui, les lambda faut les utiliser avec parcimonie.

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

          • [^] # Re: À suivre?

            Posté par (page perso) . Évalué à 0. Dernière modification le 05/10/15 à 15:05.

            Les lambdas permettent des choses intéressantes comme l'évaluation paresseuse de paramètre ou l'utilisation d'API de plus au niveau pour manipuler les conteneurs (avec des map(), filter(), find(), etc).

            Et je rajouterai la parallélisation, où un lambda t'autorise à isoler/filtrer proprement une section parallèle et ses paramètres.

            • Ça diminue le risque de corruption / sharing involontaire / deadlock ( cf parallel_for )
            • Ça permet de découpler proprement ton code en taches distincts en suivant le modèle de async/future.
        • [^] # Re: À suivre?

          Posté par . Évalué à 4.

          C'est quoi les arguments en faveur des lambda ?

          ça t'évite de devoir faire des bind ou autre truc chiants lorsque tu rédige ta fonction, tu peux capturer les membres locaux par référence (ou copie)…

          Lorsque dans ton projet tu te retrouves avec 50 fonctions de tri/tests avec des bind pour gérer les conditions ( bon dans certains cas y a juste des objets de tests avec l'operator() défini, et les membres qui vont bien), mais les lamdas donnent une certaine souplesse lors de la rédaction.

          Un autre intérêt des lambdas est lorsqu'on veux exécuter du code en fin de bloc, quel que soit le chemin utilisé (exception, return, fin de scope)… l'équivalent du finally en java.
          Soit tu mets ton code (ou ta fonction avec toutes les références nécessaires) et tu l'appelle partout où tu risque de sortir (avec des try{}/catch()), en espérant ne pas en avoir loupé, soit tu fais un bête

          auto cleanup = finally([&]{ free(bidule); });

          Note bien, c'est juste un exemple, ça peut être n'importe quoi.

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

        • [^] # Re: À suivre?

          Posté par . Évalué à 1.

          Les lambdas permettent de définir du code ayant une portée du bloc en cours.
          C'est utile par exemple dans les fonctions asynchrones, si je donne une fonction pour le retour (std::function si possible), un lambda peut contenir le contexte.

          En tant que prédicat c'est aussi très flexible, on peut exprimer ce que l'on veut sans le mettre dans l'interface de l'objet.

    • [^] # Re: À suivre?

      Posté par . Évalué à 2.

      mais qui, il me semble, n'est pas encore disponible

      Justement, c'est dans GSL dont je parle dans le journal, donc c'est disponible. J'ajoute aussi que l'idée derrière GSL, c'est aussi que les compilateurs puissent faire plus de vérification (puisque pour certaines structures de GSL, c'est un simple renommage donc c'est juste une annotation, ça n'a aucun impact sur les perfs).

      • [^] # Re: À suivre?

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

        Ah je croyais qu'il y avait d'un côté la bibliothèque que tu peux utiliser pour ton code et de l'autre un outil d'analyse (qui utilise cette bibliothèque pour te prévenir que ce que tu fais est peut-être une erreur).

        • [^] # Re: À suivre?

          Posté par . Évalué à 2.

          Oui, c'est bien le cas. La bibliothèque existe déjà, alors que les outils d'analyse (éventuellement inclus dans le compilateur), ils n'existent pas encore.

  • # GNU Scientific Library (ou GSL)

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

    une bibliothèque, GSL (Guideline Support Library)

    À ne pas confondre avec la GNU Scientific Library (ou GSL), qui est en C.

  • # Typos

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

    Tout d'abord : merci pour le journal.

    Quelques corrections pour un gentil modérateur qui passerait par là :

    • qui regroupent → qui regroupe
    • Bjarne Stroutrup → Bjarne Stroustrup
    • à fourni → a fourni
    • règles connus → règles connues
    • !! → ! ou !!!

    Debian Consultant @ DEBAMAX

Suivre le flux des commentaires

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