Journal C++ Hell/Heaven et les concepts

Posté par  (site Web personnel) . Licence CC By‑SA.
Étiquettes :
11
17
sept.
2020

Salut à tous,

C++20 apporte les concepts, mais par pure nostalgie, regardons comment émuler ça en C++11

#include <utility>

#define REQUIRES(x) class _ = decltype(x)

template<class T,
         REQUIRES(((std::declval<T>()==0), T(1), std::declval<T>() * (std::declval<T>() -1)))>
T fact(T const& n) {
    return n == 0 ? T(1) : n * fact(n -1);
}

auto x = fact(3);

// auto y = fact("er");

Dans le REQUIRES, on fout juste les expressions qu'on souhaite être valides pour note fonction, et hop, si une ds expressions est non valide, on a un message d'erreur et la fonction sera ignorée lors de la recherche des surcharges possibles de la fonction.

Note : petit code sans prétention, faillible a plein d'égards, mais pas moins amusant

  • # Typage structurel

    Posté par  . Évalué à 2 (+0/-0).

    Ça me fait beaucoup penser à du typage structurel, il y a une différence qui m'échappe ?

    • [^] # Re: Typage structurel

      Posté par  (site Web personnel) . Évalué à 2 (+0/-0). Dernière modification le 17/09/20 à 15:44.

      Ça me fait beaucoup penser à du typage structurel, il y a une différence qui m'échappe ?

      Ça l'est: Les concepts sont une forme de typage structurel si c'est ta remarque.

      On peut voir les concepts comme des assertions sur du typage structurel exécuté à la compilation.

      Le typage structurel a toujours existé en C++. Mais avant C++20, il était basé principalement sur des assomptions implicites et les : Il n'était pas déclaré de manière formelle.

      Ce qui indirectement était souvent responsable des erreurs de compilation de 45km légendaires en C++

      • [^] # Re: Typage structurel

        Posté par  . Évalué à 2 (+0/-0).

        Mais avant C++20, il était basé principalement sur des assomptions implicites et les

        Tu aurais un exemple pour que ce soit plus clair pour moi ?

        • [^] # Re: Typage structurel

          Posté par  (site Web personnel) . Évalué à 3 (+1/-0).

          template <typename T>
          void foo(T a)
          {
             a.baz();
          

          };

          Si plus loin tu appelle foo(10), on va récuperer une erreur lors de a.baz() disant que le type int n'a pas de méthode baz. Les assomptions sur T sont implicites. Et le problème c'est que cela peut se propager très très loin. Si l'appel à baz se fait après plusieurs résolution de type / appels de fonctions qui travaillent sur T, tu vas récupérer une erreur dans un bout de code que tu ne maîtrise pas du tout et largement hors du contexte. Alors que on peut supposer que le code de foo est correct et que le problème est au niveau de a.baz().

          En étant plus explicite sur le type de foo, l'erreur sera sans doute plus ciblée.

          • [^] # Re: Typage structurel

            Posté par  . Évalué à 2 (+0/-0).

            Ah oui je vois très bien. Je suis un peu trop habitué à Java qui n'a pas du tout ce comportement.

            Pour le problème de l'erreur c'est ce qui cause les erreurs très longues, non ? C'est aussi quelque chose qui peut être embêtant avec les langages qui utilisent massivement l'inférence de type. Si tu ne prends vraiment jamais le temps de définir tes types, une erreur peut se montrer que très loin.

            • [^] # Re: Typage structurel

              Posté par  . Évalué à 2 (+0/-0).

              L'inférence de types est un mécanisme qui permet à un compilateur ou un interpréteur de rechercher automatiquement les types associés à des expressions, sans qu'ils soient indiqués explicitement dans le code source.

              C'est bien le souci.
              Les templates sont censés être génériques, donc deviner le type. C++11 a sauvé ce qu'il restait de ma santé mentale, au point que je l'ai adopté en 2008 pour mes projets perso… et je n'ai toujours pas adopté le C++17 en 2020!
              C++11 (et clang aussi, ne pas oublier clang putain) a considérablement amélioré ces horreurs d'erreurs de templates (même si le gcc que j'utilise est toujours à la traîne sur le sujet).

        • [^] # Re: Typage structurel

          Posté par  . Évalué à 2 (+1/-0).

          Quand une classe/fonction attend comme paramètre une classe ou un objet ayant une particularité. En c++ 03 il n'y a aucun moyen de la préciser.

          Si la contrainte n'est pas respectée le compilateur renvoie alors un message d'erreur au moment de l'utilisation de la particularité. Ce qui était rarement clair.

          En C++ 20 on peut préciser lors de la déclaration de la classe/fonction ce qui est attendu.

          Ex pour une fonction similaire à std::find_if qui attend qui attend un objet 'p' possédant un fonction "comp()" retournant un bool.

          En C++ 03:

          template <class T, class P > bool MyFind(T first, T last, P p);

          la classe P n'a aucune contrainte précisée dans la définition de la fonction.

          En C++ 20:

          template< typename V ,typename T >
          concept MyPredicate = requires(T a, V v ) 
          {
           { v.comp(*a) } -> std::convertible_to<bool>;
          };
          
          template <class T, MyPredicate<T> P >
          bool MyFind(T first, T last, P p);

          En C++ 20 on peut définir MyPredicate avec la contrainte obligeant l'objet à posséder une fonction "comp()".

          Si jamais la condition n'est pas respectée, voici le genre d'erreurs retournées dans GCC 10:

          ../macro.cpp: In instantiation of ‘bool MyFind(T, T, P) [with T =     __gnu_cxx::__normal_iterator<int*, std::vector<int> >; P = MyCompare]’:
          ../macro.cpp:48:42:   required from here
          ../macro.cpp:28:9:   required for the satisfaction of ‘MyPredicate<P, T>’ [with P = MyCompare; T = __gnu_cxx::__normal_iterator<int*, std::vector<int, std::allocator<int> > >]
          ../macro.cpp:28:23:   in requirements with ‘T a’, ‘V v’ [with V = MyCompare; T = __gnu_cxx::__normal_iterator<int*, std::vector<int, std::allocator<int> > >]
          ../macro.cpp:30:10: note: the required expression ‘v.comp((* a))’ is invalid
                     30 |  { v.comp(*a) } -> std::convertible_to<bool>;
          
          • [^] # Re: Typage structurel

            Posté par  . Évalué à 2 (+0/-0).

            Oui au final les concepts c'est moins l'ajout d'un typage structurel que la possibilité de le contractualiser.

          • [^] # Re: Typage structurel

            Posté par  (site Web personnel) . Évalué à 2 (+3/-3).

            Euh… ca fait quand même bizarre, j'ai l'impression de devoir faire à la main un truc que le compilo pourrait me faire automatiquement avec un message moins abscons, car le message d'erreur semble dire la même chose que la contrainte manuelle, juste que le message est formaté dans un style super artificiel alors qu'il pourrait être plus sympathique (le message dit en gros qu'il manque la comparaison).

            Qu'ai-je loupé?

            • [^] # Re: Typage structurel

              Posté par  . Évalué à 4 (+2/-0).

              Qu'ai-je loupé?

              • tu peux valider au plus tôt tes entrées
              • tu as une validation complète et pas uniquement le premier problème rencontré
            • [^] # Re: Typage structurel

              Posté par  . Évalué à 5 (+3/-0). Dernière modification le 18/09/20 à 01:19.

              Qu'ai-je loupé?

              T'as le message d'erreur d'un type checker au lieu d'avoir une stack trace à la python. En gros, le système de template c'était du lamba-calcul non typé, ils y ont rajouté un système de types. Le langage est interprété par le compilateur : en l'absence de typage statique, tu as une stack trace lors de l'appel, avec un système de types tu as tous les avantages du typage statique.

              Par contre le choix du nom pour la fonctionnalité n'est pas très judicieux : un concept c'est un type (les deux mots sont synonymes), donc des concepts il y en avait déjà depuis le début en C++. Là, cela revient à paramétrer du code par un type muni de certaines opérations : ça s'appelle une algèbre. ;-)

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

            • [^] # Re: Typage structurel

              Posté par  (site Web personnel) . Évalué à 6 (+4/-0).

              J'ai l'impression de devoir faire à la main un truc que le compilo pourrait me faire automatiquement

              Qu'ai-je loupé?

              Pas grand chose à vrai dire.

              En premier lieu, les concepts vont te permettre de forcer "un peu plus" que ce que ta fonction demande vraiment. Par exemple si ta fonction ressemble à:

              template T Foo(T a, T b){
              return a + b;
              }

              Le compilateur sait que tu veux un operator+ sur T, mais c'est tout. Avec un concept tu pourra aussi demander à ce que + soit commutatif.

              En second lieu, le compilateur sait des choses en observant ton code. De la même manière que tu sais des choses en observant un code. Mais il y a une partie ambigu qu'un lecteur (humain ou compilateur) ne pourra pas décider sans aide. Si j'appelle Foo en lui passant pour T une table qui ne possède pas d'opérateur +.

              Est-ce le code de Foo qui est faux ? Est-ce que type T devrait avoir un opérateur + ou est-ce que c'est mon appel qui est faux et je voulais en fait additionner le prix des tables ? Pour chacun c'est une erreur différente avec une position différente.

              En précisant grace à un concept ce que tu attend, tu vas pouvoir supprimer tout un ensemble d'erreur, réduisant ainsi le bruit.

              Bref, c'est autant un moyen d'améliorer les erreurs que de rajouter (facilement) des contraintes additionnelles.

              • [^] # Re: Typage structurel

                Posté par  . Évalué à 3 (+1/-0).

                Avec un concept tu pourra aussi demander à ce que + soit commutatif.

                C'est fascinant ça. Mais comment on prouve au type checker que l'opérateur + que l'on définit est bien commutatif ? On se contente de l'affirmer sans preuve ? Dans les faits, elle ressemble à quoi la syntaxe pour ce genre de propriété ?

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

                • [^] # Re: Typage structurel

                  Posté par  (site Web personnel) . Évalué à 2 (+0/-0).

                  Mais comment on prouve au type checker que l'opérateur + que l'on définit est bien commutatif ?
                  Dans les faits, elle ressemble à quoi la syntaxe pour ce genre de propriété ?

                  En coq, cela pourrait ressembler à ça:

                  Theorem plus_est_commutatif: forall a, b. a + b = b + a.
                  Proof. Admitted.

                  ;) Mais je pense amicalement que tu me provoques un peu sachant que tu sais cela très bien.

                  Plus sérieusement,

                  On se contente de l'affirmer sans preuve ?

                  Oui. En Haskell, chaque "class" (qui est ce qui se rapproche le plus des concepts du C++) vient souvent avec des "lois" qu'un utilisateur peut s'attendre à voir pris en compte par tout type de la class. Par exemple map f (map g l) doit être la même chose que map (f.g) l. Appliquer g puis f sur un chaque élément de "la collection" l doit renvoyer la même chose qu'appliquer la composition de f et g (f.g) sur chaque élément de l.

                  Mais le langage ne le vérifie absolument pas. C'est laissé à la charge du développeur d'être cohérent et de s'assurer du respect des lois. Il existe des outils de tests qui peuvent tenter de prouver les propriétés, soit avec un solveur type z3, soit par génération de valeur aléatoires.

                  Mais alors, cela apporte quoi de plus ?

                  C'est juste un problème d'interface ou de contrat que tu passes avec ton développeur et la libraire. Si on reprend l'exemple de la commutativité, si en tant que développeur je sais que le type générique (e.g. template) que j'utilise est commutatif pour mon opération, alors je peux me permettre certaines choses. On peut me mentir, mais au moins j'aurais prévenu. Inversement, en demandant que le type en entrée soit commutatif, je préviens l'utilisateur et il ne peut pas rater la contrainte que je lui demande.

                  • [^] # Re: Typage structurel

                    Posté par  . Évalué à 3 (+1/-0).

                    Mais je pense amicalement que tu me provoques un peu sachant que tu sais cela très bien.

                    Ce n'était pas de la provocation, j'étais bien sérieux avec ma question. Je me doutais bien que cela devait être affirmé sans preuve, mais j'ai préféré avoir confirmation. Là où j'étais un peu railleur, c'est en te demandant la syntaxe choisie pour exprimer cela (de manière générale je me demandes quels genres de psychotropes prennent les responsables du standard C++, mais ils devraient réduire la dose, j'ai rarement vu une syntaxe aussi laide).

                    C'est juste un problème d'interface ou de contrat que tu passes avec ton développeur et la libraire. Si on reprend l'exemple de la commutativité, si en tant que développeur je sais que le type générique (e.g. template) que j'utilise est commutatif pour mon opération, alors je peux me permettre certaines choses. On peut me mentir, mais au moins j'aurais prévenu. Inversement, en demandant que le type en entrée soit commutatif, je préviens l'utilisateur et il ne peut pas rater la contrainte que je lui demande.

                    Ça se tient. C++ étant orienté performance, avoir la commutativité permet de choisir un parcours de données plus efficace pour optimiser l'usage du cache du CPU, par exemple.

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

                    • [^] # Re: Typage structurel

                      Posté par  (site Web personnel) . Évalué à 3 (+1/-0).

                      Là où j'étais un peu railleur, c'est en te demandant la syntaxe choisie pour exprimer cela (de manière générale je me demandes quels genres de psychotropes prennent les responsables du standard C++, mais ils devraient réduire la dose, j'ai rarement vu une syntaxe aussi laide).

                      ;)

                      Ça se tient. C++ étant orienté performance, avoir la commutativité permet de choisir un parcours de données plus efficace pour optimiser l'usage du cache du CPU, par exemple.

                      La commutativité et l'associativité sont des points super importants en calcul sur des gros volumes. Que ce soit en C++ ou en n'importe quoi.

                      • Associativité: tu peux séparer en morceaux indépendants
                      • Commutativité: l'ordre n'est pas important, ainsi tu peux traiter les morceaux quand ils arrivent et ne pas dépendre de la latence réseau par exemple.
                      • [^] # Re: Typage structurel

                        Posté par  . Évalué à 3 (+1/-0).

                        La commutativité et l'associativité sont des points super importants en calcul sur des gros volumes. Que ce soit en C++ ou en n'importe quoi.

                        • Associativité: tu peux séparer en morceaux indépendants
                        • Commutativité: l'ordre n'est pas important, ainsi tu peux traiter les morceaux quand ils arrivent et ne pas dépendre de la latence réseau par exemple.

                        Ce n'est pas moi qui dirait le contraire : ces propriétés permettent d'effectuer les calculs dans n'importe quel ordre, i.e. quelque soit la permutation effectuée sur la séquence d'opérations on aboutit toujours au même résultat. Ainsi, on est moins tributaire du temps d'accès aux données (peu importe l'ordre dans lesquelles elles arrivent) que ce soit du à de la latence réseau, ou à des échanges répétés entre le cache et la ram. ;-)

                        ;)

                        Il y a une série d'articles de blogs (compilé en un livre) sur le thème théorie des catégories pour les programmeurs. L'auteur y donne des illustrations de code en Haskell et C++ : c'est dingue à quel point la syntaxe du C++ est tordue et peu « parlante ». Si l'on prend la notion élémentaire de monoïde (cf chapitre 3), en Haskell cela se définit ainsi :

                        class Monoid m where
                            mempty  :: m
                            mappend :: m -> m -> m

                        autrement dit un monoïde sur un type m est la donnée d'un élément neutre et d'une opération associative (important l'associativité, même si le contrat est implicite entre les programmeurs ;-). Là où en C++, avec les concepts, on se retrouve à le définir ainsi :

                        template<class T>
                          T mempty = delete;
                        
                        template<class T>
                          T mappend(T, T) = delete;
                        
                        template<class M>
                          concept bool Monoid = requires (M m) {
                            { mempty<M> } -> M;
                            { mappend(m, m); } -> M;
                          };

                        Non, mais what the fuck !? :-o

                        Par comparaison, en ML, c'est proche du Haskell mais plus conforme à la définition formelle des mathématiciens :

                        module type Mondoid = struct
                          type t
                          val mappend : t -> t -> t
                          val mempty : t
                        end

                        c'est-à-dire la donnée conjointe d'un type (ou concept, ou ensemble…), d'une loi de composition interne associative et d'un élément neutre pour cette loi. La différence avec Haskell étant que le type fait partie du dictionnaire, là où en Haskell le type support du monoïde paramétrise le dictionnaire qui n'a que deux éléménts. C'est similaire à ce type produit :

                        type 'm monoid = {
                          mempty : 'm ;
                          mappend : 'm -> 'm -> 'm
                        }

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

          • [^] # Re: Typage structurel

            Posté par  (site Web personnel) . Évalué à 3 (+1/-0).

            Moi ça me rappelle les generics en Ada

            generic
              type Element_T is private;
              with function "*" (X, Y: Element_T) return Element_T;
            function Square (X : Element_T) return Element_T;

            Ai-je bien compris ?
            Peut-on faire des trucs encore plus forts avec les concepts que juste spécifier les opérations nécessaires ?

            • [^] # Re: Typage structurel

              Posté par  . Évalué à 3 (+1/-0).

              Ai-je bien compris ?

              Oui c'est cela, mais plus proche des packages génériques : c'est tout une section de code qui peut être paramétrée, pas seulement une fonction. Mais les concepts n'ajoutent rien de nouveau là-dessus, c'est ce qu'on toujours fait les template. Les concepts c'est juste un système de types pour les template. Le langage des template est interprété par le compilateur et, avant, lorsqu'il y avait une problème lors de l'instantiation d'une template, il y a avait l'équivalent d'une stack trace en guise de message d'erreur, qui pouvait faire 3km de long si le code utilisé était très générique. Là, ils auront des messages plus clairs du au système de type.

              Peut-on faire des trucs encore plus forts avec les concepts que juste spécifier les opérations nécessaires ?

              Je ne crois pas, mais on peut les utiliser en dehors des templates. Par exemple, pour contraindre le mécanisme d'inférence de type :

              Sortable auto x2 = f(y);

              ne compilera que si la fonction f retourne une valeur que l'on peut trier.

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

        • [^] # Re: Typage structurel

          Posté par  . Évalué à 3 (+2/-0).

          Pour compléter les autres réponses, il y a un mécanicisme un peu plus complexe mais plus puissant (et qui est celui utilisé dans le journal). Il se base sur une règle du C++ abrégée en SFINAE (Substitution Failure Is Not An Error). L'idée est que si, lors de la substitution d'un type dans un template, on obtient une déclaration que n'a pas de sens, le compilateur ignore la fonction au lieu d'émettre une erreur. Ça ne marche que dans la déclaration (paramètres du template, type de retour, ou types des paramètres), pas dans le corps de la fonction.

          #include <iostream>
          
          template<typename T>
          typename T::A f(T) {
              std::cout << "J'ai un type imbriqué A" << std::endl;
              return {};
          }
          
          template<typename T>
          typename T::B f(T) {
              std::cout << "J'ai un type imbriqué B" << std::endl;
              return {};
          }
          
          struct Type1 {
              using A = int;
          };
          
          struct Type2 {
              using B = int;
          };
          
          int main() {
              f(Type1{});
              f(Type2{});
              return 0;
          }

          Ce qui donne :

          J'ai un type imbriqué A
          J'ai un type imbriqué B

          Pas d'erreurs puisqu'à chaque fois exactement une des fonctions est gardée. Si un type avait à la fois une type imbriqué A et B, les deux fonctions seraient valide et le compilateur générerait une erreur comme il ne saurait pas choisir. Si un type n'a aucune des deux propriétés, les deux fonctions sont ignorées et le compilateur génère une erreur disant que f n'existe pas (mais s'il est sympa, il t'explique pourquoi il a ignoré les deux possibilités avec un message à rallonge).

          Ça permet d'avoir des fonctions surchargées qui sélectionnent la bonne alternative en fonction des propriétés d'un type paramètre du template. Évidemment, si la condition que tu veux avoir n'est pas un type que tu utilises en retour ou en paramètre, ça devient plus compliqué mais beaucoup de choses sont possibles. Les concepts de C++20 simplifient largement tout ça.

          Pour aider à l'utilisation avancée du SFINAE pré-C++20, il y avait quelques aides comme enable_if et une TS proposait d'ajouter des détecteurs (mais c'est sûrement devenu obsolète avec les concepts).

  • # Ça compile sous Windows ?

    Posté par  . Évalué à -5 (+1/-7).

    Si oui : pourquoi ne pas penser à créer un lait. cppfr.org ?

Envoyer un commentaire

Suivre le flux des commentaires

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