Partage
  • Partager sur Facebook
  • Partager sur Twitter

Class Garage / exercice polymorphisme

Solution

    30 mai 2020 à 17:15:11

    Bonjour à tous,

    Quelqu'un peut-il me donner la solution pour la classe Garage a créér. J'ai essayé mais le problème c'est que je ne vois comment faire le lien entre la classe Vehicule et la classe Garage, car on est bien obligé à un moment donné d'écrire l'objet Vehicule dans la classe Garage...

    Merci.

    • Partager sur Facebook
    • Partager sur Twitter
      31 mai 2020 à 0:00:12

      Tu devrais utliser un pointeur vers une voiture que tu vas stocker dans une variable de classe ou dans un vector de pointeurs sur des voitures.
      • Partager sur Facebook
      • Partager sur Twitter

      Il y a deux méthodes pour écrire des programmes sans erreurs. Mais il y a que la troisième qui marche

        31 mai 2020 à 19:37:22

        Salut ! Pas de pointeur nu ! Quelques sources pour comprendre les enjeux et ce qui nous pousse à utiliser les pointeurs intelligents:

        • Partager sur Facebook
        • Partager sur Twitter
          31 mai 2020 à 23:35:03

          Le Garage sert à héberger des véhicules. Donc éventuellement avoir en tant que membre un conteneur de véhicule !

          • Partager sur Facebook
          • Partager sur Twitter
            3 juin 2020 à 13:29:09

            Merci pour vos réponses. Mais c'est un peu compliqué à comprendre à ce stade de l'apprentissage. N'y a t-il pas un moyen sans pointeur intelligent?
            • Partager sur Facebook
            • Partager sur Twitter
              3 juin 2020 à 13:36:35

              Malgré que ça déjà été dit, je ne t'ai pas parlé de pointeur ! Tu as aussi les références.
              • Partager sur Facebook
              • Partager sur Twitter
                3 juin 2020 à 14:03:40

                Ok et avez-vous la solution complète? Ca fait trop longtemps que je cherche et il y a à chaque fois une erreur.
                • Partager sur Facebook
                • Partager sur Twitter
                Anonyme
                  3 juin 2020 à 14:16:49

                  Indique nous tes erreurs. On va les analyser ensemble et ça sera le meilleur moyen pour toi de progresser.

                  > Malgré que ça déjà été dit, je ne t'ai pas parlé de pointeur ! Tu as aussi les références.

                  Si tu parles d'un tableau de références, c'est interdit par la norme C++.

                  • Partager sur Facebook
                  • Partager sur Twitter
                    3 juin 2020 à 14:18:32

                    Je vais te donner la solution que tu trouves compliquée, celle avec avec les smart pointeurs.

                    #include <iostream>
                    #include <vector>
                    #include <string>
                    #include <memory>
                    
                    class Vehicule {
                    public:
                    	virtual std::string type()const = 0;
                    private:
                    };
                    
                    class Voiture : public Vehicule {
                    public:
                    	Voiture( std::string couleur ) : couleur_( couleur ) {
                    	}
                    	std::string type()const override {
                    		return  "voiture " + couleur_;
                    	}
                    private:
                    	std::string  couleur_;
                    };
                    
                    
                    typedef std::unique_ptr<Vehicule> PtVehicule;
                    //typedef Vehicule* PtVehicule;
                    
                    
                    class Garage {
                    public:
                    	~Garage() = default;// rien à faire si on utilise std::unique_ptr
                    	Garage( Garage const& ) = default;// rien à faire si on utilise std::unique_ptr
                    	Garage& operator=( Garage const& ) = default;// rien à faire si on utilise std::unique_ptr
                    	Garage() = default;
                    
                    	void entree( PtVehicule& pVehicule ) {
                    		parc.push_back( std::move(pVehicule) );
                    	}
                    	PtVehicule sortie() {
                    		PtVehicule pVehicule = std::move(parc.back());
                    		parc.pop_back();
                    		return  pVehicule;
                    	}
                    private:
                    	std::vector<PtVehicule>  parc;
                    };
                    
                    int main()
                    {
                    	Garage  garage;
                    	PtVehicule  v1{ new Voiture("rouge") };
                    	PtVehicule  v2{ new Voiture("verte") };
                    	PtVehicule  v3{ new Voiture("bleue") };
                    
                    	garage.entree( v1 );
                    	garage.entree( v2 );
                    	garage.entree( v3 );
                    
                    	PtVehicule  sortie = garage.sortie();
                    
                    	std::cout << "vehicule sorti du garage : " << sortie->type() << '\n';
                    }

                    Pour avoir la solution avec le pointeurs nus, il suffit de :
                    - lever le commentaire ligne 25, et en mettre un ligne 24. Tu auras alors un std::vector<Vehicule*> dans ton Garage.
                    - Tu peux ôter les appels aux métafonctions std::move(). Tu peux aussi les conserver elles servent au transfert de possession des unique_ptr<> mais n'ont pas d'impact sur des pointeurs bruts.
                    - il te reste à ajouter la gestion des allocation libérations propre, il y a du boulot. Je t'ai laissé des indications dans le code. Crois moi les pointeurs nus, c'est la solution compliquée, pas la solution simple.

                    • Partager sur Facebook
                    • Partager sur Twitter

                    En recherche d'emploi.

                      3 juin 2020 à 15:23:48

                      Pourquoi pas rajouter un attribut nodiscard à la fonction Garage::sortie() (seulement si compilation avec un standard supérieur à C++17), puisque il est important de devoir récupérer le véhicule sortant, à moins qu'il puisse se téléporter...

                      C'est pas forcément un + pour le fonctionnement, mais ce genre d'attributs ont leur utilité dans les les applications en C++ moderne, et je trouve celui-ci utile, même si on pourrait largement s'en passer.

                      -
                      Edité par Daimyo_ 3 juin 2020 à 15:33:08

                      • Partager sur Facebook
                      • Partager sur Twitter
                        3 juin 2020 à 15:36:40

                        L'attribut [[nodiscard]] est surtout vital dans le cas des pointeurs nus (si on perd le pointeur on perd une allocation, et je viens de donner le dernier indice pour l'adaptation à des pointeurs nus), dans le cas de l'unique_ptr<> c'est sans risque. Je plussois sur l'ajout de cet attribut, mais tout le monde n'a pas C++17, et le cours de d'openclassroom utilise le C++98 donc il faut peut-être un peu ménager MarcCaz qui a du rattrapage à faire.
                        • Partager sur Facebook
                        • Partager sur Twitter

                        En recherche d'emploi.

                          3 juin 2020 à 15:45:01

                          Oh le vilain pointeur

                           
                          PtVehicule  v1{ new Voiture("rouge") };
                                         /* */
                          

                          Correction:
                            

                            PtVehicule  v1{ std::make_unique<Voiture>("rouge") };
                          

                           Du coup, le type PtVehicule est plus un boulet qu'autre chose.

                          - dans entree(), on a besoin de savoir que c'est un pointeur unique, pour s'autoriser à lui appliquer std::move.

                          - Dans un exemple sur les garages, c'est le moment de sortir l'auto :

                           void entree(std::unique_ptr<Vehicule> & p)
                              {
                                  parc.push_back(std::move(p));
                              }
                          
                              std::unique_ptr<Vehicule> sortie()
                              {
                                  auto v = std::move(parc.back());
                                  parc.pop_back();
                                  return v;
                              }



                          -
                          Edité par michelbillaud 3 juin 2020 à 16:03:04

                          • Partager sur Facebook
                          • Partager sur Twitter
                            3 juin 2020 à 18:39:55

                            Pour l'utilisation de new, j'assume d'avoir choisi cette notation.
                            Pour la non utilisation de auto, j'assume d'avoir choisi cette notation.
                            Mon objectif étant d'amener "lentement" MarcCaz à passer de 1998 à 2017 (en allant trop vite, on risque de le perdre, si ce n'est déjà fait!)
                            J'avais aussi en tête pour la sortie d'un Vehicule de faire un petit std::find() sur un critère quelconque, mais il aurait fallu utiliser un double dispatch. J'assume donc d'avoir limité les nouveauté ;-)

                            • Partager sur Facebook
                            • Partager sur Twitter

                            En recherche d'emploi.

                              3 juin 2020 à 22:33:56

                              Une dernière remarque : attention à la "programmation par mots-clés"

                              Ce n'est pas parce qu'un garage contient (abrite) des voitures qu'un Garage (objet) contient (est propriétaire unique) des Voitures. Et pourquoi le Garage serait plus propriétaire de la voiture que la Personne à qui elle appartient ?

                              Donc il n'y a a priori pas de raisons convaincantes d'utiliser des "pointeurs uniques".

                              Une voiture, elle peut avoir des places dans plusieurs parkings, si on veut, non ?

                              ---

                              Mais bon, c'est encore un problème d'exemple à la con, on ne sait pas ce que le programme est censé faire avec les informations sur voitures et des garages (un programme n'agit pas sur des vraies voitures ou garages, mais manipule des infos à leur sujet).

                              -
                              Edité par michelbillaud 3 juin 2020 à 23:25:00

                              • Partager sur Facebook
                              • Partager sur Twitter
                                4 juin 2020 à 12:43:03

                                Super! J'ai failli me perdre mais à force de persévérance, j'ai compris. Je serai certes pas capable de le refaire mais c'est déjà un bon début.

                                Quelques remarques:

                                1) Il est dit que pour définir un pointeur intelligent, il faut encapsuler un pointeur nu. Je comprends que c'est pas obligatoire vu le code.

                                2) La méthode auto sert en fait à créer de nouveau pointeur intelligent? Je ne comprends pas très bien son application et sa définition.

                                3) C'est parce que sortie est un pointeur intelligent pointant sur un objet de type Vehicule qu'on peut se permettre de faire sortie->type, c'est bien ça?

                                • Partager sur Facebook
                                • Partager sur Twitter
                                  4 juin 2020 à 13:03:49

                                  auto n'est pas une méthode, mais une déclaration implicite de type.

                                  La ligne

                                  auto v = std::move(parc.back());

                                  déclare une variable v, dont le type est celui du résultat de l'expression (le type statique).

                                  Comme parc est un vecteur de unique_ptr<....>, parc.back est un unique_ptr, auquel on applique un move qui retourne un unique_ptr, et donc, ben oui, c'est un std::unique_ptr<Vehicule>.

                                  Avantage

                                  • ça allège les écritures
                                  • ça marche avec les templates qui ont des types hétérogènes
                                  • plus intéressant : ça souligne que ce qu'on écrit n'a rien de spécial. et quandon ne met pas auto, c'est qu'il y a une bonne raison.

                                  Exemple

                                  #include <iostream>
                                  
                                  template <typename T> 
                                  
                                  T foo(T t1, T t2) {
                                      auto plus = t1 + t2;
                                      return plus;
                                  } 
                                  
                                  int main() {
                                      std::string s1 = "abc";
                                      std::string s2 = "def";
                                      auto x = foo(s1, s2);
                                      auto y = foo(2, 3);
                                      std:: cout << x << " " << y << std::endl;
                                  }
                                  

                                  Le premier auto, dans la fonction, indique que le type de la variable plus est celui retourné par l'opérateur "+" quand il est appliqué à quelque chose. On aurait pu mettre T, mais si on imagine qu'on a un type où l'operator+() retourne un truc d'un autre type, ça marchera aussi

                                  Les deux derniers, tout le monde comprend, ça correspond a std::string et int, le type "normal" auquel tout le monde s'attend.

                                  Et pourquoi pas de auto pour les déclarations de s1 et s2 ? Parce que "abc" est un char*, pas une string, et qu'on ne peut pas faire + sur deux pointeurs. Donc il y a une "conversion" à faire (en fait un appel de constructeur).

                                  Dans la mesure où on a décidé d'utiliser auto partout où ça marche (le type "naturel"), le fait d'avoir indiqué explicitement un type attire l'attention sur la nécessité de le faire.

                                  ---

                                  il me semble qu'il y a une évolution des langages modernes dans ce sens, qui pousse à ce que les déclarations soient par défaut des déclarations de constantes, avec le type déduit de l'initialisation. Si on ne veut pas ça, il faut réclamer.




                                  -
                                  Edité par michelbillaud 4 juin 2020 à 13:07:26

                                  • Partager sur Facebook
                                  • Partager sur Twitter
                                    4 juin 2020 à 13:51:48

                                    > Pour définir un pointeur intelligent il faut encapsuler un pointeur nu.

                                    En fait, quand on créé une ressource qui a besoin d'être gérée manuellement, on doit trouver un accord: Qui possède la ressource (qui la gère) ? C'est tout là le problème des pointeurs nus, on doit libérer soi même la mémoire allouée. Ca peut aller très loin ce genre de situation. Dans un petit programme sans trop d'intérêt c'est pas très grave, on perdra pas forcément le fil de la ressource, par contre, dans les gros projets, ça peut poser problème, et il se peut parfaitement que la ressource ne soit pas libérée, et que personne ne sache quoi/qui la possède.

                                    En C++ (comme dans certains autres langages) une règle a été inventée: la règle RAII (Ressource Acquisition Is Initialization), qui permet de ne plus avoir à gérer manuellement une ressource. Les pointeurs intelligents sont un bon exemple d'application de cette règle. Au lieu d'avoir à dé-allouer soi même un pointeur, c'est "le contexte de vie" de l'objet (pointeur intelligent) qui encapsule la ressource, qui s'en occupe. Lors de l'initialisation d'un objet std::unique_ptr<Foo>, une ressource est encapsulée dans l'objet. Lorsque l'objet sera détruit, la ressource sera dé-allouée. Le pointeur intelligent possède la ressource, c'est lui qui la gère, plus nous.

                                    On a donc:

                                    {
                                    
                                      pointeur_intelligent<type> mon_pointeur {};
                                    
                                    } // mon_pointeur est détruit ici, la ressource est
                                      // libérée.

                                    Au lieu de:

                                    {
                                    
                                      type* mon_pointeur = new type;
                                      delete mon_pointeur; // On libère la ressource soi-même
                                    
                                    }

                                    Voilà, les pointeurs intelligents font le sale boulot de libération de ressources à notre place, et il en existe de plusieurs sortes. On peut avoir un pointeur intelligent qui partage la ressources avec d'autres pointeurs de ce type etc. C'est très vaste et complexe comme sujet.
                                    Je suis pas allé très loin et ça ne doit pas être hyper clair pour toi, mais plus tu te documenteras et mieux tu comprendras.

                                    Les connaisseurs, excusez-moi si je dérive, veuillez me corriger en cas de connerie dite xD.

                                    -
                                    Edité par Daimyo_ 4 juin 2020 à 13:52:54

                                    • Partager sur Facebook
                                    • Partager sur Twitter
                                      4 juin 2020 à 16:26:46

                                      Salut,

                                      Daimyo_ a écrit:

                                      > Pour définir un pointeur intelligent il faut encapsuler un pointeur nu.

                                      En fait, quand on créé une ressource qui a besoin d'être gérée manuellement, on doit trouver un accord: Qui possède la ressource (qui la gère) ? C'est tout là le problème des pointeurs nus, on doit libérer soi même la mémoire allouée. Ca peut aller très loin ce genre de situation. Dans un petit programme sans trop d'intérêt c'est pas très grave, on perdra pas forcément le fil de la ressource, par contre, dans les gros projets, ça peut poser problème, et il se peut parfaitement que la ressource ne soit pas libérée, et que personne ne sache quoi/qui la possède.

                                      Pour te permettre, peut-être, de mieux comprendre:

                                      Peut-être vais-je apporter une nouvelle notion, mais, il y a un principe (parmi d'autres) à respecter scrupuleusement si l'on souhaite assurer un minimum de maintenabilité au code, tout en évitant de faire "trop d'erreurs": le principe dit "de la responsabilité unique".

                                      Pour faire simple, ce principe nous dit que

                                      n'importe quelle fonction, n'importe quelle donnée, n'importe quel type de donnée ne doit poursuivre qu'un (et un seul) objectif, ne doit servir qu'à une (et une seule) chose

                                      Or, n'importe quel programme, n'importe quel partie de programme et, parfois même, certaines fonctions particulières, va suivre une séquence -- sommes toutes -- particulièrement logique qui consiste en trois étapes, que nous pourrions résumer comme suit:

                                      1. initialisation : on créée la donnée (ou le jeu de données) que l'on va manipuler (potentiellement à partir d'informations qu'on récupère "par ailleurs")
                                      2. manipulation : mon manipule la donnée (ou le jeu de données) pour obtenir le résultat attendu
                                      3. finalisation: on sauvegarde le résultat et on "fait le ménage".

                                      Typiquement, même le plus simple des programmes, comme le plus complexe d'ailleurs, pourrait donc ressembler à quelque chose comme

                                      /* on a trois fonctions effectuant chacune une des étapes cités
                                       */
                                      /* une pour l'initialisation, qui renvoie la donnée 
                                       *(ou le jeu de données)
                                       */
                                      Type init(){
                                          /* ce qu'elle doit faire */
                                      }
                                      /* une pour la manipulation, qui recoit comme paramètre la
                                       * donnée (ou le jeu de données) créé(e) par la fonction 
                                       * init
                                       */
                                      void execute(Type data){
                                         /* ce qu'elle doit faire */
                                      }
                                      /* et une pour la finalisation, qui reçoit aussi comme 
                                       * paramètre la donnée (ou le jeu de données)
                                       */
                                      void finalize(Type data){
                                         /* Tout ce qu'elle doit faire 
                                          */
                                      }
                                      /* Ces trois fonctions sont "mises en musique" et utilisée
                                       * au niveau de notre programme dans la fonction qui 
                                       * sert de "point d'entrée" au programme
                                       */
                                      int main(){
                                          Type d = init();
                                          execute(d);
                                          finalize(d);
                                          /* indique au système d'exploitation que tout s'est
                                           * déroulé "comme prévu"
                                           */
                                          return 0;
                                      }

                                      Le problème avec les pointeurs, c'est que ce sont des données (numériques, entières, généralement non signées) qui représentent ... l'adresse mémoire à laquelle se trouve (normalement) la donnée qui sera effectivement manipulée (et que l'on espère être du type indiqué).

                                      Si, dans ce code, Type représente un pointeur, il faut donc garantir que l'adresse indiquée (par d, dans le code) contienne effectivement une donnée du type indiqué.  Or, on est confronté à plusieurs problèmes avec cette approche:

                                      • un pointeur, c'est une valeur numérique entière.  Je peux donc écrire d++; dans le code, ce qui implique que l'adresse indiquée par d correspond désormais à ... la première adresse mémoire disponible qui suit l'adresse mémoire à laquelle se trouve effectivement la donnée (*)
                                      • comme un pointeur n'est qu'une valeur numérique entière, le pointeur  en lui-même ne transporte aucune information sur le type de donnée qui se trouve réellement à l'adresse mémoire: dans le meilleur des cas, le type de pointeur indique le type de la donnée qui est sensée se trouver à l'adresse mémoire indiquée.  Mais il n'y a ... aucune garantie que ce soit effectivement le cas.
                                      • lorsque le développeur utilise l'allocation dynamique de la mémoire (avec le mot clé new), il prend explicitement la responsabilité de décider du moment où la mémoire pourra être "récupérée" par le système (et potentiellement utilisée pour autre chose), ce qu'il doit faire à l'aide du mot clé delete (**).

                                      Si l'on veut placer les mots clés new et delete "au bon endroit" par rapport à la séquence logique que j'ai présentée plus haut, il faut que new prenne place dans la fonction init() et que delete prenne place dans la fonction finalize.

                                      MAIS...

                                      (*) si on a modifiée l'adresse pointée par le pointeur à l'aide de la fameuse instruction d++;, on court à la catastrophe parce que l'on ne libère absolument pas l'adresse mémoire que l'on voulait libérer.  Un petit exemple, basé sur le précédant, pour te faire comprendre:

                                      Type * init(){
                                          /* on se fout pas mal du type de la donnée que l'on
                                           * veut réer
                                          Type * ptr = new Type; 
                                          /* ce qu'elle doit faire */
                                          return ptr;
                                      }
                                      void execute(Type * data){
                                         /* ce qu'elle doit faire */
                                      }
                                      void finalize(Type * data){
                                          /* ce qu'elle doit faire, sans oublier de libérer
                                           * la mémoire à la fin
                                           */
                                          delete data;
                                      }
                                      int main(){
                                          Type d = init();
                                          execute(d);
                                          d++; // CRACK: d représente maintenant l'adresse 
                                               // mémoire équivalent à d + sizeof(Type)
                                          finalize(d); // BOUM: lors de l'appel à delete, on ne
                                                       // libère pas l'adresse mémoire qui a été
                                                       // allouée lors du new dans init
                                          /* indique au système d'exploitation que tout s'est
                                           * déroulé "comme prévu"
                                           */
                                          return 0;
                                      }

                                      (**) savais tu que C++ est un langage à exceptions? et, surtout, as tu bien compris ce que cela implique?

                                      Cela implique que, si une exception est lancée à un moment quelconque de l'exécution du programme, ton programme va littéralement abandonner flux d'exécution normal pour trouver l'endroit où l'exception pourra être gérée (s'il existe).

                                      Autrement dit, il va imposer une fin prématurée à toutes les fonctions qui sont en cours d'exécution, jusqu'à ce qu'il trouve l'endroit où l'exception pourra être gérée; et, la fonction main, qui sert de "point d'entrée" au programme n'y fera pas exception :'(

                                      Si bien que, si l'exception lancée n'a pas pu être gérée au plus tard dans la fonction main, c'est carrément tout le programme qui va s'arrêter.

                                      Voici un petit exemple pour te permettre de comprendre:

                                      void * maybeThrows(){
                                          /* du code */
                                          if(condition){
                                              throw std::runtime_exception("une erreur s'est produite");
                                          }
                                          /* Tout le code qui se trouve ici NE SERA PAS EXECUTE
                                           * si l'exception est lancée
                                           */
                                      }
                                      void unSecureCall(){
                                          /* du code */
                                          maybeTrhows(); // appelle la fonction qui risque de 
                                                         // lancer une exception
                                          /* Tout le code qui se trouve ici NE SERA PAS EXECUTE
                                           * si l'exception est lancée par maybeThrows
                                           * ou, si tu préfères:
                                           * tout le code qui se trouve ici ne sera exécuté
                                           * QUE SI maybeThrows S'EXECUTE CORRECTEMENT 
                                           */
                                      }
                                      void secureCall(){
                                          /* du code */
                                          try(){
                                              /*il n'y a qu'ici que l'exception lancée
                                               * par maybeThrows sera "récupérée"
                                               */
                                             unSecureCall();
                                             /* mais tout le code qui se trouve ici NE SERA PAS EXECUTE
                                           * si l'exception est lancée par maybeThrows
                                           * ou, si tu préfères:
                                           * tout le code qui se trouve ici ne sera exécuté
                                           * QUE SI maybeThrows S'EXECUTE CORRECTEMENT 
                                           */
                                          }catch(std::runtime_exception &e){
                                              /* on traite l'exception
                                               * si on arrive à résoudre le problème, tant mieux
                                               * sinon, on relance l'exception
                                               */
                                              if(! probleme_resolu)
                                                  throw e;
                                          }
                                          /* le code qui se trouve ici ne sera exécuté 
                                           * QUE SI L'EXCEPTION N'A PAS ETE RELANCEE DANS LE
                                           * BLOC catch
                                           */
                                      }
                                      int main(){
                                          /* du code */
                                          /* l'appel à secureCall offre trois possibilités
                                           *
                                           * - maybeThrows ne lance pas d'exception
                                           * - maybeThrows lance une exception qui n'est pas
                                           *   relancée par secureCall
                                           * - maybeThrows lance une exception qui est relancée
                                           *   par secureCall
                                           */
                                          secureCall();
                                          /* l'exécution du code qui est ici dépendra de la 
                                           * situation dans laquelle on se trouve suite
                                           * à l'appel de secureCall
                                           *
                                           * si maybeThrows ne lance pas d'exception ou que
                                           * secureCall ne la renvoie pas, le code qui se trouve
                                           * ici sera exécuté
                                           *
                                           * si maybeThrows lance une exception et que secureCall
                                           * la relance, le code ne sera pas exécuté
                                           */
                                      }

                                      Evidemment, je t'ai présenté un exemple dans lequel "tout se joue" au niveau de la fonction main, mais ca pourrait tout aussi bien être une fonction quelconque appelée par une obscure fonction se trouvant dans un tout aussi obscure sous module d'un module abandonné au fin fond du répertoire oublié de ton projet :waw::'(.

                                      Tu te demandes peut-être ce que cela peut changer avec les pointeurs nus.  Hé bien, modifions un peu le code que j'ai présenté avec ma "séquence logique":

                                      Type * initSomeObscurePart(){
                                          /* on se fout pas mal du type de la donnée que l'on
                                           * veut réer
                                          Type * ptr = new Type; 
                                          /* ce qu'elle doit faire */
                                          return ptr;
                                      }
                                      void executeSomeObscurePart(Type * data){
                                          /* du code */
                                          if(condition)
                                              throw std::runtime_error("something goes wrong");
                                          /* encore du code
                                           * et tu sais ce qu'il en advient
                                           */
                                      }
                                      void finalizeSomeObscurePart(Type * data){
                                          /* ce qu'elle doit faire, sans oublier de libérer
                                           * la mémoire à la fin
                                           */
                                          delete data;
                                      }
                                      void someObcureCaseInForgottenSubModule(){
                                          Type * d = initSomeObscurePart(); // OK
                                          executeSomeObscurePart(d); // !!!!
                                                                     // l'exception pourrait très
                                                                     // bien être lancée, pour
                                                                     // ce qu'on en sait
                                          /* le code qui suit sera-t-il exécuté???? */
                                          finalizeSomeObscurePart(d);
                                          /* que se passe-t-il avec d si l'exception a été 
                                           * lancée???
                                           */
                                      }

                                      Le tout, en sachant qu'il se peut "tout aussi bien" que l'exception soit récupérée "en amont" de ce "sous module oublié de tous", et que, à ce moment là, on n'a plus aucun moyen de récupérer la valeur de d ????

                                      A coté de cela, C++ impose des règles bien précises concernant les données que l'on peut désigner comme "définies sur la pile", par opposition des données "définies sur le tas" (en utilisant l'allocation dynamique de la mémoire).

                                      Et le fait est que ces règles sont particulièrement simples: les données définies "sur la pile" n'existent et ne sont accessibles qu'entre le moment de leur déclaration et l'accolade fermant le bloc d'instructions dans lequel elles sont déclarées (on parle de la "portée" des données).

                                      Elles indiquent aussi (même si cela n'a pas beaucoup d'importance ici) que les données seront détruite automatiquement lorsque l'on atteint la fin de leur portée dans l'ordre inverse de leur déclaration.

                                      Voici un petit exemple pour comprendre le principe:

                                      int main(){
                                          /* je me fous pas mal des types de données représentées
                                           */
                                          Type a;  // a est accessible à partir d'ici
                                          /* du code */
                                          AutreType b; // et b est accessible à partir d'ici
                                          if(condition){
                                              TroisiemeType c; // toujours pareil
                                               // a, b et c sont acccesibles ici
                                              /* du code */
                                          }  /* fin de portée pour c qui est automatiquement
                                              * détruit ici. Son destructeur est automatiquement
                                              * appelé
                                              */
                                          /* a et b sont toujours accessibles ici, mais c ne
                                           * l'est plus
                                           */
                                          DernierType d; //bon, tu commences à savoir quoi
                                          /* on peut utiliser a, b et d ici
                                           */
                                      } /* fin de portée pour a, b et d qui sont détruites 
                                         * automatiquement dans l'ordre inverse de leur 
                                         * déclaration, à savoir:
                                         * - d (qui a été déclaré en dernier) en premier
                                         * - suivi de b (qui a été déclaré avant d, mais après a)
                                         * - et enfin a (qui a été déclaré en premier)
                                         * et dont les destructeurs respectifs sont appelés dans 
                                         * l'ordre indiqué
                                         */

                                      Le but des pointeurs intelligents est effectivement de les charger du "sale boulot" qui consiste à décider quand une ressource -- allouée "sur le tas" -- peut être libérée, grâce au fait que eux, ils sont créés sur "la pile", et que l'on peut donc définir très précisément le moment où leur destructeur sera appelé ;)

                                      • 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
                                        4 juin 2020 à 17:55:15

                                        On peut tenter d'expliquer le lien exceptions/pointeur intelligents de façon plus compacte.

                                        Imaginons une fonction qui alloue un truc, et fait un traitement dessus,  Avec les pointeurs "ordinaires"

                                        void foo() {
                                           Truc * t = new Truc;
                                           traiter(t);   // bof
                                        }
                                        

                                        Vous savez qu'il va y avoir un problème, parce qu'on n'a pas libéré l'objet alloué, pointé par t. Bon, ok, peut être que traiter() s'en occupe, mais normalement, pour que les vaches soient bien gardées, ça devrait être celui qui alloue qui s'occupe aussi de libérer. Si la fonction s'appelle traiter(), c'est pas traiter_et_liberer().

                                        Donc pour avoir une programmation saine où on sait qui fait quoi, ça sera plutôt

                                        void foo() {
                                           Truc * t = new Truc;             // allocation
                                           traiter(t);   
                                           delete t;                        // libération
                                        }

                                        Bon maintenant, on se rappelle qu'en C++ on utilise les exceptions pour gérer les erreurs. Si dans une autre fonction, je mets un try catch autour de l'appel à foo

                                        try {
                                          foo();
                                        } catch (MonException e) {
                                            ...
                                        }

                                        je vais intercepter l'exception (cool) et faire ce qu'il faut, sauf que le delete t n'aura pas été exécuté dans foo(). Un objet a été alloué et ne sera jamais libéré : on a une belle fuite mémoire.

                                        Il faut se rappeler que les variables d'un bloc sont _détruites_ quand on sort d'un bloc, que ça soit à la fin ou qu'on s'échappe en levant une exception, mais que détruire un pointeur (qui est un type de base), ça veut juste dire que l'espace occupé par le _pointeur_ est libéré. Mais ce qui est pointé est toujours là. Ca ne fait pas le delete. Un pointeur "de base", il n'exécute pas de destructeur.

                                        ---- en route pour les pointeurs intelligents

                                        Alors une manière de tricher serait de définir un objet Gardien, qui contient un pointeur, et le libère quand il est détruit

                                        void foo() {
                                           Gardien g {new Truc};
                                           traiter(g.ptr);   
                                        }
                                        
                                        
                                        class Gardien {
                                        public : 
                                           Truc *ptr;
                                           Gardien(Truc *p) : ptr{p} {
                                           }
                                          ~Gardien() { 
                                             delete ptr; 
                                           }
                                        };
                                        

                                        Et voila.  En sortant, le gardien est détruit, ce qui amène automatiquement l'exécution de son destructeur, qui s'occupe de la libération.

                                        On travaille un peu le concept, on habille ça décemment avec quelques surcharges pour faire croire que le gardien est un pointeur, et empêcher de faire n'importe quoi avec, et hop, on vient d'inventer le "pointeur intelligent".


                                        PS: l'encapsulation du pointeur nu, c'est un grand mot pour dire qu'un Gardien contient un pointeur, sur lequel il veille avec des petits yeux affectueux.

                                        -
                                        Edité par michelbillaud 4 juin 2020 à 18:04:53

                                        • Partager sur Facebook
                                        • Partager sur Twitter

                                        Class Garage / exercice polymorphisme

                                        × 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