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

Langage C++ Discussion :

Héritage public et copie/déplacement/échange


Sujet :

Langage C++

Vue hybride

Message précédent Message précédent   Message suivant Message suivant
  1. #1
    Membre émérite Avatar de Steph_ng8
    Homme Profil pro
    Doctorant en Informatique
    Inscrit en
    Septembre 2010
    Messages
    677
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 40
    Localisation : France

    Informations professionnelles :
    Activité : Doctorant en Informatique

    Informations forums :
    Inscription : Septembre 2010
    Messages : 677
    Par défaut Héritage public et copie/déplacement/échange
    Bonjour.

    Ce sujet fait suite à un échange lancé par cette question, qui n'avait plus rien à voir avec le sujet d'origine...
    Je crée donc un topic séparé.

    Donc, on dispose d'une hiérarchie de classes en héritage public.
    Elles ont très certainement une sémantique d'entité, et donc ne sont ni copiables (ne possèdent pas de constructeur par copie (accessible)) ni assignable (ne possèdent pas d'opérateur d'affectation (par copie) (accessible)).

    Je ne voyais pas vraiment pourquoi jusqu'à cette remarque, qui pointe un problème de cohérence.
    Sans doute parce que je considérais qu'une classe doit être pleinement responsable des attributs membre qu'elle définit.
    Peut-être une vision un peu trop stricte...
    Bref.


    Si l'on veut tout de même copier un objet d'une telle hiérarchie, on peut utiliser le design pattern « clone », dixit la F.A.Q..
    Mais cela implique que les constructeurs par copie existent, bien que protégés ou privés.
    Dans ce cas, à quoi faut-il faire attention lors de l'écriture de ces constructeurs, pour être sûr de rester correct et cohérent ?
    Est-ce que les constructeurs par copie « par défaut » peuvent convenir ?

    Maintenant que l'on a vu la copie, la question de l'assignation se pose.
    Est-ce incorrect de se dire : « Je ne veux plus que cette variable représente cet objet, dorénavant elle représentera cet autre objet. » ?
    Si la variable en question est un pointeur, un delete suivi d'une affectation au résultat de clone() sur l'objet adéquat fait l'affaire.
    Mais sinon ?
    Peut-on envisager une fonction membre clone() qui prendrait en paramètre l'adresse à laquelle dupliquer l'objet, ou une référence sur l'objet cible, et qui utiliserait l'opérateur d'affectation ?
    Si oui, même question que précédemment.


    Voyons à présent le déplacement.
    Il n'y a pas de raison qu'une classe non copiable ne supporte pas le déplacement, n'est-ce pas ?
    J'imagine que l'on pourrait se contenter du constructeur par déplacement et de l'opérateur d'affection par déplacement par défaut, mais on risque de se retrouver confronté au problème de cohérence indiqué par la remarque citée plus haut.
    Alors comment faut-il faire ?


    Passons maintenant à l'échange.
    Ce n'est pas parce qu'une classe n'est pas copiable qu'elle n'est pas échangeable, si ?
    L'implémentation « par défaut » de l'échange passe par le constructeur par copie et l'opérateur d'affectation.
    Dommage...
    Pour le C++11, c'est le constructeur par déplacement et l'opérateur d'affectation par déplacement.
    Dommage également...

    Du coup, je me pose les mêmes question que précédemment...
    Surtout qu'il y a deux cas à gérer : si on utilise l'idiome « copy and swap » pour l'opérateur d'affectation ou non...


    Voilà.
    J'espère que vous pourrez m'aider à y voir un peu plus clair.

  2. #2
    Membre Expert

    Homme Profil pro
    Développeur informatique
    Inscrit en
    Septembre 2007
    Messages
    1 895
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 49
    Localisation : France, Bouches du Rhône (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : High Tech - Opérateur de télécommunications

    Informations forums :
    Inscription : Septembre 2007
    Messages : 1 895
    Par défaut
    Tout ce qui suit n'est que mon opinion, et j'espère que la discussion qui va suivre va s'avérer tout aussi intéressante que la question.

    Situons nous, histoire d'essayer d'y voir plus clair, dans une hiérarchie de classes A->B->C qui ont une sémantique de valeur - elles sont donc copiables et assignables (-> signifie "est la classe mère de").

    Construction par copie

    Dans ce cas, la question est de savoir si les relations suivantes ont du sens :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
     
    /* - */ A a1;
    /* 1 */ B b1(a1);
    /* 2 */ C c1(b1);
    /* 3 */ C c2(a1);
    /* - */ C c3;
    /* - */ C c4;
    /* - */ B b2;
    /* 4 */ B b3(c3);
    /* 5 */ A a2(c4);
    /* 6 */ A a3(b2);
    S'il y a relation d'héritage, alors le principe de substitution est censé être respecté : c1 et c2 se comportent comme une instance de A vis à vis des clients de A (pour le dire autrement, tout utilisateur d'une instance de A peut utiliser à la place une instance de C sans se rendre compte du fait qu'il utilise une instance de C). De la fonction :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
     
    A *clone(A* instance)
    {
      return new A(*instance);
    }
    Il ressort que les relations 4, 5 et 6 ci-dessus sont parfaitement valides - et même nécessaire, si on souhaite que le principe de substitution de Liskov soit respecté. Le constructeur par copie de A se comporte à son tour comme un client de A vis à vis du LSP, donc il faut que le comportement des instances de type B et C ne diffèrent pas du comportement d'une instance de A dans ce cadre.

    Quid des relations 1, 2, et 3 ? Le code suivant
    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
     
    #include <iostream>
     
    #define PRINT do { std::cout << __PRETTY_FUNCTION__ << std::endl; } while (0)
     
    class A
    {
    public:
    	A() { PRINT; }
    	A(const A&) { PRINT; }
    	virtual ~A() { }
    	A& operator=(const A&) { PRINT; return *this; }
    };
     
    class B : public A
    {
    public:
    	B() { PRINT; }
    	~B() { }
    };
     
    int main()
    {
    	A a1;
    	A a2(a1);
    	B b1;
    	B b2(b1);
    	B b3(a2);
    }
    Ne compile pas :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
     
    g++     cctor.cpp   -o cctor
    cctor.cpp: In function ‘int main()’:
    cctor.cpp:27:9: error: no matching function for call to ‘B::B(A&)’
    cctor.cpp:27:9: note: candidates are:
    cctor.cpp:17:2: note: B::B()
    cctor.cpp:17:2: note:   candidate expects 0 arguments, 1 provided
    cctor.cpp:14:7: note: B::B(const B&)
    cctor.cpp:14:7: note:   no known conversion for argument 1 from ‘A’ to ‘const B&’
    Il y a une bonne raison à ça : le constructeur par copie implicite et public qui est créé pour une class B par le compilateur de manière automatique a pour signature B::B(const B&), et la conversion A& vers B& est impossible (même si A& est en fait une référence vers une instance de B ; dans ce cas, il faut un dynamic_cast<> pour récupérer l'instance de B).

    A noter que supprimer la déclaration explicite du constructeur par copie de A ne change rien à l'affaire : le constructeur par copie implicite de B continuera de prendre en paramètre un const B&.

    Donc la réponse à la question revient à se demander si c'est une bonne chose de créer un constructeur de B qui prendrait en paramètre une référence constante sur une instance de A - en gros, créer de manière explicite un constructeur B::B(const A&).

    Une telle approche peut être tout a fait légitime dans certains cas (on peut l'imaginer assez aisément lors d'une utilisation du CRTP). En fait, elle est valide dès lors que B modifie le comportement de la classe A dont elle hérite sans changer ses propriétés intrinsèques (B est un A avec des algorithmes différents). Il ne faut pas se leurrer : de tels cas sont peu courants. Là où on se rends compte qu'une telle approche n'est pas la bienvenue, c'est lorsqu'on a une troisième classe C qui hérite de B : dès lors que C doit contenir son constructeur par copie explicite, ainsi que un constructeur par classe parent, alors il est plus que probable qu'il y a un problème de respect du LSP ou de OCP.

    Assignation

    Au niveau de l'assignation, on a strictement le même raisonnement, avec exactement les même limites.

    Déplacement

    On amende le code ci-dessus pour obtenir :

    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
     
    #include <iostream>
     
    #define PRINT do { std::cout << __PRETTY_FUNCTION__ << std::endl; } while (0)
     
    class A
    {
    public:
    	A() { PRINT; }
    	A(const A&) { PRINT; }
    	A(A&&) { PRINT; }
    	virtual ~A() { }
    	A& operator=(const A&) { PRINT; return *this; }
    };
     
    class B : public A
    {
    public:
    	B() { PRINT; }
    	~B() { }
    };
     
    int main()
    {
    	A a3(std::move(B()));
    }
    Le compilateur permet bien évidemment d'écrire ce code - il est tout a fait valide de déplacer une instance de B dans une instance de A, parce que une instance de B est une instance de A.

    Un problème survient quand même, car on perds les comportements spécifique de l'instance de B en faisant ça. Ca n'est pas particulièrement choquant, mais on doit le garder en mémoire lorsqu'on effectue cette opération très particulière.

    Maintenant, déplacer une instance de A dans une instance de B a-t-il du sens ? Instinctivement, je dirais non, parce qu'il semblerait que ce déplacement n'en soit plus un, car il s'agirait là d'une transformation.

    Mais c'est sans compter le raisonnement fait ci-dessus : si construire une instance de B à partir d'une instance de A a du sens, alors le déplacement d'une instance de A dans une instance de B a tout autant de sens.

    Echange
    L'échange étant défini par l'autorisation (ou non) de l'affectation, dès lors que les opérateurs d'affectation sont correctement définis alors l'échange est correctement défini. Ceci dit, le prototype canonique de la fonction swap est (pour simplifier ; ce n'est pas tout a fait exact (c'est même beaucoup plus compliqué en fait)):

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
     
    namespace std
    {
        template <class _Type>
            void swap(_Type& instance1, _Type& instance2);
    }
    Et non pas
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
     
    namespace std
    {
        template <class _Type1, class _Type2>
            void swap(_Type1& instance1, _Type2& instance2);
    }
    Qui serait nécessaire pour implémenter l'échange entre une instance de A et une instance de B (même si B hérite de A et que tous les opérateurs sont là). Les concepts auraient probablement permis de se passer de cette limitation, mais les concepts ont été abandonnés en route.

    Conclusion

    Dans la hiérarchie A->B, les relations suivantes sont trivialement acceptables :
    • copier un B dans une nouvelle instance de A
    • assigner un B dans une instance de A
    • déplacer un B dans une instance de A

    Les relations suivantes ne sont acceptables que dans le cadre d'une architecture particulière, avec condition
    • copier un A dans une nouvelle instance de B
    • assigner un A dans une instance de B
    • déplacer un A dans une instance de B

    Enfin, la relation suivante pose des problèmes d'implémentation, mais est acceptable aux mêmes conditions que les 3 relations précédentes:
    • échanger une instance de A et une instance de B
    [FAQ des forums][FAQ Développement 2D, 3D et Jeux][Si vous ne savez pas ou vous en êtes...]
    Essayez d'écrire clairement (c'est à dire avec des mots français complets). SMS est votre ennemi.
    Evitez les arguments inutiles - DirectMachin vs. OpenTruc ou G++ vs. Café. C'est dépassé tout ça.
    Et si vous êtes sages, vous aurez peut être vous aussi la chance de passer à la télé. Ou pas.

    Ce site contient un forum d'entraide gratuit. Il ne s'use que si l'on ne s'en sert pas.

  3. #3
    Membre émérite Avatar de Steph_ng8
    Homme Profil pro
    Doctorant en Informatique
    Inscrit en
    Septembre 2010
    Messages
    677
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 40
    Localisation : France

    Informations professionnelles :
    Activité : Doctorant en Informatique

    Informations forums :
    Inscription : Septembre 2010
    Messages : 677
    Par défaut
    Citation Envoyé par Emmanuel Deloget Voir le message
    Tout ce qui suit n'est que mon opinion, et j'espère que la discussion qui va suivre va s'avérer tout aussi intéressante que la question.
    C'est bien pour cela que j'ai créé cette discussion...


    Citation Envoyé par Emmanuel Deloget Voir le message
    Dans la hiérarchie A->B, les relations suivantes sont trivialement acceptables :
    • copier un B dans une nouvelle instance de A
    • assigner un B dans une instance de A
    • déplacer un B dans une instance de A
    C'est exactement ce que je pensais jusqu'à la remarque de jblecanard (citée dans mon premier message).
    Mais qu'entends-tu exactement par « trivialement acceptables » ?
    Qu'elles sont correctes syntaxiquement et sémantiquement, mais que ce n'est pas forcément une bonne idée de les utiliser ?


    Citation Envoyé par Emmanuel Deloget Voir le message
    Les relations suivantes ne sont acceptables que dans le cadre d'une architecture particulière, avec condition
    • copier un A dans une nouvelle instance de B
    • assigner un A dans une instance de B
    • déplacer un A dans une instance de B
    Je dois dire que cela ne m'avait jamais traversé l'esprit.
    Elles me paraissent tellement absurdes...


    Citation Envoyé par Emmanuel Deloget Voir le message
    Enfin, la relation suivante pose des problèmes d'implémentation, mais est acceptable aux mêmes conditions que les 3 relations précédentes:
    • échanger une instance de A et une instance de B
    Pour ce cas-là, il faudra transtyper explicitement l'instance de B en une instance de A, puisque comme tu le fais justement remarquer, la fonction swap() par défaut n'accepte que deux arguments du même type.
    Et dans ce cas, le programmeur ne peut pas échanger une instance de A et une instance de B sans s'en rendre compte ; il doit savoir ce qu'il fait.
    Par contre, si la fonction swap() a été redéfinie pour le type A, là ça peut poser problème.

    Je dois dire que là aussi, je n'ai jamais pensé à échanger deux instances de types différents mais liés hiérarchiquement.
    Quoiqu'il suffit de travailler avec des références ou des pointeurs sur les types de bases pour masquer cet état de fait.
    Du coup, je suppose que ça annule ma première remarque...


    Au final, quel est ton point de vue Emmanuel ?
    Que l'on peut utiliser une hiérarchie de classes à sémantique de valeur, à condition de prendre des précautions, ou qu'on ne peut pas (devrait pas ?) le faire, car cela viole le principe de substitution de Liskov ?

  4. #4
    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,
    Citation Envoyé par Steph_ng8 Voir le message
    C'est exactement ce que je pensais jusqu'à la remarque de jblecanard (citée dans mon premier message).
    Mais qu'entends-tu exactement par « trivialement acceptables » ?
    Qu'elles sont correctes syntaxiquement et sémantiquement, mais que ce n'est pas forcément une bonne idée de les utiliser ?
    Emmanuel veut simplement dire que l'on peut accepter l'idée de ces opérations sans se poser de question (le terme trivial indique que c'est quelque chose qui peut survenir de manière habituelle )

    En effet, étant donné que tout B est de toutes manières un A, copier, assigner ou déplacer un B vers un A se fera sans se poser de question, tout en gardant en mémoire que certaines informations sont perdues: on ne fait que prendre un sous ensemble de ce qui compose le B
    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

  5. #5
    Membre émérite Avatar de Steph_ng8
    Homme Profil pro
    Doctorant en Informatique
    Inscrit en
    Septembre 2010
    Messages
    677
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 40
    Localisation : France

    Informations professionnelles :
    Activité : Doctorant en Informatique

    Informations forums :
    Inscription : Septembre 2010
    Messages : 677
    Par défaut
    Ok, merci.
    C'est ce que je me disais, mais je voulais m'en assurer.

    Finalement, cela me conforte dans l'idée que l'on peut (du moins en théorie) avoir une hiérarchie de classes avec héritage public ayant une sémantique de valeur.
    (Ou alors, c'est que je n'ai pas compris là où il voulait en venir... )
    Ça n'a pas l'air de te choquer plus que ça, koala01...
    Je me fais des idées ?

  6. #6
    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
    Le fait est que je suis d'accord avec l'analyse théoriqued'Emmanuel, mais que je ne vois absolument pas dans quelle circonstance nous pourrions avoir une hiérarchie de classe ayant sémantique de valeur!

    Ma réaction est donc proche du
    En théorie, cela devrait pouvoir se faire sous condition, mais, en pratique, j'attends encore que l'on me présente un exemple concret de hiérarchie de classses ayant sémantique de valeur
    Je ne dis pas qu'il est impossible d'apporter un tel exemple, je dis juste que ce genre d'exemple doit se compter sur les doigts d'une main en regroupant l'ensemble des projets existant

    J'ai d'autant plus de mal à imaginer une telle hiérarchie que l'analyse d'Emmanuel montre qu'il y a quand même un grand nombre d'écueils à éviter.
    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

Discussions similaires

  1. [Débutant] Copie, déplacement et suppression d'un fichier en c#
    Par garfieldlcht dans le forum C#
    Réponses: 13
    Dernier message: 18/06/2015, 12h31
  2. Réponses: 5
    Dernier message: 03/12/2006, 15h55
  3. Réponses: 9
    Dernier message: 09/11/2006, 10h10
  4. Réponses: 8
    Dernier message: 10/09/2005, 20h12

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