Partage
  • Partager sur Facebook
  • Partager sur Twitter

Constructeur qui prend un unique_ptr en argument

    2 septembre 2024 à 18:37:51

    Salut tout le monde !

    J'ai dû mal à me représenter comment je dois utiliser les unique_ptr en C++. Voici un code simple que j'ai implémenté et qui compile mais je ne sais pas si l'implémentation est correcte :

    #include <iostream>
    #include <memory>
    
    
    class Vehicule {
     public:
      Vehicule(const std::string& brand) : brand_(brand) {}
    
      virtual ~Vehicule() = default;
      virtual void startEngine() = 0;
    
     protected:
      std::string brand_;
    };
    
    class Car : public Vehicule {
     public:
      Car(const std::string& brand) : Vehicule(brand) {}
    
      void startEngine() override {
        std::cout << "Car : " << brand_ << " started engine" << std::endl;
      }
    };
    
    class Motorbike : public Vehicule {
     public:
      Motorbike(const std::string& brand) : Vehicule(brand) {}
    
      void startEngine() override {
        std::cout << "Motorbike : " << brand_ << " started engine" << std::endl;
      }
    };
    
    
    class Pilot {
     public:
      Pilot(std::unique_ptr<Vehicule> vehicule) {
        vehicule_ = std::move(vehicule);
      }
    
      void startVehiculeEngine() {
        vehicule_->startEngine();
      }
    
     private:
      std::unique_ptr<Vehicule> vehicule_;
    };
    
    
    int main() {
      std::unique_ptr<Car> car = std::make_unique<Car>("Nissan");
      std::unique_ptr<Motorbike> motorbike = std::make_unique<Motorbike>("Ducati");
    
      Pilot carPilot(std::move(car));
      Pilot motorbikePilot(std::move(motorbike));
    
      carPilot.startVehiculeEngine();
      motorbikePilot.startVehiculeEngine();
    
      return 0;
    }

    Je suis perdu sur plusieurs points pourtant très basiques :

    Comment dois-je passer le std::unique_ptr en argument de mon constructeur ?

    Option 1 :

    Pilot(std::unique_ptr<Vehicule> vehicule) {
      vehicule_ = std::move(vehicule);
    }

    Option 2 :

    Pilot(std::unique_ptr<Vehicule>&& vehicule) {
      vehicule_ = std::move(vehicule);
    }


    Option 3 (ne compile pas) :

    Ce code ne compile pas mais je ne comprends pas pourquoi. Je pensais qu'un passage par référence pouvait fonctionner avec un std::unique_ptr puisqu'il n'y pas de copie du pointeur

    Pilot(std::unique_ptr<Vehicule>& vehicule) {
      vehicule_ = std::move(vehicule);
    }

    Autre question : est-ce qu'il y a un moyen de s'assurer que personne n'utilisera les variables car (l.51) et motorbike (l.52) après avoir utilisé std::move dessus ? J'aime bien le concept des std::unique_ptr qui sont plus légers en mémoire que les std::shared_ptr mais s'ils sont aussi plus dangereux car ils peuvent entraîner des undefined behavior c'est pas génial...




    -
    Edité par JeffWallace 2 septembre 2024 à 18:40:44

    • Partager sur Facebook
    • Partager sur Twitter
      2 septembre 2024 à 19:29:59

      Option 2, pour avoir une sémantique explicite de rvalue. L'option 1 (passage par valeur) est sensé accepter des lvalue et rvalue.

      L'option 3 n'est pas valide, parce que une référence non const n'accepte que les lvalues.

      Pour le détail des value categories https://en.cppreference.com/w/cpp/language/value_category 

      Pour la dernière question, c'est au dev de faire attention de ne pas utiliser un pointeur après un move. Normalement, c'est pas une chose qu'on loupe, parce que le move est explicite.

      • Partager sur Facebook
      • Partager sur Twitter
        2 septembre 2024 à 19:55:51

        Merci pour ta réponse ! ^^

        Comment choisir entre l'option 1 et l'option 2 dans ce cas ? (c'est peut-être un détail mais souvent il y a une manière à privilégier plutôt qu'une autre en C++)

        Pour l'option 3, pourquoi je ne peux pas passer le std::unique_ptr par valeur dans le constructeur dans ce cas ?

        Ce code ne compile pas :

        Pilot(std::unique_ptr<Vehicule>& vehicule) {
          vehicule_ = std::move(vehicule);
        }
        
        ...
        
        Pilot carPilot(car);



        • Partager sur Facebook
        • Partager sur Twitter
          2 septembre 2024 à 20:20:20

          Je l'ai dit, c'est une question de value categories.
          • Partager sur Facebook
          • Partager sur Twitter
            2 septembre 2024 à 21:25:45

            Bonjour,

            Dans le dernier cas. on ne peut pas convertir une lvalue unique_ptr<Car> en unique_ptr<Vehicule>&. Les 2 types sont "unrelated".

            Dans les premiers cas, un unique_ptr<X> est prévu pour accepter d'être construit à partir d'un unique_ptr<Y>&&, à condition que Y* soit implicitement convertible en X*. C'est intéressant pour la conversion dérivée vers base qui est justement le cas ici.

            Si dans le dernier cas, on passait un unique_ptr<Vehicule> ça marcherait, mais passer par l-rvalue reference un unique_ptr pour en faire un move est plutôt source de problème (alors que quand on reçoit par valeur ou par &&, on est sûr que le move ultérieur est sain).

               std::unique_ptr<Vehicule>  ptr_intermediaire{std::move(car)};
               Pilot  carPilot( ptr_intermediaire ); // on a fait un passage par référence!
               // A cet instant, car ET ptr_intermediaire sont vides.

            -
            Edité par Dalfab 2 septembre 2024 à 21:28:48

            • Partager sur Facebook
            • Partager sur Twitter
              2 septembre 2024 à 23:12:58

              gbdivers a écrit:

              Je l'ai dit, c'est une question de value categories.


              Donc ça veut dire que je suis obligé d'implémenter les 2 possibilités ou seulement celle-ci ?

              Pilot(std::unique_ptr<Vehicule> vehicule) {
                vehicule_ = std::move(vehicule);
              }

              Dans tous les cas je suis obligé de construire les instances de la classe Pilot avec les std::move. Impossible de passer les variables directement :

              Pilot carPilot(std::move(car));
              Pilot motorbikePilot(std::move(motorbike));
              


              Désolé je suis vraiment à la ramasse avec les unique_ptr. J'ai du mal à trouver des exemples de code les utilisant. Pourtant j'ai regardé dans dans plein de livres sur le C++.

              J'ai juste trouvé ça sur cppreference.com : https://en.cppreference.com/w/cpp/memory/unique_ptr#Example

              Il y a ce code :

              // a function consuming a unique_ptr can take it by value or by rvalue reference
              std::unique_ptr<D> pass_through(std::unique_ptr<D> p)
              {
                  p->bar();
                  return p;
              }

              Et plus bas ce code :

              // Create a (uniquely owned) resource
              std::unique_ptr<D> p = std::make_unique<D>();
               
              // Transfer ownership to `pass_through`,
              // which in turn transfers ownership back through the return value
              std::unique_ptr<D> q = pass_through(std::move(p));

              Ok donc il faut bien passer en argument par valeur dans les fonctions puis utiliser std::move lorsque que le passage par copie n'est pas disponible.

              Mais du coup ça veut pas dire que le passage par valeur est peu efficace ? Un passage par référence ne serait pas plus approprié ?


              -
              Edité par JeffWallace 2 septembre 2024 à 23:17:22

              • Partager sur Facebook
              • Partager sur Twitter
                3 septembre 2024 à 0:06:16

                Tu te prend trop la tête avec le passage d'un unique_ptr. Comme on te l'a dit et comme c'est indiqué dans le code que tu as copié, "a function consuming a unique_ptr can take it by value or by rvalue reference". C'est juste une question de préférence d'écriture, mais le résultat est le même dans ce cas (le compilateur sait qu'il doit optimiser le passage par valeur)

                Dans tous les cas, la copie n'est pas utilisable (c'est le principe du "unique"). 

                Et les references non constantes ne sont pas utilisables sur un temporaire.

                void foo(std::unique_ptr<int>& p) {
                  p = std::make_unique<int>(123);
                }
                
                std::unique_ptr<int> p;
                
                foo(p); //ok
                foo(std::make_unique<int>(123)); // erreur
                foo(std::move(p)); // erreur

                Et pour le retour d'un unique_ptr, c'est aussi un cas où le compilateur optimise, parce qu'il sait que c'est pas une copie.

                • Partager sur Facebook
                • Partager sur Twitter
                  3 septembre 2024 à 1:16:28

                  Ah en fait, l'option 3 ne fonctionnait pas tout simplement parce que les pointeurs n'étaient pas de type Vehicule o_O

                  Ce code compile :

                  #include <iostream>
                  #include <memory>
                  
                  
                  class Vehicule {
                   public:
                    Vehicule(const std::string& brand) : brand_(brand) {}
                  
                    virtual ~Vehicule() = default;
                    virtual void startEngine() = 0;
                  
                   protected:
                    std::string brand_;
                  };
                  
                  class Car : public Vehicule {
                   public:
                    Car(const std::string& brand) : Vehicule(brand) {}
                  
                    void startEngine() override {
                      std::cout << "Car : " << brand_ << " started engine" << std::endl;
                    }
                  };
                  
                  class Motorbike : public Vehicule {
                   public:
                    Motorbike(const std::string& brand) : Vehicule(brand) {}
                  
                    void startEngine() override {
                      std::cout << "Motorbike : " << brand_ << " started engine" << std::endl;
                    }
                  };
                  
                  
                  class Pilot {
                   public:
                    Pilot(std::unique_ptr<Vehicule>& vehicule) {
                      vehicule_ = std::move(vehicule);
                    }
                  
                    void startVehiculeEngine() {
                      vehicule_->startEngine();
                    }
                  
                   private:
                    std::unique_ptr<Vehicule> vehicule_;
                  };
                  
                  
                  int main() {
                    std::unique_ptr<Vehicule> car = std::make_unique<Car>("Nissan");
                    std::unique_ptr<Vehicule> motorbike = std::make_unique<Motorbike>("Ducati");
                  
                    Pilot carPilot(car);
                    Pilot motorbikePilot(motorbike);
                  
                    carPilot.startVehiculeEngine();
                    motorbikePilot.startVehiculeEngine();
                  
                    return 0;
                  }

                  Après je trouve qu'il y a un vrai sujet sur cette histoire de comment passer les pointeurs à une fonction. Je tombe sur beaucoup de discussions sur internet sur ce sujet.

                  J'ai notamment trouvé cette vidéo CppCon 2019 https://www.youtube.com/watch?v=xGDLkt-jBJ4&t=862s dans laquelle l'intervenant indique qu'il faut passer les smart pointeurs par valeur ! :o

                  Alors que d'habitude je suis plutôt habitué à voir des passages par référence constante :

                  void foo(const std::shared_ptr<std::string>& p) {
                    // code
                  }


                  Purée j'étais dans l'erreur pendant toutes ces années !



                  -
                  Edité par JeffWallace 3 septembre 2024 à 1:17:06

                  • Partager sur Facebook
                  • Partager sur Twitter
                    3 septembre 2024 à 1:41:18

                    Dans tous les cas, pas par reference constante, l'ownership n'est pas transféré.

                    Et pour les passages par valeur vs rvalue-reference, c'est osef, c'est pareil.

                    Et non, bof, il n'y a pas de vrai sujet. Dès le début, on t'a expliqué et les choses sont assez claires.

                    • Partager sur Facebook
                    • Partager sur Twitter
                      4 septembre 2024 à 1:09:52

                      La différence entre par rvalue et par valeur est surtout si on veut une erreur récupérable.

                      Par valeur -> l'appelant transfère la propriété, si la fonction échoue (par exemple avec exception), le pointeur est libéré. Par rvalue -> l'appelé peut récupérer l'ownership au dernier moment et s'il y a une erreur, l'appelant possède toujours le pointeur.

                      Dans les fait, il y a rarement besoin que l'appelant conserve l'ownership, mais par habitude j'opte pour la rvalues.

                      > Alors que d'habitude je suis plutôt habitué à voir des passages par référence constante

                      Il faut bien comprendre qu'un transfère d'owernship n'est pas définition pas quelque chose qui peut être constant.

                      • Partager sur Facebook
                      • Partager sur Twitter
                        14 septembre 2024 à 2:26:39

                        Salut,

                        Je n'ai pas lu l'ensemble de la discussion, je risque donc de répéter quelque chose qui a déjà été dit.

                        Le problème de ta conception actuelle est qu'elle semble partir du principe que, si un pilote est détruit, le véhicule dont il est propriétaire le sera forcément aussi.

                        Cela me fait me poser une question idiote : Le pilote d'un véhicule en est-il réellement le propriétaire, celui qui a le droit de le détruire quand bon il lui semble, ou n'est il en définitive qu'un utilisateur du véhicule, à charge pour "quelqu'un d'autre" de choisir le moment où il faut changer de véhicule, voire, qui pourrait décider de fournir le véhicule d'un pilote "disparu" à un autre pilote?

                        Car le principe même du std::unique_ptr est vraiment d'être "seul propriétaire de la ressource sous-jacente"; d'être le seul à pouvoir décider lorsque cette ressource doit (ou plus souvent peut) être libérée. C'est sa responsabilité absolue.

                        Seulement, comme std::unique_ptr n'est jamais qu'un "détail d'implémentation" de ta classe Pilot, cette classe agit "aux yeux du monde" comme si ... elle avait pris à son compte la responsabilité du std::unique_ptr.

                        Or, tout nous indique qu'elle a une autre responsabilité : celle d'utiliser la ressource mise à disposition par le std::unique_ptr. Si bien qu'elle se retrouve désormais avec deux responsabilités : celle d'utiliser la ressource qui lui est propre ET celle de décider du moment opportun de la libérée, et qui vient de std::unique_ptr.

                        Mais alors, que fait on du SRP (Single Responsibility Principle ou Principe de la Responsabilité unique), qui nous dit que chaque type de donnée, chaque donnée, chaque fonction ne devrait faire qu'une seule chose, pour s'assurer de la faire correctement? On le jette aux ortilles?

                        ** Peut-être ** devrais tu faire en sorte que la propriété effective du véhicule n'échoie pas au pilote, mais à "autre chose" comme un VehicleOwner (un propriétaire effectif et reconnu comme tel), qui sera le seul à pouvoir décider de quand "le véhicule a fait son temps et doit être mis à la casse", ce qui permettrait de clarifier la responsabilité du pilote vu qu'il n'en serait alors plus que ... l'utilisateur.

                        Toute la question étant alors de savoir comment faire pour fournir un véhicule à un pilote ;)

                        Je verrais bien l'utilisation d'un std::reference_wrapper dans la classe Pilot pour représenter le véhicule mis à sa disposition.  Ce qui permettrait de changer facilement le véhicule mis à disposition du pilote.

                        Seulement, un std::reference_wrapper doit forcément référence un objet, ce qui obligerait tous les pilotes à avoir un véhicule à leur disposition en permanence.  Ce qui n'est sans doute malheureusement pas toujours garanti...

                        Cependant, nous pourrions résoudre ce problème en créant ce que l'on appelle un "null object": une classe (héritée de la classe Véhicule, dans le cas présent) dont tous les comportement nous font bien comprendre que l'instance utilisée n'est pas un véhicule "valide et cohérent".

                        Elle pourrait prendre une forme proche de

                        class NoVehicule : public Vehicule {
                         public:
                          NoVehicule(const std::string& brand) : Vehicule("None") {}
                         
                          void startEngine() override {
                            std::cout << "No vehicle on which to start engine" << std::endl;
                          }
                        };

                        (histoire de s'adapter à l'existant)

                        Et comme il nous faudra peut-être une instance de ce "non véhicule" y compris pour créer les pilotes (après tout, rien ne dit que chaque pilote aura son véhicule dés son engagement :D), mais qu'il ne sert à rien d'avoir trente six instances de ce non véhicule en mémoire, nous pouvons en créer une instance "globale" grâce à une fonction proche de

                        NoVehicule const & noVehicleAvailable(){
                            static NoVehicule data; // toute la magie sera là
                            return data;
                        }

                        Et bien sur, nous n'oublirons pas de corriger la classe Pilot en conséquence, en lui donnant une forme proche de

                        class Pilot {
                         public:
                          Pilot(Vehicule const & v = noVehicleAviable()):vehicule_(v) {
                          }
                         
                          void startVehiculeEngine() {
                            vehicule_.startEngine();
                          }
                          void changeVehicule(Vehicule const & v=noVehicleAviable()){
                             vehicule_ = std::wrapper(v);
                          }
                         private:
                          std::reference_wrapper<Vehicule> vehicule_;
                        };

                        Alors, je sais que j'ai complètement modifié le problème, que je n'ai absolument pas répondu à ta question d'origine, et que je suis sans doute allé beaucoup plus loin que ce à quoi tu t'attendais.

                        Cependant, ce genre d'approche pourrait te faciliter énormément la vie pour la suite de ton projet ;)

                        -
                        Edité par koala01 14 septembre 2024 à 2:27:52

                        • 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

                        Constructeur qui prend un unique_ptr en argument

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