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 :

héritage privé: Quand l'utiliser ?


Sujet :

C++

Vue hybride

Message précédent Message précédent   Message suivant Message suivant
  1. #1
    Membre Expert
    Homme Profil pro
    Étudiant
    Inscrit en
    Juin 2012
    Messages
    1 711
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Juin 2012
    Messages : 1 711
    Par défaut héritage privé: Quand l'utiliser ?
    hello,

    j'aimerai bien comprendre l'héritage privé...

    Je post suite à ce message (cf ici)
    (ça à pas l'air de marcher, mais je link le post de Kalith, #9).

    Oui, probablement un message de grand débutant ^^"

    Quand peut-t-on hérité d'une classe "hinéritable" (destructructeur privé) ?
    Et quels sont les "gains" / "pertes" face à un héritage classique ?

    Oui, question de débutant, m'enfin, il faut bien =)

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

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 644
    Par défaut
    Salut,

    Allez, un petit tour vers la FAQ.

    L'héritage publique et l'héritage privé sont deux choses totalement différentes:

    L'héritage publique permet de profiter de ce que l'on appelle la substituabilité, ce qui ouvre la voie à l'utilisation de comportements polymorphique.

    L'idée est que l'on peut transmettre un pointeur ou une référence vers un objet de type dérivé partout à toute fonction qui prend un pointeur ou une référence vers un objet du type de base et que les comportements observés seront ceux que l'on attendrait de la part du type dérivé.

    L'héritage privé n'a rien à voir avec la substituabilité : tu ne peux absolument pas envisager de transmettre un pointeur ou une référence vers un objet du type dérivé à une fonction qui attend comme argument un pointeur ou une référence vers un objet du type de base.

    La raison en est simple : le type de base n'est purement et simplement pas visible depuis l'extérieur (c'est le principe de l'accessibilité privée )

    L'héritage privé doit être vu comme une relation "EST IMPLEMENTE EN TERMES DE" et permet, essentiellement, de récupérer facilement certaines fonctions propres au type de base (tout en nous autorisant l'accès à toutes les fonction publique du type de base à l'intérieur des fonctions membres du type dérivé).

    C'est une relation finalement très proche de la relation de composition, à ceci près qu'elle te permet, en plus, d'accéder aux éventuelles fonctions protégées du type de base.

    C'est donc, malgré tout, une relation beaucoup plus forte que la "simple" composition, et il reste malgré tout chaudement recommandé de préférer la composition à l'héritage privé, à moins bien sur que tu n'aies un besoin impératif de pouvoir accéder aux éventuelles fonctions protégées
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

  3. #3
    Membre Expert
    Homme Profil pro
    Étudiant
    Inscrit en
    Juin 2012
    Messages
    1 711
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Juin 2012
    Messages : 1 711
    Par défaut
    Mais dans ce cas, ce qui correspond plus ou moins à hériter des conteneur de la STL, y a t'il une fuite mémoire ?

    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
    #include <iostream>
     
    struct A {
    	int *m_a;
     
    	A(): m_a(new int[42])
    	{ }
     
    	~A() {
    		delete[] m_a;
    	}
     
    	virtual  void foo() {
    		std::cout << "A\n";
    	}
    };
     
    struct B: private A {
    	B(): A()
    	{ }
     
    	virtual void foo() {
    		std::cout << "B\n";
    	}
     
    };
     
    int main() {
    	B b;
    	b.foo();
     
    	return 0;
    }

  4. #4
    Rédacteur/Modérateur


    Homme Profil pro
    Network game programmer
    Inscrit en
    Juin 2010
    Messages
    7 147
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 38
    Localisation : Canada

    Informations professionnelles :
    Activité : Network game programmer

    Informations forums :
    Inscription : Juin 2010
    Messages : 7 147
    Billets dans le blog
    4
    Par défaut
    Il n'y a aucune fuite mémoire et aucune raison d'y en avoir.
    Tu déclares un B, qui a pour typage statique B.
    C'est le destructeur de B qui sera appelé, qui appelera automatiquement celui de A, c'est comment fonctionne l'héritage.
    Le polymorphisme (la substitualité mentionnée par Koala) est lié au typage dynamique.
    Pensez à consulter la FAQ ou les cours et tutoriels de la section C++.
    Un peu de programmation réseau ?
    Aucune aide via MP ne sera dispensée. Merci d'utiliser les forums prévus à cet effet.

  5. #5
    Membre Expert
    Homme Profil pro
    Étudiant
    Inscrit en
    Juin 2012
    Messages
    1 711
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Juin 2012
    Messages : 1 711
    Par défaut
    Ok!

    Je pensais que le destructeur non virtuel posais problème (il y aurait bien une fuite mémoire avec une héritage publique ?)

    En tout cas c'est bon à savoir à savoir, merci !

  6. #6
    gl
    gl est déconnecté
    Rédacteur

    Homme Profil pro
    Inscrit en
    Juin 2002
    Messages
    2 165
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 46
    Localisation : France, Isère (Rhône Alpes)

    Informations forums :
    Inscription : Juin 2002
    Messages : 2 165
    Par défaut
    Citation Envoyé par Iradrille Voir le message
    Je pensais que le destructeur non virtuel posais problème (il y aurait bien une fuite mémoire avec une héritage publique ?)
    Pas avec un tel usage (il n'y de substituabilité ici) que ce soit en héritage privée ou public.

    le problème se pose lorsque le type statique et dynamique

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    int main() 
    {
    	A* a = new B;
    	a->foo();
    	delete a;
     
    	return 0;
    }
    Construction que te permet l'héritage public. C'est même son principal intérêt : sous-typage et polymorphisme de sous-typage (aka polymorphisme d'inclusion).
    Et ne te permet pas l'héritage privé qui est un héritage de réutilisation.

    C'est pourquoi les classes où le destructeur n'est pas virtuel ne sont pas des candidats à un héritage public mais peuvent l'être pour un héritage privé (au passage, c'est ce qu'expliquait déjà koala01 dans son post)

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

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 644
    Par défaut
    En fait, le problème est inverse par rapport à ce que tu crois

    Mais pour te permettre de comprendre cette remarque et, surtout, d'en prendre la pleine mesure, je vais encore une fois devoir me lancer dans un véritable romane (re )

    D'abord, il faut savoir que le destructeur d'une classe est, à quelques points près, tout à fait semblable à n'importe quelle autre fonction.

    Les seules différences sont:
    1. que c'est toujours une fonction membre non statique
    2. qu'il ne peut pas prendre d'argument
    3. que c'est l'une des rares fonctions qui ne peut pas avoir de valeur de retour
    4. qu'il est automatiquement appelé lors de la destruction de l'objet
    5. que c'est l'une des quatre fonctions dont le compilateur fournira une version "naive" (publique, non statique, non virtuelle et inline) s'il ne trouve aucune raison de ne pas le faire (les raisons de ne pas le faire étant que nous en ayons donné une déclaration et (c'est préférable) une implémentation )
    Ensuite, il faut aussi se rappeler de ce qu'est la virtualité d'une fonction.

    Lorsque l'on déclare une fonction virtuelle, on demande au compilateur de mettre en place "tout un mécanisme" qui permettra de faire en sorte que si l'on fait appel à cette fonction depuis un pointeur ou une référence sur un objet du type de base alors que l'objet référencé (pointé) est un objet d'un type dérivé, nous observerons le comportement propre au type réel (autrement dit, du type dérivé).

    Ce genre de comportement est ce que l'on appelle "le polymorphisme".

    De cette manière, si la classe B hérite de la classe A sous une forme proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    class A{
        public:
            virtual void print() const{std::cout<< "je suis un A"<<std::endl;}
    };
    class B : public A{
        public:
            /* virtual */ void print() const{std::cout<<"je suis un B"<<std::endl;}
    };
    le code
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    int main(){
       B obj; // une instance "normale" de B
       A* ptrA = &obj; // un pointeur sur un objet "passant pour être" de type A
       B* ptrB = &obj; // un pointeur sur un objet de type B
       A& refA = obj;  // une référence sur un objet "passant pour être de type A
       B& refB = obj;  // une référence sur un objet  de type B
       obj.print();
       ptrA->print();
       ptrB->print();
       refA.print();
       refB.print();
       return 0;
    }
    nous donnera à chaque fois la même sortie, à savoir "je suis un B", et ce, grâce au fait que la fonction print a été déclarée virtuelle dans la classe de base (A).

    Par contre, si l'on a les classe C et D et que D hérite de C sous une forme proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class C{
        public:
            /* ATTENTION : ceci N'EST PAS UNE FONCTION VIRTUELLE */
            void print() const{std::cout<<"je suis un C"<<std::endl;}
    };
    class D : public C{
        public:
            /* ATTENTION : ceci N'EST PAS UNE FONCTION VIRTUELLE */
            void print() const{std::cout<<"je suis un D"<<std::endl;}
    };
    le meme code (adapté au nouveaux noms de types )
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    int main(){
       D obj; // une instance "normale" de D
       C* ptrC = &obj; // un pointeur sur un objet "passant pour être" de type C
       D* ptrD = &obj; // un pointeur sur un objet de type D
       C& refC = obj;  // une référence sur un objet "passant pour être de type C
       D& refD = obj;  // une référence sur un objet  de type D
       obj.print();
       ptrC->print();
       ptrD->print();
       refC.print();
       refD.print();
       return 0;
    }
    donnera une sortie tout à fait différente, sous la forme de "je suis un C" pour ptrC et refC et "je suis un D" pour ptrD et refD.

    Ce qui se passe dans ce cas là, c'est que l'implémentation propre à la classe D de la fonction print ne fait que "cacher" l'implémentation propre à la classe C de cette fonction.

    Du coup, lorsque l'objet "passe pour êre" de type C, c'est le comportement de l'implémentation de C que l'on observe et, lorsque le type réel de l'objet est utilisé, on observe le comportement du type réel.

    Et peut etre vas tu commencer à voir où se situe le problème lorsque le destructeur n'est pas virtuel

    Allez, je suis bon prince, je vais te laisser le temps d'un pour y réfléchir

    Alors, ca y est Tu as trouvé

    Humm... Tu n'as pas l'air convaincu

    Bon. Imaginons que nous ayons deux classe, A et B, et que B hérite de A sous la forme 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
    15
    class A{
        public:
            /* on ne déclare pas le destructeur de A comme étant virtuel
             * Le compilateur lui donnera une implémentation publique,
             * non statique et non virtuelle!!!
             */
    };
    class B : public A{
        public:
            B(int size):size_(size), tab_(new int(size)){}
            ~B(){delete [] tab_}
        private:
            int size_;
            int * tab_;
    }
    Que se passerait il, selon toi, si on les utilisait sous une forme proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    int main(){
        A * temp = new B(15);
        /* ... */
        delete temp ;
        return 0;
    }
    AAAHHH, je sens que tu commences à comprendre:

    Comme temp est un pointeur sur A, c'est le destructeur (dont l'implémentation est fournie par le compilateur dans le cas présent) de A qui sera appelé, et non celui de B.

    Du coup, nous nous retrouverons avec un objet qui n'est que partiellement détruit, et donc, avec une belle fuite mémoire selon mon exemple

    Pour que ce code fonctionne correctement, il eut fallu que le destructeur de A soit virtuel.

    Maintenant, on peut envisager une autre solution.

    On peut parfaitement décider de ne pas rendre le destructeur de la classe de base virtuel, mais de le déclarer dans l'accessibilité protégée.

    Cela donnerait des classes proches de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    class A{
        protected:
            ~A(){}
    };
    class B : public A{
     
    };
    Comme le destructeur de A est protégé, la classe B, et surtout, le destructeur de B (qui hérite de A) est parfaitement en droit d'y accéder.

    Ce qui fait qu'un code proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    int main(){
        B obj;
        B * ptr = new B;
        delete ptr; // ca marche aussi bien avec un pointeur
    } // qu'avec une variable détruite lorsque l'on sort de la portée dans laquelle elle est déclarée
    fonctionnera sans problème.

    La seule restriction, c'est que les objets "passant pour être" de type A ne pourront plus être détruits par eux-même.

    Ainsi, avec ces deux mêmes classe A et B, le code
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    int main(){
        A * ptrA = new A;
        A * ptrB = new B;
        /*  ... */
        delete ptrA;
        delete ptrB;
        return 0;
    }
    ne compilera pas sous prétexte que "A::~A() est protégé dans ce contexte", aussi bien pour ptrA que pour ptrB.

    Evidemment, tu es surement occupé à te demander "mais à quoi ca sert de permettre cela s'il n'est pas possible de détruire l'objet de type A "

    Hé bien, la réponse tient dans le fait que C++ est un langage multiparadigme.

    Jusqu'à présent, nous avons parlé du paradigme orienté objets uniquement.

    Il est donc temps de parler d'un "mix" entre le paradigme orienté objets et le paradigme générique (les template).

    De par leur nature, les classes et les structures template sont "peu enclines" à accepter des fonctions virtuelles

    La raison en est toute simple:

    Une classe template est souvent définie sous la forme de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    template <typename T>
    class A{
        /* tout ce qu'il faut
    };
    ce qui fait que le compilateur ne pourra générer le code exécutable correspondant que... quand il saura quelle taille le type T prend en mémoire, c'est à dire: lorsque la classe A sera utilisée avec un type bien précis.

    Et il ne pourra générer le code binaire correspondant à chaque fois qu'en fonction du type particulier qui est utilisé.

    De plus, pour générer le code binaire correspondant aux différentes fonctions membre de la classe pour un type T particulier, il devra disposer du code source de la fonction.

    Oui, mais, voilà, pour que cela puisse marcher (je ne vais pas trop rentrer dans les détails ici ), il faut que la fonction soit "inline".

    Et c'est là que réside tout le problème : le mécanisme mis en place pour assurer la virtualité d'une fonction n'est, décidément, pas compatible avec l'inlining des fonctions (encore une fois, il faudrait trop rentrer dans les détails pour expliquer pourquoi )

    On ne peut donc pas déclarer le destructeur de A comme étant publique et virtuel.

    Par contre, on peut effectivement le déclarer comme étant protégé et non virtuel, et nous pourrions donc écrire un code (qui utilise le CRTP) proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    template <typename T>
    class A{
        protected:
            A(){} // après tout, si on ne peut pas détruire un A de manière directe, 
                  // autant ne pas pouvoir le créer non plus :D
            ~A(){}
    };
    class B : public A<B>{
       /* comme le constructeur et le destructeurs sont dans l'accessibilité 
        * protégée, ils reste accessible depuis les fonctions membres
        * (essentiellement le constructeur et le destructeur) de B ;)
        */
    };
    Alors, bien sur, vous me demanderez sans doute quel est l'avantage de travailler de la sorte.

    L'avantage est tout simple: la classe A va agir comme une interface "améliorée": nous pourrons définir "tous les comportements relatifs" au "concept" représenté par la classe A et faire en sorte qu'ils soient d'office adapté au type de la classe dérivée, et ce, sans que l'héritage (multiple ) n'occasionne forcément des hiérarchies de classes trop importante (pour rappel : bien que A<truc> et A<machin> dispose de fonctions identique, il n'y a strictement aucun lien entre les deux, et ce, meme si machin hérite de truc )
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

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

Discussions similaires

  1. Problème à l'affichage quand j'utilise SDL
    Par vincechaff10 dans le forum SDL
    Réponses: 8
    Dernier message: 25/07/2006, 11h34
  2. fonctions et classes... quand les utiliser ?
    Par fastmanu dans le forum Langage
    Réponses: 6
    Dernier message: 03/04/2006, 00h39
  3. [Struts-Layout] exception quand j'utilise <layout:submit&
    Par jahjah42 dans le forum Struts 1
    Réponses: 2
    Dernier message: 29/11/2005, 11h17
  4. "sauter" un champ quand on utilise la tabulation
    Par jisse dans le forum Balisage (X)HTML et validation W3C
    Réponses: 1
    Dernier message: 18/09/2005, 01h42
  5. [héritage privé] appel du constructeur de base
    Par PINGOUIN_GEANT dans le forum C++
    Réponses: 4
    Dernier message: 19/10/2004, 14h05

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