Partage
  • Partager sur Facebook
  • Partager sur Twitter

Dispatching et lambda

    11 juin 2021 à 0:40:07

    Bonjour,

    Soit le code suivant:

    class Base
    {};
    
    class Foo :
    	public Base
    {};
    
    class Bar :
    	public Base
    {};
    
    void test(const Base&)
    {
    	std::cout << "class Base" << std::endl;
    }
    
    void test(const Bar&)
    {
    	std::cout << "class Bar" << std::endl;
    }
    
    int main()
    {
    	Base base;
    	test(base);
    	Foo foo;
    	test(foo);
    	Bar bar;
    	test(bar);
    }

    J'obtiens la sortie suivante:

    class Base
    class Base
    class Bar

    Rien à redire, grâce au mécanisme des surcharges, on peut appeler une fonction spécialisée en fonction du type concret du paramètre.

    Peut-on obtenir le même résultat avec des lambdas ?

    • Partager sur Facebook
    • Partager sur Twitter
      11 juin 2021 à 0:48:35

      > Rien à redire, grâce au mécanisme des surcharges

      J'imagine que tu veux parler des redéfinitions (override) et pas des surcharges (overload).

      Je ne suis pas sur de ce que tu cherches à réaliser. Tu pourrais nous montrer un exemple (en code pseudo  correct) ?

      -
      Edité par lmghs 11 juin 2021 à 0:49:05

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

        Je m'intéresse aux mécanismes de dispatch, voir double dispatch (le pattern visitor).

        Je me demande s'il est réalisable avec des lambdas. Ni plus, ni moins.

        • Partager sur Facebook
        • Partager sur Twitter
          11 juin 2021 à 1:15:30

          Salut,

          Pour autant que, comme l'a si bien dit lmghs, tu parle bien de redéfinition et non de surcharge et, bien sur, que tu utilises les référence (ou, au pire, les pointeurs) pour les paramètres polimorphes, oui, bien sur, en vertu du LSP ;)

          Car Foo EST-UNE Base, ce qui implique qu'une instance de Foo peut  être transmise à n'importe quelle fonction s'attendant à recevoir une (référence ou un pointeur sur une) instance de Base.

          Grâce au principe de la meilleure correspondance (best match), nous pouvons donc être sur que notre instance de Bar va faire appeler la version s'attendant à recevoir une référence sur une Bar, ce qui est parfaitement logique.

          Par contre, Bar a beau être également une Base, Foo n'est absolument pas  une Bar, et donc, la meilleure correspondance va devoir s'arrêter au niveau  de Base pour ce qui concerne Foo.

          Que la fonction en question soit une fonction libre, une fonction membre, une fonction membre statique ou une expression lambda ne changera absolument rien: le compilateur essayera toujours de faire appel à la version qui correspond le mieux à l'argument que l'on va transmettre (dans la limite des héritages dont il aura connaissance).

          La véritable question à  se poser aurait plutôt trait à la raison pour laquelle tu tiendrais absolument à utiliser une expression lambda plutôt qu'une fonction "classique".

          Je ne suis, en effet, absolument pas convaincu qu'il y ait un réel intérêt à utiliser les expressions lambda dans ce cas de figure  car, en dehors des cas où l'on sait exactement quel est le type réel de notre instance (comme c'est le cas pour le code que tu montre dans ta question), ce "best match" s'applique parfaitement dans le cadre du deuxième appel effectué lors d'un double dispatch ;)

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

            Salut ! Je crois qu'il est impossible de redéfinir une lambda en c++. Donc non tu ne peux pas spécifier un comportement différent suivant le type d'argument passé à une lambda. Même les lambda template introduites en c++20 ne le permettent pas vraiment... de ce que j'en ai compris.
            • Partager sur Facebook
            • Partager sur Twitter
              11 juin 2021 à 4:52:34

              Deedolith a écrit:

              Rien à redire, grâce au mécanisme des surcharges, on peut appeler une fonction spécialisée en fonction du type concret du paramètre.

              Ton compilateur te ment ! :)

              Pour qu'on soit d'accord sur le vocabulaire (j'ai un doute, lmghs pourra me corriger). C'est du rappel, je sais que tu sais, mais c'est pour que les choses soient claires (et éventuellement me corriger).

              Quand tu as :

              A* p = new B;

              - B est le type dynamique ou type concret, c'est à dire type de l'objet tel qu'il a été créé en mémoire

              - A est le type statique, c'est à dire le type que l'on "voit" dans le code.

              Cette distinction intervient dans un héritage quand on veut utiliser une fonction virtuelle d'un objet qu'on manipule par référence ou pointeur.

              struct A {
                  virtual void foo();
                  void bar();
              }
              
              struct B : A {
                  void foo() override;
                  void bar();
              };
              
              void f(A* a) {
                  a->foo();
                  a->bar();
              }
              
              A a;
              f(&a);
              
              B b;
              f(&a);
              

              Dans ce code, la fonction f() est appelée avec un type static A, mais avec un type dynamique A dans un premier cas et B dans un second cas. La virtualisation de foo() va faire que A::foo() va être appelé dans le premier cas, et B::foo() dans le second. Par contre, c'est A::bar() qui est appelé dans le 2 cas.

              Le fonctionnement de foo() est ce qu'on appelle le simple dispatch. En fonction d'un type dynamique, on va appeler différentes fonctions au runtime.

              Le problème de ton code, c'est que les types statiques et dynamiques sont les mêmes à chaque appel de fonction. Ca donne l'impression d'un simple dispatch, mais ce n'est pas le cas : il n'y a pas résolution des appels de fonctions au runtime. Tout est fait au compile time !

              Pour voir le simple dispatch agir, il faut que le résolution des fonctions soient au runtime. Tu peux par exemple écrire :

              int main()
              {
                  volatile int i = 2;
                  
                  Base* b = nullptr;
                  if (i == 0)
                      b = new Base;
                  else if (i == 1)
                      b = new Foo;
                  else
                      b = new Bar;
                      
                  test(*b);
              }

              Si tu fais cela, tu verras que cela affiche toujours "Base". Tout simplement parce que c'est le type statique utilisés pour l'appel de fonctions par le compilateur.

              Dit autrement, le simple dispatch ne fonctionne pas avec la surcharge (overloading). C'est la redéfinition (override) qui permet, via le mécanisme des v-tables, de faire un simple dispatch. (D'où la question de lmghs pour lever la confusion surcharge vs redéfinition).

              Et donc, pour répondre à ta question, le simple dispatch ne va pas fonctionner avec des lambdas, puisque cela ne fonctionne pas du tout avec la surcharge en règle générale.

              -----------------------------

              Je suppose que ta confusion vient du fait que l'on utilise effectivement la surcharge quand on fait du double dispatch avec le pattern visitor. Si tu regardes le code C++ de l'article "double dispatch" de wikipedia https://en.wikipedia.org/wiki/Double_dispatch, ce code utilise effectivement la surcharge. Mais dans les explications, on voit que le code donné correspond à une simple dispatch et le code pour le double dispatch est expliqué ensuite (mais pas donné).

              Ce qu'on aimerait faire, quand on veut faire du double dispatch, c'est :

              #include <iostream>
              
              // first types
              struct B1 {};
              struct T1 : B1 {};
              struct U1 : B1 {};
              
              // secondes types
              struct B2 { 
                  virtual void foo(T1*) = 0;
                  virtual void foo(U1*) = 0;
              };
              
              struct T2 : B2 { 
                  void foo(T1*) override { std::cout << "T2.foo(T1)" << std::endl; }
                  void foo(U1*) override { std::cout << "T2.foo(U1)" << std::endl; }
              };
              
              struct U2 : B2 { 
                  void foo(T1*) override { std::cout << "U2.foo(T1)" << std::endl; }
                  void foo(U1*) override { std::cout << "U2.foo(U1)" << std::endl; }
              };
              
              int main()
              {
                  B1* b1 = new T1;
                  B2* b2 = new U2;
                  
                  b2->foo(b1);
              }

              Mais ce code ne fonctionne pas, parce que la résolution de foo() va se faire statiquement, c'est à dire avec le type B1 et pas T1.

              Avec le pattern visitor, on ajoute une couche d'indirection, qui contient elle-même des fonctions virtuelles. Et c'est le fait d'avoir ces 2 v-tables qui permet de faire le double dispatch. Sans v-table, pas de dispatch. 

              • Partager sur Facebook
              • Partager sur Twitter
              Pour poser des questions ou simplement discuter informatique, vous pouvez rejoindre le discord NaN.

              Dispatching et lambda

              × Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
              • Editeur
              • Markdown