IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

C++ Discussion :

Surcharge d'opérateur : Retour par référence


Sujet :

C++

  1. #1
    Nouveau membre du Club
    Homme Profil pro
    Étudiant
    Inscrit en
    Octobre 2018
    Messages
    28
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 27
    Localisation : France, Isère (Rhône Alpes)

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Octobre 2018
    Messages : 28
    Points : 30
    Points
    30
    Par défaut Surcharge d'opérateur : Retour par référence
    Bonjour,
    Je suis un débutant en C++ et je suis en train de voir la surcharge d'opérateur.
    Il y a une partie qui me pose problème, en effet, je ne comprends pas pourquoi il est impératif de mettre une référencer au type de sorti quand on veut pouvoir faire des appels en cascade.

    De ce que j'avais compris, une référence et l'objet de la référence sont équivalents et du coup, je ne comprends pas pourquoi les appels en cascade ne fonctionne pas si je ne mets pas la référence.

    Voici le code sur lequel je travaille :
    J'ai deux classes, Utilisateur et Groupe.

    Groupe.h
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    class Groupe {
    public:
        Groupe& operator+=(Utilisateur* user);
     
    private:
        vector<Utilisateur*> utilisateurs_;
    }
    Groupe.cpp
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    Groupe& Groupe::operator+=(Utilisateur* user) {
        utilisateurs_.push_back(user);
     
        return*this;
    }
    Dans le main.cpp je fais :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    Utilisateur* u1 = new Utilisateur("Martin");
    Utilisateur* u2 = new Utilisateur("Franklin");
    Utilisateur* u3 = new Utilisateur("Geraldine");
    Utilisateur* u4 = new Utilisateur("Bernard");
    Utilisateur* u5 = new Utilisateur("Christian");
     
    Groupe* groupe = new Groupe("vacances");
     
    ((((*groupe += u1) += u2) += u3) += u4) += u5;
    Je ne comprends pas pourquoi si je supprime la référence de sorti et que j'écris : Groupe operator+=(Utilisateur* user); à la place de Groupe& operator+=(Utilisateur* user); (pas que dans le .h bien sûr), ça ne marche plus. A chaque +=, le programme crée un nouveau Groupe contenant à chaque fois un nouvel Utilisateur (le dernier += retourne un Groupe avec les 5 Utilisateurs) et à la fin de la ligne, 4 constructeurs sont appelés et le Groupe "groupe" ne contient qu'un utilisateur, u1.

    Je ne comprends pas en quoi retourner un Groupe à la place d'un Groupe& change quoi que ce soit. Si quelqu'un peut m'expliquer, je lui en serai très reconnaissant.

    Merci !

  2. #2
    Modérateur

    Avatar de Bktero
    Homme Profil pro
    Développeur en systèmes embarqués
    Inscrit en
    Juin 2009
    Messages
    4 483
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 37
    Localisation : France, Loire Atlantique (Pays de la Loire)

    Informations professionnelles :
    Activité : Développeur en systèmes embarqués

    Informations forums :
    Inscription : Juin 2009
    Messages : 4 483
    Points : 13 684
    Points
    13 684
    Billets dans le blog
    1
    Par défaut
    Bonjour,

    Il faudrait définir "ça ne marche plus" avec un peu plus de détails...

    Il faut bien comprendre que ce veut dire "retourner *this par référence" et "retourner *this pas par référence". Dans le premier, tu renvoies l'objet en cours (une référence sur *this). Dans le second cas, que renvoies-tu ? Si tu ne renvoies pas une référence sur l'objet en cours, tu renvoies..... une copie ! Quand tu chaines les appels par copie, et bien tu modifies, tu copies, tu modifies, tu copies, et à la fin, si tu ne remets pas tout ça dans un objet que tu gardes et donc tu perds tout...

  3. #3
    Expert éminent
    Avatar de Pyramidev
    Homme Profil pro
    Tech Lead
    Inscrit en
    Avril 2016
    Messages
    1 492
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Tech Lead

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 492
    Points : 6 202
    Points
    6 202
    Par défaut
    Bonjour,

    L'opérateur += est associatif à droite, donc n'est pas adapté pour chaîner les appels dans ce sens : ça t'oblige à écrire une tonne de parenthèses.
    En outre, pour la gestion de la mémoire, il n'y a pas besoin de mettre des new partout. Le C++ est différent du Java.

    Voici une autre version du code qui ne nécessite pas plus de prérequis en C++ :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    #include <string>
    #include <vector>
     
    class Utilisateur {
    public:
    	Utilisateur(std::string nom) :
    		nom_{nom}
    	{}
    private:
    	std::string nom_;
    };
     
    class Groupe {
    public:
    	Groupe(std::string nom) :
    		nom_{nom}
    	{}
    	Groupe& push_back(Utilisateur* user) {
    		utilisateurs_.push_back(user);
    		return *this;
    	}
    private:
    	std::string nom_;
    	std::vector<Utilisateur*> utilisateurs_;
    };
     
    int main()
    {
    	Utilisateur u1{"Martin"};
    	Utilisateur u2{"Franklin"};
    	Utilisateur u3{"Geraldine"};
    	Utilisateur u4{"Bernard"};
    	Utilisateur u5{"Christian"};
     
    	Groupe groupe{"vacances"};
     
    	groupe.push_back(&u1)
    	      .push_back(&u2)
    	      .push_back(&u3)
    	      .push_back(&u4)
    	      .push_back(&u5);
    }
    Plus tard, tu découvriras la range-based for loop qui permettra de remplacer ces appels de push_back par ceci :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    	for(Utilisateur* user : {&u1, &u2, &u3, &u4, &u5})
    		groupe.push_back(user);
    En utilisant d'autres fonctionnalités que tu verras plus tard, le code pourra ressembler à :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    #include <functional>
    #include <string>
    #include <utility>
    #include <vector>
     
    class Utilisateur final {
    public:
    	explicit Utilisateur(std::string nom) :
    		nom_{std::move(nom)}
    	{}
    private:
    	std::string nom_;
    };
     
    class Groupe final {
    public:
    	explicit Groupe(std::string nom) :
    		nom_{std::move(nom)}
    	{}
    	void push_back(Utilisateur& user) {
    		utilisateurs_.push_back(user);
    	}
    private:
    	std::string nom_;
    	std::vector<std::reference_wrapper<Utilisateur>> utilisateurs_;
    };
     
    int main()
    {
    	Utilisateur u1{"Martin"};
    	Utilisateur u2{"Franklin"};
    	Utilisateur u3{"Geraldine"};
    	Utilisateur u4{"Bernard"};
    	Utilisateur u5{"Christian"};
     
    	Groupe groupe{"vacances"};
     
    	for(auto user : {&u1, &u2, &u3, &u4, &u5})
    		groupe.push_back(*user);
    }

  4. #4
    Nouveau membre du Club
    Homme Profil pro
    Étudiant
    Inscrit en
    Octobre 2018
    Messages
    28
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 27
    Localisation : France, Isère (Rhône Alpes)

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Octobre 2018
    Messages : 28
    Points : 30
    Points
    30
    Par défaut
    Tout d'abord merci à tous les deux pour vos réponses.

    Il faudrait définir "ça ne marche plus" avec un peu plus de détails...
    C'est ce que j'essayais d'expliquer, en gros, si je mets pas la référence, mon objet "groupe" ne contient qu'un seul Utilisateur correspondant au premier += qui est fait.

    Il faut bien comprendre que ce veut dire "retourner *this par référence" et "retourner *this pas par référence". Dans le premier, tu renvoies l'objet en cours (une référence sur *this). Dans le second cas, que renvoies-tu ? Si tu ne renvoies pas une référence sur l'objet en cours, tu renvoies..... une copie ! Quand tu chaines les appels par copie, et bien sur modifies, tu copies, tu modifies, tu copies, et à la fin, si tu ne remets pas tout ça dans un objet que tu gardes et bien tu perds tout...
    Alors, si je comprends bien, le premier += modifie l'objet courant et renvoie une copie de l'objet courant qui est donc créée, appelons la groupe1. C'est groupe1 qui est pris en paramètre pour += u2, là encore on retourne une copie, groupe2, qui contient u1 et u2. Ainsi de suite jusqu'à += u5 qui retourne groupe5 qui contient bien u1, u2, u3, u4 et u5.
    Ensuite, toutes les copies (groupe1, ..., groupe5) sont détruites et il ne reste que groupe qui n'a été modifié que par le premier += et qui ne contient donc que u1.

    Est ce bien ça ?

    Pour ce qui est du commentaire de Pyramidev, le main.cpp m'est donné par ma prof et je ne peux pas le modifier. Je suis d'accord sur le fait que ça ne semble pas super optimal, mais j'imagine que son objectif est de nous apprendre à utiliser certains outils.

    En outre, pour la gestion de la mémoire, il n'y a pas besoin de mettre des new partout. Le C++ est différent du Java.
    Je veux bien que tu développes un peu plus ce que tu veux dire par là, mon prof insiste beaucoup pour utilise l'allocation dynamique au maximum.

    Je ne connaissais en effet pas la range-based for loop je vais me renseigner à ce sujet, merci !

  5. #5
    Expert éminent
    Avatar de Pyramidev
    Homme Profil pro
    Tech Lead
    Inscrit en
    Avril 2016
    Messages
    1 492
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Tech Lead

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 492
    Points : 6 202
    Points
    6 202
    Par défaut
    Citation Envoyé par iXeRay Voir le message
    Je veux bien que tu développes un peu plus ce que tu veux dire par là, mon prof insiste beaucoup pour utilise l'allocation dynamique au maximum.
    En général, quand on peut, on construit des objets dans la pile, parce que c'est plus facile à gérer : quand on sort de la portée d'un objet qui a été alloué dans la pile, cet objet est automatiquement détruit.

    Quand on utilise new, il ne faut pas oublier d'appeler delete. Les choses se compliquent quand on prend en compte un mécanisme de gestion d'erreurs qui s'appelle les exceptions, que tu verras plus tard. Concrètement, il y a beaucoup de code qui, quand il rencontre une erreur, interrompt l'exécution du bloc en cours et lance une erreur qui sera peut-être rattrapée quelque part. Or, si ce code qui lance l'erreur se trouve après ton code qui appelle new mais avant celui qui appelle delete, alors le code qui appelle delete ne sera pas appelé.
    Pour cette raison, parmi les bonnes pratiques du C++, il y en a une qui conseille d'éviter de faire des delete à la main et qui conseille d'utiliser des types comme std::unique_ptr, mais tu verras ça aussi plus tard.

    Un autre problème de l'allocation dynamique est qu'elle est moins performante que l'allocation dans la pile, car elle oblige le programme à chercher où il peut allouer dans la mémoire dynamique. Par contre, quand on alloue dans la pile, il n'y a aucune recherche à faire : le programme alloue toujours au sommet de la pile, dont il connaît l'adresse à jour en permanence.

  6. #6
    Nouveau membre du Club
    Homme Profil pro
    Étudiant
    Inscrit en
    Octobre 2018
    Messages
    28
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 27
    Localisation : France, Isère (Rhône Alpes)

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Octobre 2018
    Messages : 28
    Points : 30
    Points
    30
    Par défaut
    D'accord, je vois, je pensais justement qu'on essayait d'utiliser la pile un maximum car sa mémoire était limitée.

    Merci beaucoup pour l’explication !

  7. #7
    Expert éminent
    Avatar de Pyramidev
    Homme Profil pro
    Tech Lead
    Inscrit en
    Avril 2016
    Messages
    1 492
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Tech Lead

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 492
    Points : 6 202
    Points
    6 202
    Par défaut
    C'est vrai que, quand un objet a un type Type tel que sizeof(Type) est très gros, alors il faut envisager de construire cet objet dans la mémoire dynamique pour éviter d'avoir un débordement de pile. C'est le cas de certains tableaux de taille fixe.

    Cependant, il y a une subtilité : si on prend un type comme std::vector<Utilisateur*>, que ton vecteur contienne 0 ou 1000 éléments, il aura toujours le même sizeof. Concrètement, quand tu alloues dans la pile un std::vector<Utilisateur*>, tout ce que std::vector<Utilisateur*> consomme dans la pile, c'est trois nombres : l'adresse vers le premier élément, le nombre d'éléments et la capacité. Les éléments du vecteur seront stockés dans la mémoire dynamique. Et quand cet objet de type std::vector<Utilisateur*> sera détruit, la mémoire dynamique qu'il consommait pour stocker ses éléments sera automatiquement libérée, car c'est std::vector qui gère lui-même la mémoire de ses éléments.
    Rq : attention, le destructeur de std::vector<Utilisateur*> ne détruit pas les objets Utilisateur. Il ne fait que libérer la mémoire où il stockait les éléments de type Utilisateur* qui ne sont que des adresses.

    sizeof(std::string) n'est pas grand non plus. std::string doit stocker la chaîne dans la mémoire dynamique quand celle-ci est trop grande pour être stockée sur place.

  8. #8
    Nouveau membre du Club
    Homme Profil pro
    Étudiant
    Inscrit en
    Octobre 2018
    Messages
    28
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 27
    Localisation : France, Isère (Rhône Alpes)

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Octobre 2018
    Messages : 28
    Points : 30
    Points
    30
    Par défaut
    Du coup en sachant ça, je comprends mieux pourquoi les vecteurs sont si utiles. Certes pas besoins de les agrandir mais en plus ils gèrent automatiquement la mémoire.

    Sachant ça, est ce que ça serait pas plus simple de définir directement un vector<Utilisateur> à la place d'un vector<Utilisateur*> ?
    Mis à part le fait que le vecteur alloue plus d'espace qu'il n'y a d'élément, étant donné qu'on n'aurait plus à faire d'allocation dynamique et donc qu'on ne risquerait plus de fuite de mémoire, est ce que ça ne serait pas plus avantageux de directement définir des vecteur d'Utilisateur ?
    Le seul cas pour lequel ça me semblerait une mauvaise idée c'est si sizeof(Utilisateur) est vraiment très grand.

    En tout cas, merci pour toutes ces explications

  9. #9
    Expert éminent sénior
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Février 2005
    Messages
    5 199
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : France, Val de Marne (Île de France)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : Conseil

    Informations forums :
    Inscription : Février 2005
    Messages : 5 199
    Points : 12 352
    Points
    12 352
    Par défaut
    Très bonne remarque.
    std::vector utilise la copie/clonage d'objet pour gérer les redimensionnements.
    "vector<Utilisateur>" est pertinent si la classe Utilisateur a une sémantique de valeur et non d'entité.
    Une classe est à sémantique de valeur si le fait d'avoir plusieurs copies identiques d'un objet et de facilement le cloner ne pose pas de problème, comme une classe ne contenant que des coordonnées d'un point dans l'espace, par exemple.
    Mais la classe "Utilisateur" a de grande chance d'être à sémantique d'entité, où le cloner (via constructeur de copie, par exemple) pose problème.

  10. #10
    Expert éminent
    Avatar de Pyramidev
    Homme Profil pro
    Tech Lead
    Inscrit en
    Avril 2016
    Messages
    1 492
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Tech Lead

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 492
    Points : 6 202
    Points
    6 202
    Par défaut
    Même avec une classe Utilisateur non copiable, on peut définir et manipuler un std::vector<Utilisateur>. Mais cela implique de connaître la sémantique de mouvement, que l'on ne voit pas encore au début de l'apprentissage du C++.

    En tout cas, même si Utilisateur a une sémantique d'entité, il peut être pertinent de créer un std::vector<Utilisateur>.

  11. #11
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 629
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 629
    Points : 30 692
    Points
    30 692
    Par défaut
    Salut,
    Citation Envoyé par iXeRay Voir le message
    Du coup en sachant ça, je comprends mieux pourquoi les vecteurs sont si utiles. Certes pas besoins de les agrandir mais en plus ils gèrent automatiquement la mémoire.

    Sachant ça, est ce que ça serait pas plus simple de définir directement un vector<Utilisateur> à la place d'un vector<Utilisateur*> ?
    Mis à part le fait que le vecteur alloue plus d'espace qu'il n'y a d'élément, étant donné qu'on n'aurait plus à faire d'allocation dynamique et donc qu'on ne risquerait plus de fuite de mémoire, est ce que ça ne serait pas plus avantageux de directement définir des vecteur d'Utilisateur ?
    Le seul cas pour lequel ça me semblerait une mauvaise idée c'est si sizeof(Utilisateur) est vraiment très grand.

    En tout cas, merci pour toutes ces explications
    Alors, là, il va falloir réfléchir un tout petit peu pour trouver la bonne réponse...

    On peut, en effet, classer peu ou prou tous les types de données personnalisés que nous créerons à l'aide du mot clé class ou du mot clé struct en deux grandes catégories:

    Par leur nature même, les classes qui ont sémantique de valeur sont copiables, et peuvent donc être placées "tels quels" dans les différentes collections dont on peut disposer (dont la classe std::vector), alors que, par leur nature, les classes qui ont sémantique d'entité ne peuvent pas être copiés, et ne pourront donc pas être placés "tels quels" dans une collection quelconque.

    A cela s'ajoute un problème particulier pour les classes ayant sémantique d'entité : ce sont souvent des classes qui interviennent dans des "hiérarchies de classes" (comme une classe "voiture" et une classes "camion" qui héritent toutes les deux d'une classe "véhicule"), et, si l'on décide de maintenir des éléments du type de la classe de base (véhicule, selon mon exemple) dans notre collection, afin de pouvoir y placer "n'importe quel type dérivé" ("voiture" et / ou "camion", selon mon exemple), et profiter du polymorphisme par inclusion pour que chaque élément réagisse exactement de la manière attendue par rapport à son type réel("voiture" OU "camion"), nous devons utiliser des pointeurs ou des références.

    La première question à se poser est donc : "quelle sémantique vas-tu donner à ta classe Utilisateur " ou, si tu préfères : "juges tu cohérent de permettre d'avoir, à un instant T de l'exécution de ton programme, deux instances de ta classe Utilisateur en mémoire qui représentent exactement le même utilisateur "

    Si tu réponds "oui" à cette question, alors, ta classe Utilisateur a bel et bien sémantique de valeur, et l'utilisation d'un std::vector<Utilisateur> peut avoir du sens.

    Mais j'aurais personnellement tendance à répondre "non" à cette question, car, à mon sens, si je veux donner un ordre quelconque à l'un des utilisateurs, je veux que cet ordre puisse s'appliquer exclusivement à cet utilisateur particulier, et non à une "quelconque copie" de cet utilisateur, qui sera sans doute détruite par la suite, et qui n'aura sûrement pas "transmis" ou "répercuté" le résultat de mon ordre à la donnée d'origine.

    De plus, en termes d'utilisateurs, tu dois sans doute t'attendre à avoir de nombreux "utilisateurs simples" (monsieur et madame "Tout Le Monde"), qui n'ont que quelques "droits" très "restrictifs" et quelques "administrateurs", beaucoup moins nombreux que les utilisateurs simples, qui ont -- bien sur -- "tous les droits" dont disposent les utilisateurs simples, mais qui ont -- en plus -- quelques droits "spécifiques", que l'on ne mettra pas entre "toutes les mains", car ils auront accès (sans doute en modification) à des "données sensibles".

    Cela impliquerait que ta classe Utilisateur aurait ... sémantique d'entité, que tu ne pourrait pas copier un utilisateur, et de plus, que tu voudras sans doute placer des "utilisateurs simples" et des "administrateurs".

    Cela impliquerait aussi que tu voudras sans doute profiter du polymorphisme d'inclusion, pour que les administrateurs puissent profiter de leurs "avantages", des droits supplémentaires dont ils disposent par rapport aux utilisateurs simples.

    Or, je l'ai dit plus haut : pour profiter du polymorphisme, tu dois manipuler soit des pointeurs, soit des références!!! Et, du coup, nous entrerons sans doute dans une logique dans laquelle nous devrions effectivement avoir recours à l'allocation dynamique de la mémoire.

    Mais cela implique alors que tu devras te poser une question supplémentaire : "qui / qu'est-ce qui sera le propriétaire des instances de ta classe ".

    En effet, lorsque tu rentres dans une logique de gestion dynamique de la mémoire, il faut se rappeler qu'il y a trois étapes importantes:
    1. la création d'une nouvelle instance (d'utilisateur simple ou d'administrateur)
    2. l'utilisation d'une instance existante (qui sera soit un utilisateur simple soit un administrateur) et
    3. la destruction d'une instance devenue inutile

    Le (1) (la création d'une nouvelle instance) devra -- très clairement -- être dédié à "quelque chose de spécifique" : il ne s'agit pas que tu puisse commencer à créer de nouvelles instances d'utilisateurs n'importe quand, n'importe comment.

    le (2) (l'utilisation des instances existante) regroupe toutes les manipulations que tu pourras entreprendre sur les éléments existants; tous les ordres que tu pourras leur donner, toutes les questions que tu pourras leur poser.

    Toutes les manipulations sauf ... celle qui consisterait à détruire l'instance.

    Quant au (3) (la destruction d'une instance devenue inutile), si tu veux pouvoir travailler "sereinement", tu dois prévoir qu'elle ne pourra être "prise en charge" que par "une seule chose" bien spécifique, qui aura -- littéralement -- "droit de vie et de mort" sur les différentes instances de ta classe.

    Nous désignons cet élément qui a droit de vie et de mort sur les différentes instances de la classe comme le "propriétaire" des instances; "tout le reste" -- ce qui se contente d'"utiliser" les différentes instances de ta classe (qui a droit au (2) mais pas au (3) )-- sera désigné comme "l'utilisateur" des instances, pour bien faire la différence entre les deux.

    Cette distinction est absolument primordiale, car on aura très certainement énormément "de choses" (des classes et des fonctions) qui pourront utiliser les différentes instances de notre classe Utilisateur, alors que nous n'aurons -- a priori -- qu'une seule "chose" (une classe ou une collection) qui pourra effectivement décider de détruire les différentes instances de notre classe (*).

    Il faut savoir aussi que, depuis C++11 (ca date déjà d'il y a sept ans! il serait plus que temps de penser à les utiliser !!!), la bibliothèque nous fourni ce que l'on appelle des "pointeurs intelligents": des classes qui "encapsulent" la notion de pointeur dans ce que l'on appelle une "capsule RAII".

    Le terme RAII est l'abréviation de "Ressource Acquizition Is Initialization" et représente "toute la mécanique" qui permet, lorsque l'on crée une ressource, d'obtenir une ressource parfaitement valide et -- surtout -- lorsque la ressource en question est détruite, de s'assurer qu'elle sera correctement détruite. On pourrait parler de RDIF (pour "Ressource Destruction Is Finalization").

    Les pointeurs intelligents sont donc des classes dont nous pourrons créer des instances "normales" (sans avoir recours à l'allocation dynamique), qui contiennent en interne un pointeur (obtenu au travers de l'allocation dynamique de la mémoire) et qui s'assurent que la mémoire ainsi obtenue sera ... correctement libérée lorsque l'instance du pointeur intelligent sera détruite.

    L'une de ces classes est la classe std::unique_ptr. Son principe est d'être l'unique propriétaire (le seul élément à avoir "droit de vie et de mort") du pointeur qu'il contient et pour lequel nous avons eu recours à l'allocation dynamique de la mémoire.

    Si bien que, quand l'instance de unique_ptr sera détruite, cela ne pourra signifier qu'une seule chose : nous n'avons plus besoin d'accéder à l'espace mémoire représenté par le pointeur qu'il contient en interne, et cet espace mémoire peut donc être libéré correctement.

    Par contre, cette classe pose un problème : si chaque instance de la classe doit être l'unique propriétaire du pointeur qu'elle contient en interne, on se rend compte ... qu'elle ne peut en aucun cas être copiée, autrement, nous nous retrouverions avec deux instances de la même classe qui pourraient toutes les deux prétendre à être ... l'unique propriétaire du pointeur en question.

    C++11 est donc arrivé avec une notion supplémentaire : la notion de déplacement.

    On s'est en effet rendu compte que, lorsque l'on a recours à l'allocation dynamique de la mémoire et que l'on manipule un pointeur, il est "assez facile" -- et souvent bien plus rapide que de faire une copie profonde de l'espace mémoire représenté par le pointeur -- de fournir l'adresse mémoire représentée par le pointeur à ... un autre pointeur; et que, pour peu que l'on modifie le pointeur d'origine pour lui faire représenter une adresse "connue comme étant invalide" (typiquement nullptr), nous nous retrouvons effectivement dans une situation dans laquelle il n'y a ... qu'un seul pointeur qui "pointe" vers l'espace mémoire indiqué.

    Nous pouvons donc envisager de créer une collection d'utilisateur sous une forme proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    std::vector<std::unique_ptr<Utilisateur>> utilisateurs;
    dans lequel nous placerions des std::unique_ptr<Utilisateur> qui seraient chacun l'unique propriétaire de l'espace mémoire vers lequel pointe le pointeur de type Utilisateur * qu'ils contiennent, et qui pourraient libérer correctement la mémoire allouée à ce pointeur chaque fois que ... nous retirons un élément du tableau.

    La seule différence, c'est que, au lieu de copier réellement le std::unique_ptr<Utilisateur> pour le placer dans le tableau (comme l'aurait fait la fonction push_back pour n'importe quel autre type de donnée), nous allons utiliser la sémantique de déplacement, et faire ce que l'on appelle une "copie par déplacement", en utilisant une nouvelle fonction ajoutée à la classe std::vector : la fonction emplace_back, et en précisant que nous voulons effectivement ... utiliser la sémantique de déplacement grâce à std::move)

    En outre, pour créer l'instance de unique_ptr, nous nous tournerons vers une fonction qui n'est apparue qu'en C++14 (mais cela fait quand même déjà quatre ans!!!!): la fonction std::make_unique.

    Et, pour faire "bonne mesure", comme la création d'un nouvel élément ne doit se faire que de manière "très réglementée", nous créerons sans doute une fonction qui la prendra en charge, sous une forme qui serait sans doute proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    void createNewUser(std::vector<std::unique_ptr<Utilisateur>> & users, /* autre paramètres, à définir */){
        if(condition_pour_creer_utilisateur simple){
             auto it = std::make_unique<UtilisateurNormal>(/* paramètres requis*/);
             users.emplace_back(std::move(it));
        }else{
             auto it = std::make_unique<Administrateur>(/* paramètres requis*/);
             users.emplace_back(std::move(it));
        }
    }
    L'idéal étant alors de systématiquement transmettre les utilisateurs sous forme de références (constante ou non, selon le cas) aux fonctions qui en auront besoin, sous une forme qui serait sans doute proche de de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    void foo(Utilisateur /* const*/ & user){
        /* ce qui doit être fait */
    }
    int main(){
         std::vector<std::unique_ptr<Utilisateur>> users;
         /* on crée tous les utilisateurs */
         createAllUsers(users);
         /* puis on les utilise */
         for(auto /* const*/ & it : users){
             foo(*(it.get()));
         }
        return 0;
    } // tous les unique_ptr<Utilisateur> sont détruits, 
      // la mémoire de tous les Utilisateur * "sous-jacents" est correctement libérée
    (*)Dans certains cas bien particuliers, il se peut que tu soit dans l'impossibilité de définir un propriétaire strictement unique pour le pointeur pour lequel tu a recours à l'allocation dynamique de la mémoire, sans doute parce que "plusieurs éléments" doivent garantir la validité du pointeur sous-jacent.

    La libération de la mémoire allouée au pointeur sous-jacent ne peut alors survenir que... lorsque le dernier élément à utiliser le pointeur en prendra la décision.

    Dans ce cas, nous utiliserons la notion de "pointeur partagé" qui nous est fournie le couple de classes std::shared_ptr et std::weak_ptr.

    Cependant, la mécanique qui permet de permet de "compter le nombre de propriétaires" d'un pointeur est lourde et prend "énormément de temps", ce qui aura forcément un effet négatif sur les performances de l'application.

    Il est donc "largement préférable" de tout faire pour arriver à travailler avec un propriétaire strictement unique (et donc avec std::unique_ptr), et de ne "se résoudre" à utiliser les pointeurs partagés que... lorsque l'on n'a vraiment pas d'autre choix

  12. #12
    Nouveau membre du Club
    Homme Profil pro
    Étudiant
    Inscrit en
    Octobre 2018
    Messages
    28
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 27
    Localisation : France, Isère (Rhône Alpes)

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Octobre 2018
    Messages : 28
    Points : 30
    Points
    30
    Par défaut
    Wow, merci pour toutes ces réponses et en particulier merci à koala01 pour ta réponse très détaillée.

    Je n'avais pas entendu parler des sémantiques de valeurs et d'entité. Dans mon exemple, Utilisateur est effectivement une classe à sémantique d'entité comme vous l'avez deviné (même si mon prof nous a fait coder des constructeurs par copie dans mon exercice, encore une fois sûrement dans le but de nous faire pratiquer).

    Je pense avoir compris les différences entre les deux types de classes et même si je n'ai pas tout compris à 100% sur les pointeurs intelligent je pense avoir compris leur utilité. En plus si je me trompe pas, on va aborder cette notion en cours d'ici fin décembre donc je pense qu'à ce moment je serai plus à l'aise avec ce concept.

    Merci encore à tous !

+ Répondre à la discussion
Cette discussion est résolue.

Discussions similaires

  1. Retour par référence sur const
    Par Cheps dans le forum C++
    Réponses: 3
    Dernier message: 14/12/2008, 23h36
  2. Retour par référence d'un pointeur
    Par FunkyTech dans le forum C++
    Réponses: 16
    Dernier message: 22/07/2008, 14h56
  3. Réponses: 2
    Dernier message: 05/04/2008, 17h07
  4. retour par référence de l'opérateur ++
    Par BigNic dans le forum C++
    Réponses: 4
    Dernier message: 02/08/2006, 19h35

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo