Partage
  • Partager sur Facebook
  • Partager sur Twitter

tableau dynamique constant et variadic function

Sujet résolu
    22 novembre 2021 à 21:15:12

    Bonsoir bonsoir.

    J'essaie de créer un programme possédant deux objets :

    - chaise

    - maison

    Mon but étant de créer un maison et de lui passer dans son constructeur un certain nombre de chaises.

    Il y a une subtilité, c'est que la classe chaise est une classe virtuel pure. Et que j’utilise des classes héritant de chaise: easychaise et complexchaise

    Voila mon code (qui ne marche pas) :

    class chaise
    {
    public:
    	chaise(int val) : m_value(val)
    	{ }
    	virtual void set(int val) = 0;
    	virtual int get() = 0;
    protected:
    	int m_value;
    };
    class easychaise : protected chaise
    {
    public:
    	easychaise(int val) : chaise(val)
    	{ }
    	virtual void set(int val) override {
    		m_value = val;
    	}
    	virtual int get() override { return m_value; }
    };
    class complexchaise : protected chaise
    {
    public:
    	complexchaise(int val) : chaise(val)
    	{ }
    	virtual void set(int val) override {
    		m_value = val;
    	}
    	virtual int get() override { return m_value; }
    };
    class maison
    {
    public:
    	maison(std::initializer_list<complexchaise&> chaises) : m_test(chaises)
    	{
    	}
    private:
    	std::vector<complexchaise&> m_test;
    };

    Quand je compile j'ai pleins d'erreur différentes :

    pointeur vers référence non autorisé

    ou

    pointeur non conforme vers la référence

    Pour préciser, j'ai utilisé initializer_list, mais je peux utiliser aussi un variadic function parameter.

    Merci,

    Bonne soirée.

    -
    Edité par heryz 22 novembre 2021 à 21:16:07

    • Partager sur Facebook
    • Partager sur Twitter
      22 novembre 2021 à 22:30:15

      On ne peux pas faire de vector de références, ni d'initializer_list de références d'ailleurs.
      • Partager sur Facebook
      • Partager sur Twitter
      ...
        22 novembre 2021 à 22:55:35

        Bonjour,

        Tout d'abord tu veux mettre des chaises dans ta maison. Il faudrait donc des chaises plutôt que des complexchaises.

        Ensuite le std::initializer_list<> est rarement le bon type à utiliser quant aux variadic function parameters ça semble être hors du niveau pour débuter. On peut rester sur std::initializer_list<> ou utiliser std::vector<> qui serait surement plus simple.

        Tu veux créer des collections de références (std::initializer_list<> ou std::vector<>). Ça n'a pas de sens, une référence n'est pas un type, c'est un moyen d'accéder à une instance. On peut par exemple avoir un paramètre de type référence à un std::vector, mais pas un std::vector<> de références.

        Tu tentes d'initialiser un std::vector<> à partir d'un std:initializer_list<>. Tu ne peux créer un std::vector<> comme ça. Au mieux tu peux dire que tu construits le std::vector<> à partir des éléments d'un std::initializer_list<>. Ça s'écrirait:

        maison(std::initializer_list<complexchaise*> chaises) : m_test(chaises.begin(),chaises.end())
            {
            }

        Alors par quoi remplacer tes références? Tu peux utiliser :
        a) un std::reference_wrapper<>, ça a le comportement d'une référence sans en être une.
        b) un simple pointeur, ça va désigner chaque chaise
        c) un pointeur intelligent, lui aussi va désigner chaque chaise, mais en plus il en sera le "propriétaire".
        Le choix dépend de ce qui doit être propriétaire de tes chaises.
        Si tu considères que c'est celui qui crée la maison qui reste propriétaire des chaises, alors il faut utiliser le (a) ou le (b).
        Si tu considères que celui qui crée la maison va transférer la propriété des chaises, c'est le (c) qui est préférable.

        La suite dépend donc de comment tu envisages d'appeler le constructeur de ta maison. Ce code est important.
        Exemple, tu crées des chaises, et tu les passes par référence à la maison. Le propriétaire de tes chaises, ça n'est pas la maison.

        int main() {
           easychaise  chaise1{1];
           complexchaise  chaise2{2};  // les chaises SONT ICI
           // cas a
           maison  ma_maison{ {std::ref(chaise1), std::ref(chaise2)} }; // la maison va référencer les chaises
           // cas b
           maison  ma_maison{ {&chaise1, &chaise2} }; // la maison a alors des pointeurs sur les chaises
        }

        Tu vois bien que dans ce cas, les chaises sont dans le main(), qui en est le propriétaire.

        Mais les chaises devraient peut-être plutôt appartenir à la maison. Et le pointeur intelligent est prévu pour gérer ce transfert de propriété:

        class maison {
        public:
            maison(std::initializer_list<std::unique_ptr<chaise>> chaises) : m_test(std::make_move_iterator(chaises.begin()),std::make_move_iterator(chaises.end())) {
            }
        private:
            std::vector<std::unique_ptr<chaise>>  m_test;
        };
        int main() {
           auto  p_chaise1 = std::make_unique<easychaise>(1);   // on crée un pointeur intelligent sur une easychaise
           auto  p_chaise2 = std::make_unique<complexchaise>(2);
           // pour le moment c'est main() qui possède les 2 chaises
           maison  ma_maison{ {std::move(p_chaise1), std::move(p_chaise2)} };
           // maintenant les p_chaise1 et p_chaise2 sont vides, la maison est le propriétaire des chaises.
        }

         Avec un paramètre std::vector<> on a une écriture plus simple, car on peut créer simplement un std::vector<X> à partir d'un std::vector<X>

        class maison {
        public:
            maison(std::vector<std::unique_ptr<chaise>> chaises) : m_test(std::move(chaises)) {
            }
        private:
            std::vector<std::unique_ptr<chaise>>  m_test; // un tableau de pointeurs (qui sont propriétaires des chaises)
        };
        • Partager sur Facebook
        • Partager sur Twitter

        En recherche d'emploi.

          23 novembre 2021 à 21:21:28

          Bonsoir,

          Merci beaucoup de vos réponses.


          Alors il se trouve que je voulais utiliser un std::initializer_list pour le test. Pourquoi considères-tu le std::initializer_list comme inadapté ? Je veux pas particulièrement l'utiliser, mais j'avais l'impression d'être obligé de l'utiliser.

          La raison pour laquelle je pose ma question est qu'il m'arrive de plus en plus souvent de devoir créer des listes d'objets. La dernière fois j'ai crée un tableau (en tant que membre de ma classe) et dans le constructeur j'ai utilisé des make_unique pour créer mes objets et les insérer en même temps dans mon tableau. Sauf que dans mon nouveau cas, ça ne marche pas bien car je veux passer des objets directement en paramètre (où à minima des informations en paramètre) de mon constructeur et que mon instance crée un tableau de taille adapté (si possible déclaré de manière statique) et y stocke soit :

          1. les objets si ils sont envoyés dans le constructeur

          2. soit des références vers ces objets

          3. Soit des pointeurs constants vers ces objets

          Par rapport à mon test, j'aimerais pouvoir faire quelque chose comme ça :

          void main()
          {
          	maison maison1(complexchaise(), complexchaise(), easychaise());	// maison1 crée un tableau avec dedan les trois objets en paramètre
          
          	complexchaise c1;
          	easychaise c3;
          
          	maison maison2(c1, c3);	// maison2 crée un tableau constant de trois pointeurs vers les trois objets c1 et c3
          
          }

          Est-ce que tu sais comment je peux faire cela ?

           La méthode des unique_ptr peut fonctionner mais je veux savoir si je peux faire cela sans allocation dynamique.

          ET pour terminer, si il existe un autre moyen que des variadic template parameters et que le initializer_list, je suis tout ouï. :)

          Merci,

          Bonne soirée

          -
          Edité par heryz 23 novembre 2021 à 21:53:30

          • Partager sur Facebook
          • Partager sur Twitter
            24 novembre 2021 à 14:11:40

            Salut,

            Un bon truc pour décider de la logique que ton application devra suivre, c'est souvent -- quand c'est applicable -- de t'inspirer de la logique que tu suivrais dans les mêmes circonstances dans la "vie de tous le jours".

            Ici, la logique que tu essaies de mettre en place serait -- en gros -- d'acheter des chaises, de les mettre au milieu d'un espace vide, et de demander à un maçon de monter des murs autours pour construire la maison à laquelle les chaises sont destinées.

            C'est une manière de voir les choses qui en vaut une autre, cependant, ne serait-il pas plus "cohérent" de demander au maçon de monter les murs de la maison avant d'y entrer les chaises une fois que la maison est "habitable"?

            Car la relation qui unit les chaises destinées à une maison à la maison à laquelle elles sont destinées est totalement différente de la relation qui unit un moteur destiné à une voiture à la voiture à laquelle il est destiné.  Je m'explique:

            Dans le cas d'un moteur et d'une voiture, il n'y a rien à faire: le moteur doit être intégré à  la voiture au plus tard lorsque la voiture est sur le point de quitter la chaine de production, pour que "quelqu'un" soit en mesure de déplacer la voiture jusqu'à l'endroit où elle pourra attendre sa livraison finale.

            Dans le cadre des chaises et d'une maison, les chaises ne sont pas spécifiquement destinées à une maison particulière. et la maison n'a aucun besoin de  contenir des chaises pour pouvoir être considérée comme "habitable et prête à la vente".

            Cela te permettra d'ailleurs de choisir un autre modèle de chaises jusqu'au tout dernier moment, et, pourquoi pas, de changer trois ou quatre fois d'avis entretemps.

            Mais, du coup, ne serait-il pas plus logique de construire ta maison "vide" (à l'exception bien sur des robinets, interrupteurs, prises et autres éléments qui ne pourront plus bouger) et d'avoir une fonction quelconque permettant de rajouter une (liste de chaises) une fois que la maison existe bel et bien, quitte à ajouter la notion de "fabrique de chaises" en cas de besoin et à  faire en sorte que cette fonction ne pourra être utilisée que par cette notion particulière ?

            Cela t'amènerait donc à avoir un code qui serait sans doute plus proche de quelque chose comme

            int main(){
                Maison laMaison{/* superficie, adresse*/};
                FabriqueDeChaises laFabrique;
                laFabrique.ajouterChaises(laMaison, nombre_de_chaises, type_de_chaises);
                /* rien n'empêcherait d'ailleurs d'ajouter plusieurs chaises de types différents */
                laFabrique.ajouterChaises(laMaison, nombre_de_chaises, autre_type_de_chaises);
                /* laMaison dispose maintenant du nombre de chaises
                 * du type adéquat dont on s'attend à ce qu'elle dispose
                 */
            }

            Ensuite, il faudra bien sur  s'inquiéter de savoir qui est le "propriétaire légal" des chaises qui seront créées, à quelle notion sera la seule à avoir le droit de détruire une chaise lorsqu'elle s'avère être devenue inutile (ou qu'elle a cassé).

            Et ce sera d'autant plus important du fait que tes chaises font partie d'une hiérarchie de classes, qu'elles ont donc sémantique d'entité, et qu'elles sont donc réputées "unique et non copiables"

            On pourrait en effet se dire que "tout s'arrête" pour les chaises une fois qu'elles entre dans une maison, que quand une maison décide de se débarrasser d'une chaise devenue inutile, la chaise est tout simplement détruite et que l'on n'en parle plus ;)

            Mais on peut aussi se dire que, quand la maison décide de se débarrasser d'une chaise, elle a malgré tout l'occasion de décider de la donner à "quelqu'un (ou quelque chose) d'autre", qui en deviendra alors le "propriétaire légitime".

            Au niveau de la maison, cela aurait malgré tout le même effet: une fois que la chaise a quitté la maison, la maison n'a plus de raison de s'inquiéter du devenir de la chaise (qui est pris en charge par le "nouveau propriétaire"). 

            La seule différence, c'est qu'elle exposera peut-être une fonction "donner la chaise (identifiant de la chaise  à donner)" permettant de retirer la chaise de la maison sans la détruire.

            • 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
              24 novembre 2021 à 19:46:45

              Bonjour,

              Ce que tu veux faire et loin d'être simple.

              void main()
              {
                  maison maison1(complexchaise(), complexchaise(), easychaise()); // maison1 crée un tableau avec dedan les trois objets en paramètre
               
                  complexchaise c1;
                  easychaise c3;
               
                  maison maison2(c1, c3); // maison2 crée un tableau constant de trois pointeurs vers les trois objets c1 et c3
               
              }

              Pour la maison1. Tu crées des chaises, et à la ligne 4 les chaises on déjà disparu. Donc le constructeur de la maison n'aura pas d'autre choix que de recopier ces chaises, par conséquent il devra en connaître le type exact (on ne peut pas copier une chaise, on ne peut copier que des vraies choses comme une complexchaise.)

              Pour la maison2. Les chaises continuent d'exister dans le main(), donc ton constructeur de maison à plus de possibilités. Il pourra en retenir la position, ou il pourrait aussi en faire une copie. Dans le second cas, on revient à la manière du premier constructeur.
              Dans les 2 cas, tu supposes une fonction variadique qui devra donc s'adapter à la fois au type de chacune des chaises et aussi au nombre de chaises passées en paramètre.
              Pour reprendre ce qu'a expliqué Koala01, dans "la vie de tous les jours" ça n'a pas de sens de copier et détruire des chaises.

              Et oui, c'est faisable en C++ mais ça nécessite d'utiliser des fonctions template avec un parameter pack. C'est plutôt complexe; je fais du C++ depuis plus de 30 ans et avant d'y avoir recours j'hésite toujours.
              Dans les exemples que je t'ai donné, note que j'utilise une notation à peine différente mais ainsi je contourne à la fois la nécessité de connaître le type exact, celui d'utiliser un parameter pack et à aucun moment je ne crée des chaises pour les copier et les détruire. Ce qui n'a pas de sens dans "la vie de tous les jours".

              • Partager sur Facebook
              • Partager sur Twitter

              En recherche d'emploi.

                26 novembre 2021 à 0:05:26

                Bonsoir.

                Merci pour vos réponses,

                je remarque de plus en plus que mon exemple avec des maisons et des chaises était vraiment très mauvais, mais au moins cela m'aura permis de voir une autre manière de penser les relations entre objets, ce qui est une bonne chose.

                koala01: Oui, tu as raison dans mon exemple, il y a un réel problème de logique. Cet exemple n'est pas bien. J'essaierai dans mes prochains tests d'utiliser des patrons fabrique, pour le moment ça me semble assez abstrait :).

                Dalfab :

                J'ai fait le test avec les std::vector, et sans étonnement, ça a bien marché ^^"

                Test du reference_wrapper :

                class chaise
                {
                public:
                	chaise(int val) : m_value(val)
                	{ }
                	int m_value;
                };
                class easychaise : public chaise
                {
                public:
                	easychaise(int val) : chaise(val)
                	{ }
                };
                class complexchaise : public chaise
                {
                public:
                	complexchaise(int val) : chaise(val)
                	{ }
                };
                class maison
                {
                public:
                	maison(std::vector<std::reference_wrapper<chaise>> chaises) : m_test(chaises)
                	{
                	}
                	std::vector<std::reference_wrapper<chaise>> m_test;
                };
                int main()
                {
                   easychaise  chaise1{ 1 };
                   complexchaise  chaise2{2};  
                   maison  ma_maison{ {std::ref(chaise1), std::ref(chaise2)} }; 
                }

                Cela fonctionne plutôt bien. J'ai remarqué que je pouvais retirer les deux std::ref dans le main, et que cela compilait quand même. J'aime plutôt bien le résultat.

                Test avec unique_ptr

                Alors j'ai essayé de reproduire ton exemple avec les unique_ptr, je ne pensais pas que j'allais galérer dessus :

                // Les classes chaise complexchaise et easychaise n'ont pas changées
                class maison
                {
                public:
                	maison(std::vector<std::unique_ptr<chaise>> chaises) : m_test(std::move(chaises))
                	{
                	}
                	std::vector<std::unique_ptr<chaise>> m_test;
                };
                int main()
                {
                    auto  p_chaise1 = std::make_unique<easychaise>(1);
                    auto  p_chaise2 = std::make_unique<easychaise>(2);
                    maison  ma_maison{ {std::move(p_chaise1), std::move(p_chaise2)} };
                }

                Quand je compile je reçois : tentative de référencement d'une fonction supprimée.

                Test avec template

                Bon alors vu que je maitrise pas trop les templates, j'ai regardé rapidement les templates pack que tu as indiqué, mais je ne pense pas (je sais même pas en fait ^^) que je les aie utilisé.

                template <std::size_t len>
                struct TTest
                {
                    explicit TTest(std::array<int, len> p_data) :
                        ar(p_data)
                    {
                    }
                
                    TTest() = delete;
                
                    std::array<int, len> ar;
                };
                int main()
                {
                    TTest<2> t({ std::array{1, 2} });
                    std::cout << t.ar[0] << std::endl; // affiche 1
                    t.ar[0] ++;
                    std::cout << t.ar[0] << std::endl; // affiche 2
                }

                J'ai bien le résultat que je voulais, par contre j'ai l'impression d'utiliser un cast en faisant un std::array, et ça ne me semble pas super propre.

                En fait (et c'est pour cela que mon exemple était très mauvais), dans le domaine où je travaille, on souhaite généralement ne pas faire d'allocations dynamiques. J'aurai vraiment dû poser la question de but en blanc, cela aurait été plus simple.

                -
                Edité par heryz 26 novembre 2021 à 0:23:34

                • Partager sur Facebook
                • Partager sur Twitter
                  26 novembre 2021 à 1:16:43

                  Si tu ne veux pas d'allocation dynamique, il faut oublier std::vector. Et les type polymorphiques, ca va être bof aussi.

                  Il faudrait aussi préciser ce que tu veux au runtime ou compile. Par exemple, si tu veux que du compile time, tu peux écrire des choses assez simples comme ça :

                  #include <tuple>
                  
                  class complexchaise {};
                  class easychaise {};
                  
                  template<typename Tuple>
                  class Maison {
                  public:
                      Maison(Tuple tuple) : m_tuple(std::move(tuple)) {}
                  private:
                      Tuple m_tuple;
                  };
                  
                  int main() {
                      Maison maison1(std::make_tuple(complexchaise{}, complexchaise{}, easychaise{}));
                      
                      complexchaise c1;
                      easychaise c3;
                      Maison maison2(std::make_tuple(std::cref(c1), std::cref(c3)));
                  }

                  (avec std::visit pour les accès aux membres de tes classes chaises).

                  • Partager sur Facebook
                  • Partager sur Twitter
                    26 novembre 2021 à 13:35:15

                    Pour l'exemple utilisant passant en paramètre un std::vector<> de std::unique_ptr<>, en effet il ne marche pas. Je pensais qu'une ligne std::vector<std::unique_ptr<chaise>>  v{std::move(p_chaise1), std::move(p_chaise2)}; était valide, mais non. On arrive du coup à un code moins pratique:
                    int  main() {
                    	std::unique_ptr<chaise>  p_chaise1 = std::make_unique<easychaise>( 1 );
                    	std::unique_ptr<chaise>  p_chaise2 = std::make_unique<complexchaise>( 2 );
                    
                    	std::vector<std::unique_ptr<chaise>>  des_chaises;
                    	des_chaises.emplace_back( std::move(p_chaise1) );
                    	des_chaises.emplace_back( std::move(p_chaise2) );
                    
                    	maison  ma_maison{ std::move(des_chaises) };
                    }
                    Pour une solution avec les parameter packs. C'est juste pour l'exemple, comme on te l'a dit ça n'est certainement pas une bonne solution dans ton cas:
                    class maison {
                    public:
                    	template<class...CHAISE_S>
                    	maison( CHAISES const& ... chaises ) {
                    		// ici on a le type exact de chacune des chaises (c'est CHAISE_S), on peut créer un unique_ptr<> sur une copie et oublier sont type exact
                    		( m_test.emplace_back( std::make_unique<CHAISE_S>(chaises) ) , ... );
                    	}
                    	std::vector<std::unique_ptr<chaise>>  m_test;
                    };
                    
                    int  main() {
                    	maison  ma_maison1( easychaise{1}, complexchaise{2}, easychaise{3} );
                    
                    	easychaise  chaise4{4};
                    	complexchaise  chaise5{5};
                    	maison ma_maison2( chaise4, chaise5, easychaise{6}, complexchaise[7} );
                    }

                    C'est vrai que ça permet un appel intuitivement simple. Mais il cache plein de choses (à chaque création de maison, on crée un nouveau constructeur et il y aura plein de easychaise/complexchaise créées pour être copiées et détruites. C'est mal.
                    Il est nettement mieux que les chaises soient gérées comme des entités (une chaise est unique, vouloir copier une chaise c'est en créer une toute nouvelle qui lui ressemble) plutôt que comme des valeurs (une chaise peut être copiée on obtient la même, ce qui importe c'est le contenu en prenant le risque d'avoir des clones qui ne sont ni tout à fait la même ni tout à fait une autre).

                    Tu as aussi la solution de gbdivers, mais là il y a alors un nouveau type de maison qui est associé aux chaises qu'elle contient. Sa solution a un avantage sur la mienne que le type exact de la chaise n'est jamais perdu mais pour en sortir une chaise il faut détruire la maison!

                    S'il ne fallait lire qu'une réponse, c'est celle de koala01 qui t'a indiqué comment bien poser les choses.

                    • Partager sur Facebook
                    • Partager sur Twitter

                    En recherche d'emploi.

                      26 novembre 2021 à 17:11:45

                      Dalfab a écrit:

                      Sa solution a un avantage sur la mienne que le type exact de la chaise n'est jamais perdu mais pour en sortir une chaise il faut détruire la maison!

                      On ne sait pas si c'est un problème :)

                      En réalité, ce que je voulais signaler, c'est que si les spécifications ne sont pas bien bien définies, on va se retrouver avec du code qui ne va pas correspondre (par exemple, il a dit qu'il ne voulait pas d'allocation dynamique, on fait quoi avec vector et unique_ptr ?) ou de l'over engineering. 

                      Si quelqu'un vient me voir et me propose un code comme ca :

                      maison maison1(complexchaise(), complexchaise(), easychaise()); 
                       
                      maison maison2(c1, c3); 

                      Mon conseil sera tres probablement... de ne pas faire ca !

                      Comme tu l'as dit, c'est pas simple a faire. Avoir une collection qui peut manipuler des valeurs ou des indirections sur des valeurs. De façon transparente pour les utilisateurs. Perso, on me présente ca, ma réponse sera que cela nécessitera un code inutilement complexe, et que la meilleure solution est de faire un choix dans la syntaxe à utiliser. A vouloir en faire trop, on fait du caca.

                      • Partager sur Facebook
                      • Partager sur Twitter
                        27 novembre 2021 à 11:18:46

                        @gbdivers,

                        C'est vrai que ta solution est celle qui respecte exactement la demande, elle ne fait pas d'allocations dynamiques. Sans allocation dynamique tout est fixé d'où, le seul moyen de changer c'est détruire et refaire une nouvelle maison.

                        • Partager sur Facebook
                        • Partager sur Twitter

                        En recherche d'emploi.

                          27 novembre 2021 à 17:17:37

                          Bonsoir,

                          Merci, pour vos réponses.

                          gbdivers a écrit:

                          On ne sait pas si c'est un problème :)

                          En réalité, ce que je voulais signaler, c'est que si les spécifications ne sont pas bien bien définies, on va se retrouver avec du code qui ne va pas correspondre (par exemple, il a dit qu'il ne voulait pas d'allocation dynamique, on fait quoi avec vector et unique_ptr ?) ou de l'over engineering.



                          Je crois que je tourne trop autour du pot. Je vais essayer d'être plus claire :

                          Le but de ce petit programme de test, c'est comprendre comment utiliser des objets gérant une petite collection de données. L'objet et la collection doivent être déclarés de manière statique(si possible). Dans le cas du test, la collection ne change pas une fois créée, elle reste toujours ce qu'elle est. Si j'ai une maison avec trois chaises dedans, tant que la maison est là, les chaises seront là (C'est pour ça que j'ai dit que mon exemple était assez mauvais, mais j'ai tellement modifié et re-modifié mon message précédent que j'ai dû supprimer cette information). J'ai essayé de trouver un exemple concret d'objet avec un "côté immuable" entre l'objet et sa collection dans mon message précédent, mais j'ai pas trouvé alors j'ai laissé tombé ^^.

                          J'ai essayé d'utiliser le std::tuple que tu as présenté. Mais je ne pense pas que je pourrai m'en servir. En effet, je reçois une erreur quand je tente d'accéder à l'un des objets de la collection en faisant un std::get<variable>. Il semble qu'on ne peut pas utiliser d'index variable donc non connu à la compilation pour un std::tuple.


                          J'ai utilisé cette technique (j'en avais parlé dans un précédent message), et je trouve qu'elle est plutôt pas mal. Qu'en pensez-vous ?

                          class chaise
                          {
                          public:
                          	chaise(int val) : m_value(val)
                          	{ }
                          	int m_value;
                          };
                          class easychaise : public chaise
                          {
                          public:
                          	easychaise(int val) : chaise(val)
                          	{ }
                          };
                          class complexchaise : public chaise
                          {
                          public:
                          	complexchaise(int val) : chaise(val)
                          	{ }
                          };
                          template <std::size_t len>
                          struct TTest
                          {
                              explicit TTest(std::array<chaise, len> p_data) :
                                  ar(p_data)
                              {
                              }
                          
                              TTest() = delete;
                          
                              std::array<chaise, len> ar;
                          };
                          int main()
                          {
                              TTest<2> t{ {complexchaise(1), easychaise(2) } };
                          }


                          Comme l'a dit Dalfab, il y a toujours le constructeur, qui crée ensuite une copie dans mon exemple, je vais essayer de trouver un moyen de faire un passage plus propre de complexchaise et easychaise

                          Au final j'ai utilisé un reference_wrapper pour récupérer mes données :

                          template <std::size_t len>
                          struct TTest
                          {
                              explicit TTest(std::array<std::reference_wrapper<chaise>, len> p_data) :
                                  ar(p_data)
                              {
                              }
                          
                              TTest() = delete;
                          
                              std::array<std::reference_wrapper<chaise>, len> ar;
                          };
                          int main()
                          {
                              easychaise  chaise1{ 1 };
                              complexchaise  chaise2{ 2 };
                              TTest<2> t{ {chaise1, chaise2 } };
                          }



                           Qu'est-ce que vous en pensez ? Si vous trouvez ça pas bien, pouvez-vous me dire ce qui vous gène ?

                          Merci, bonne soirée,

                          -
                          Edité par heryz 27 novembre 2021 à 17:30:44

                          • Partager sur Facebook
                          • Partager sur Twitter
                            28 novembre 2021 à 0:53:41

                            heryz a écrit:

                            Il semble qu'on ne peut pas utiliser d'index variable donc non connu à la compilation pour un std::tuple.

                            Déso, j'ai confondu : std::visit est pour les std::variant, pas pour les std::tuple.

                            Mais tu peux quand même écrire des loops sur des tuples. La syntaxe est juste un peu plus complexe qu'un simple for :

                            #include <tuple>
                            #include <iostream>
                            #include <string>
                            
                            struct A { int i = 123; };
                            struct B { int i = 456; std::string s = "blabla"; };
                            
                            void print(A const& a) {
                                std::cout << a.i << " ";
                            }
                            
                            void print(B const& b) {
                                std::cout << b.s << " ";
                            }
                            
                            int main() {
                                std::tuple<A, A, B, A> t;
                                std::apply([](auto const&... arg){ ((std::cout << arg.i << " "), ...); }, t); // 1
                                std::apply([](auto const&... arg){ ((print(arg)), ...); }, t);                // 2
                            }    
                            

                            C'est un exemple avec std::apply et les fold expression, mais d'autres syntaxes sont possibles (je pense que le code que j'ai donné est la syntaxe la plus simple ?)

                            Les gros avantages est que c'est résolu à la compilation (on appelle ça du polymorphisme statique. Je pense que c'est assez proche du polymorphisme par trait qu'on retrouve dans Rust - mais je ne suis pas spécialiste de Rust) et il n'y a pas besoin d'une classe de base, de virtual/override, etc. (Tu peux utiliser directement une API commune aux classes - comme par exemple la variable membre i dans la ligne 1 - ou des fonctions comme print pour chaque classe).

                            Cela fonctionne par ce que le compilateur a toutes les informations pour savoir par exemple que le 3ème élément du tuple est de type B et qu'il sait quelle fonction "print" appeler la fonction correspondante.

                            Et comme il a toutes les informations, il va pouvoir aussi optimiser à fond, en virant toutes les structures intermédiaires (std::apply; std::tuple, print, etc), pour produire un code équivalent à :

                            // ligne 1
                            std::cout << A.i << " ";
                            std::cout << A.i << " ";
                            std::cout << B.i << " ";
                            std::cout << A.i << " ";
                            
                            // ligne 2
                            std::cout << A.i << " ";
                            std::cout << A.i << " ";
                            std::cout << B.s << " ";
                            std::cout << A.i << " ";
                            

                            La preuve : https://gcc.godbolt.org/z/8GdT3b443 

                            heryz a écrit:

                            Qu'est-ce que vous en pensez ? Si vous trouvez ça pas bien, pouvez-vous me dire ce qui vous gène ?

                            J'ai détaillé la solution avec tuple pour t'expliquer en quoi ta solution est différente. Par contre, je ne peux pas dire si c'est mieux ou moins bien, c'est à toi de voir selon les contraintes de ton projet.

                            L'approche avec un std::array est tout à fait valide. Par contre, il faut voir ce qui sera fait au runtime alors (avec ou sans constexpr ?). Mais je m'attend à ce que tu as une empreinte mémoire plus importante (pour le array) et plus de code généré (pour le for et surtout l'héritage).

                            -
                            Edité par gbdivers 28 novembre 2021 à 0:55:01

                            • Partager sur Facebook
                            • Partager sur Twitter

                            tableau dynamique constant et variadic function

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