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 sans virtualité ?


Sujet :

Langage C++

  1. #1
    Membre habitué
    Homme Profil pro
    Doctorant en Astrophysique
    Inscrit en
    Mars 2009
    Messages
    312
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Doctorant en Astrophysique
    Secteur : Enseignement

    Informations forums :
    Inscription : Mars 2009
    Messages : 312
    Points : 176
    Points
    176
    Par défaut Héritage sans virtualité ?
    Bonjour à tous.

    Je me pose actuellement la question de savoir si ce que je suis en train de faire est ok du point de vue du C++, ou si c'est un blasphème qui mérite les pires des chatiments.

    Le code sur lequel je travaille comprend déjà pas mal de virtualité, mais j'ai besoin d'objets de bases que je souhaiterai être assez efficace du point de vue du calcul pur (et donc éviter l'overhead du à la virtualité pourrait être pas mal).

    Le schéma très simple serait le suivant :

    ClasseBase
    -> ClasseA
    -> ClasseB
    -> ClasseC

    Je veux juste utiliser l'héritage pour éviter des copier-coller énormes et pas pratiques parce que le fichier de la classe de base doit peser un truc comme 60Ko. Donc je prévois une ClasseBase, sans virtualité et sans destructeur virtuel.

    Bien entendu, dans le code, ClasseA, ClasseB et ClasseC seront considérées comme des classes "indépendantes".

    La question est donc la suivante : ce genre de choses est-il OK du point de vue du C++, ou cela n'est pas clean , voire dangereux et peut mener à des désastres (et si oui lesquels).

    Merci beaucoup

  2. #2
    Membre émérite
    Profil pro
    Inscrit en
    Novembre 2004
    Messages
    2 764
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Novembre 2004
    Messages : 2 764
    Points : 2 705
    Points
    2 705
    Par défaut
    La virtualité est une option, pas une obligation. Cela dépend de tes besoins (besoin de polymorphisme ?).

    Si c'est uniquement pour factoriser des fonctionnalités, tu peux aussi utiliser la composition. Et si tu choisis de l'héritage, envisager l'héritage privé.

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

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 614
    Points : 30 626
    Points
    30 626
    Par défaut
    Salut,

    La virtualité a beau être une option, elle n'en est pas moins indispensable du point de vue orienté objet dés que l'on parle d'héritage.

    En effet, le fondement même de la conception orientée objets est la substituabilité : le fait que l'on puisse considérer un objet d'un type particulier (par exemple de type Voiture) comme étant du type de la classe de base (par exemple : véhicule) tout en gardant les comportements (du moins, ceux qui existent dans la classe de base) adaptés au type réellement manipulé.

    Retirer la virtualité du concept orienté objets, reviens à retirer le moteur ou les roues d'une voiture : on obtient quelque chose qui n'est plus en état de fournir les services que l'on en attend de manière correcte

    En outre, il faut être conscient que l'héritage est quand meme la relation la plus forte qui puisse exister entre deux objets car c'est une relation EST-UN, au sens sémantique du terme (on peut décemment dire qu'une voiture EST, du point de vue sémantique, UN véhicule ).

    Il faut donc veiller à respecter strictement le Principe de Substitution de Liskov (Liskov Subtsitution Principle en anglais, ou LSP pour les intimes ) et à recourir à l'héritage uniquement quand LSP est strictement respecté.

    Si tu ne peux pas décemment estimer que tes objets de type classeA, classeB ou classeC sont des objets de type de ClasseBase du point de vue de la sémantique, tu ne dois, en vertu de LSP, purement et simplement pas envisager l'héritage

    Enfin, il s'agit de faire attention à la "granularité" de tes classes de base, car, si tu remontes suffisamment haut dans la hiérarchie de classes, tu pourrais presque estimer que la classe Voiture et la classe flûte peuvent avoir une classe de base "Objet". (c'est d'ailleurs le principe sur lequel se basent d'autres langages )

    Seulement, les points communs entre une flute et une voiture sont tellement limités qu'une telle classe n'offre aucun avantage et, au contraire, place un certain nombre de restrictions sur ce qui peut etre fait par la suite

    Outre le respect de LSP, l'héritage devrait n'être envisagé que si tu prévois effectivement de placer des objet de différents types dérivés dans une seule et meme collection d'objets (par exemple : de placer des objets dont le type est classeA, classeB ou classeC dans un tableau de pointeurs de type ClasseBase)

    Bien souvent, l'héritage peut etre remplacer par une composition, et il doit l'être si LSP n'est pas respecté

    Enfin, il faut savoir que C++ est multi paradigme et que, outre le paradigme impératif et le paradigme orienté objets, il offre un paradigme qui a de grandes chances de t'intéresser également : le paradigme générique.

    En effet, l'idée du paradigme orienté objets est de ne plus réfléchir en terme de données mais en terme de services attendus de la part des objets que l'on va manipuler.

    Le paradigme générique va plus loin, et l'idée qui le sous tend est de se dire que : si je ne sais pas encore quel est le type de la donnée que je vais manipuler, je sais, en revanche, comment je vais la manipuler".

    Tu peux alors réfléchir en termes de "fonctionnalités transversales", ou en termes de "concepts" et envisager d'implémenter de manière générique ces différentes fonctionnalités ou concepts pour, par la suite, décider que classeA, classeB et / ou classeC utilisent ces fonctionnalités, ou ces concepts

    Il faut "simplement" que tes différentes classes présentent une interface commune permettant à ces fonctionnalités de faire leur travail.

    Mais l'interface qui devient nécessaire peut se limiter à des comportement simples tels que des mutateurs ou des accesseurs "basiques"

    L'énorme avantage, c'est que les concepts que tu implémentes déterminent le type réellement manipulé au moment de la compilation, et ne nécessitent donc pas la virtualité et que, de plus, tes classes restent indépendantes des autres car UneClasse<Type1> n'a aucune relation (hormis le fait qu'elle ait la meme interface) que UneClasse<Type2>

    PS : l'héritage privé n'est pas une relation de type EST-UN comme l'est l'héritage public : c'est en fait une relation de type EST-IMPLEMENTE-SOUS-LA-FORME-DE, et ne crée aucune relation entre les différentes classes qui héritent de manière privée d'une même classe de base (on ne peut pas placer les classes dérivées dans un tableau de pointeurs sur la classe de base cfr l'entrée de la FAQ sur le sujet)
    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

  4. #4
    Membre émérite
    Avatar de white_tentacle
    Profil pro
    Inscrit en
    Novembre 2008
    Messages
    1 505
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Novembre 2008
    Messages : 1 505
    Points : 2 799
    Points
    2 799
    Par défaut
    Pour faire plus court que Koala, non, ce n’est pas un problème, tant que tu n’utilises jamais de pointeur vers la classe de base pour détruire des instances de tes classes filles (ce qui conduirait sinon à des fuites mémoire).

    L’héritage privé ou protégé, qui ont entre autres comme effet d’empêcher cette utilisation erronée, sont donc à envisager très fortement dans ce cas.

    Autre solution, comme le suggère Koala, passer par la généricité si le but est de factoriser du code. Cela peut, suivant les cas, améliorer (pas de coût de l’appel virtuel) ou diminuer (augmentation de taille du code et donc saturation des cache) les perfs, c’est donc à mesurer si c’est critique pour toi.

  5. #5
    Membre émérite
    Profil pro
    Inscrit en
    Novembre 2004
    Messages
    2 764
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Novembre 2004
    Messages : 2 764
    Points : 2 705
    Points
    2 705
    Par défaut
    Citation Envoyé par koala01 Voir le message
    La virtualité a beau être une option, elle n'en est pas moins indispensable du point de vue orienté objet dés que l'on parle d'héritage.
    Dès que l'on parle d'héritage public.

  6. #6
    Membre habitué
    Homme Profil pro
    Doctorant en Astrophysique
    Inscrit en
    Mars 2009
    Messages
    312
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Doctorant en Astrophysique
    Secteur : Enseignement

    Informations forums :
    Inscription : Mars 2009
    Messages : 312
    Points : 176
    Points
    176
    Par défaut
    Merci beaucoup pour vos réponses, je vois comme je m'y attendais, que c'est un sujet sensible. Pour les concepts, ils ne sont pas dans la norme C++ 2011 si je me trompe pas...

    En fait voici mon problème :
    Dans mon programme, j'ai besoin de vecteurs/matrices/tenseurs 3 et 4D de petite taille fixe (pour représenter des dimensions physiques). Ca me saoulait depuis quelques temps déjà de ne pas trouver mon bonheur dans quelques librairies dispo (les trucs types BLAS/ATLAS sont surtout optimisées pour les matrices de grande taille), donc j'ai pris le problème à bras le corps et j'ai codé mes propres trucs en une semaine. Les perfs sont plutôt bonne puisque c'est meilleur que des valarray. Bref ça me satisfait entièrement comme ça.

    Du coup je m'étais dis la chose suivante :
    Je vais coder une classe MathArray qui va implémenter tous les opérateurs qui vont travailler sur chacun des éléments de façon indépendante (genre pas une multiplication de matrices) et de cette classe je vais dériver MathVector, MathMatrix etc... qui ajouteront quelques opérateurs spécifiques.

    La question est simple : comment faire cela proprement par héritage (pas par composition) ? Les MathVector, MathMatrix SONT des tableaux MathArray. A part si quelqu'un a une solution "immédiatement" implémentable, je pense que je vais garder pour l'instant mon héritage public sans virtualité (avec un héritage privé, mes vector et matrix hériteraient des +, - etc.. en privé ce qui me bloquerait un peu non ?).

    J'ai regardé du côté du CRTP qui à mon avis serait la bonne façons, mais pour certains cas c'est un peu prise de tête.

    Si certains d'entre vous ont des avis sur la question.

    Merci beaucoup

    EDIT : je viens de penser à un truc tordu. J'utilise le CRTP à certains autres endroits de mon code pour 2 ou 3 fonctions de classes "abstraites" (virtualité "statique" pour le coup), mais là pour bien réécrire toutes les fonctions et opérateurs surchargés de ma classe de base, ce serait une très grosse prise de tête. Du coup, je me disais : pourquoi ne pas simplement CRTP-isé le destructeur ?

    Un truc genre:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    template<class TCRTP, class T, unsigned int SIZE> class MathArray
    {
        ~MathArray() {delete static_cast<TCRTP*>(this);}
    }
    Ca marcherait un truc comme ça (TCRTP est un type dérivé par exemple "MathVector") ?

  7. #7
    Membre émérite
    Profil pro
    Inscrit en
    Novembre 2004
    Messages
    2 764
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Novembre 2004
    Messages : 2 764
    Points : 2 705
    Points
    2 705
    Par défaut
    Citation Envoyé par Kaluza Voir le message
    Les MathVector, MathMatrix SONT des tableaux MathArray.
    On peut également dire que les MathVector sont des MathMatrix...

    Réfléchis à ce que ça implique. :-)

  8. #8
    Membre émérite
    Avatar de white_tentacle
    Profil pro
    Inscrit en
    Novembre 2008
    Messages
    1 505
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Novembre 2008
    Messages : 1 505
    Points : 2 799
    Points
    2 799
    Par défaut
    À la va vite, je dirai que oui, le CRTP est une bonne solution, en tout cas à creuser. Si tu as des soucis avec, peut-être peux-tu les détailler ici, il y a des cas un peu tordus qui peuvent nécessiter de passer par une classe de traits par exemple (j’ai déjà eu le besoin, notamment pour des histoires de taille de tableau).

  9. #9
    Rédacteur
    Avatar de 3DArchi
    Profil pro
    Inscrit en
    Juin 2008
    Messages
    7 634
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Juin 2008
    Messages : 7 634
    Points : 13 017
    Points
    13 017
    Par défaut
    Citation Envoyé par Kaluza Voir le message
    Ca marcherait un truc comme ça?
    stackoverflow (le destructeur de base appelle explicitement le destructeur de derivée qui automatiquement appel le destructeur de base qui appelle explicitement le destructeur de derivée qui automatiquement appel le destructeur de base qui appelle explicitement le destructeur de derivée qui automatiquement appel le destructeur de base qui appelle explicitement le destructeur de derivée qui automatiquement appel le destructeur de base qui appelle explicitement le destructeur de derivée qui automatiquement appel le destructeur de base qui appelle explicitement le destructeur de derivée qui automatiquement appel le destructeur de base qui appelle explicitement....)

    Tu aurais pu envisager une fonction tierce :
    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
    template<class T>
    struct base
    {
       ~base()
       {
          static_cast<T&>(*this).destroy_it();
       }
    };
     
    struct derived : public base<derived>
    {
       ~derived()
       {destroy_it();}
     
    private:
       friend struct base<derived>;
       void destroy_it()
       {}
    };
    ce qui en soit est souvent pas terrible, mais :

    De base, l'appel d'un destructeur non virtuel sur un pointeur dont le type dynamique est différent du type statique provoque un comportement indéterminé.

    Plutôt rendre protégé les constructeurs et destructeurs de la classe MathArray pour qu'on ne puisse créer/supprimer un objet vecteur ou matrice à partir de cette classe.

    Ceci dit, rien n'empêche d'avoir un héritage privé et d'utiliser ensuite using pour re 'publier' les fonctions qui t'intéressent:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    struct base
    {
       int foo()const;
    private:
       base()=default;
       ~base()=default;
    };
     
    struct derived : private base
    {
       using base::foo;
    };
    Enfin, j'aurais plutôt vu les opérateurs et autres fonctions de manipulation à l'extérieur des classes de représentation des matrices/vecteurs/tenseurs plutôt que comme membres de la classe.

  10. #10
    Modérateur
    Avatar de Obsidian
    Homme Profil pro
    Développeur en systèmes embarqués
    Inscrit en
    Septembre 2007
    Messages
    7 369
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 47
    Localisation : France, Essonne (Île de France)

    Informations professionnelles :
    Activité : Développeur en systèmes embarqués
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Septembre 2007
    Messages : 7 369
    Points : 23 623
    Points
    23 623
    Par défaut
    Hello,

    Citation Envoyé par koala01 Voir le message
    La virtualité a beau être une option, elle n'en est pas moins indispensable du point de vue orienté objet dés que l'on parle d'héritage.
    Ben justement, moi je ne trouve pas. Au contraire, je trouve que c'est une des forces du C++ de faire clairement le distingo entre les deux et de permettre d'exprimer une approche objet d'un programme en limitant au minimum les contraintes résolues au runtime.

    En effet, le fondement même de la conception orientée objets est la substituabilité : le fait que l'on puisse considérer un objet d'un type particulier (par exemple de type Voiture) comme étant du type de la classe de base (par exemple : véhicule) tout en gardant les comportements (du moins, ceux qui existent dans la classe de base) adaptés au type réellement manipulé.
    Pas nécessairement.

    Retirer la virtualité du concept orienté objets, reviens à retirer le moteur ou les roues d'une voiture : on obtient quelque chose qui n'est plus en état de fournir les services que l'on en attend de manière correcte
    Ben non. Si ton véhicule est réputé avoir des roues, le fait de le spécialiser en voiture n'implique pas forcément que tu vas modifier ses roues. Et étendre la classe ne supprime en rien l'existant.

    En outre, il faut être conscient que l'héritage est quand meme la relation la plus forte qui puisse exister entre deux objets car c'est une relation EST-UN, au sens sémantique du terme (on peut décemment dire qu'une voiture EST, du point de vue sémantique, UN véhicule ).

    Il faut donc veiller à respecter strictement le Principe de Substitution de Liskov (Liskov Subtsitution Principle en anglais, ou LSP pour les intimes ) et à recourir à l'héritage uniquement quand LSP est strictement respecté.
    Eh bien, pour moi, c'est justement le LSP qui va faire que la virtualité doit être forcément optionnelle et, dans ce cas, explicite (même si Java adopte l'approche inverse).

    Le LSP est le principe qui impose que toute propriété qui est vraie pour tout objet de type A l'est forcément aussi pour tout objet d'un type B sous-type de A. Le principe de substitution dit clairement qu'on doit pouvoir remplacer un A par un B de façon complètement transparente. C'est particulièrement important lorsque les routines qui reçoivent en arguments des objets d'un type donné n'ont pas connaissance de l'existence de sous-classe de ce type. Et justement, en virtualisant des fonctions-membres, on brise cette transparence.

    Ce n'est pas un détail parce que virtualiser des fonctions rend non déterministes des choses qui l'étaient pourtant au départ. Prenons l'exemple classique : une classe « forme » que je dérive en « cercle », « carré », « triangle », etc.

    Code C++ : 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
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    #include <string>
    #include <iostream>
     
    using namespace std;
     
    class Forme
    {
        public:
            virtual string QueSuisJe ();
    };
     
    class Cercle : public Forme
    {
        public:
            virtual string QueSuisJe ();
    };
     
    class Carre : public Forme
    {
        public:
            virtual string QueSuisJe ();
    };
     
    class Triangle : public Forme
    {
        public:
            virtual string QueSuisJe ();
    };
     
     
    string Forme::QueSuisJe ()
    {
        return "Forme";
    }
     
    string Cercle::QueSuisJe ()
    {
        return "Cercle";
    }
     
    string Carre::QueSuisJe ()
    {
        return "Carré";
    }
     
    string Triangle::QueSuisJe ()
    {
        return "Triangle";
    }
     
    int main (void)
    {
        unsigned int i;
        Forme *  tableau [4];
        Forme    fo;
        Cercle   ce;
        Carre    ca;
        Triangle tr;
     
        tableau[0] = &fo;
        tableau[1] = &ce;
        tableau[2] = &ca;
        tableau[3] = &tr;
     
        for (i=0;i<sizeof tableau / sizeof (*tableau);++i)
        cout << "Position " << i << ", je suis un(e) " << tableau[i]->QueSuisJe() << endl;
     
        return 0;
    }

    Code shell : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    $ ./programme
    Position 0, je suis un(e) Forme
    Position 1, je suis un(e) Cercle
    Position 2, je suis un(e) Carré
    Position 3, je suis un(e) Triangle
    $ _

    On pourrait déjà objecter que c'est le mauvais exemple parce que « Cercle », « Carré » et « Triangle » sont plus restrictives que « Forme » en général qui peut être n'importe quoi, mais c'est une question de définition. Il s'agit de savoir si la classe Forme peut définir à elle seule n'importe quelle forme (avec une liste de points, par exemple), ou si elle ne sert qu'à indiquer qu'il s'agit d'une forme, sans plus de détails. Mais ce n'est pas ce qui nous intéresse.

    Maintenant, imaginons que je compte allouer de la place pour stocker les chaînes renvoyées par les objets de mon tableau. Sans virtualité, je sais qu'ils sont tous les mêmes, et en plus je connais cette chaîne à l'avance. J'ai donc tout-à-fait le droit d'écrire par exemple :

    Code C++ : Sélectionner tout - Visualiser dans une fenêtre à part
        ptr = new char[tableau[0]->QueSuisJe().length() * sizeof tableau / (sizeof *tableau)];

    Par contre, je ne peux absolument le garantir à l'avance avec des fonctions virtuelles. Ça a même toutes les chances de planter puisque j'ai volontairement choisi le nom « Forme » plutôt que « Polygone » ou autre pour qu'il soit le plus court de tout les noms définis ici et que les autres provoquent systématiquement un dépassement de tableau. :-)

    Pire encore : si j'admets ce qui vient d'être dit et que je ne m'en tiens qu'à un seul élément du tableau, si :
    1. Je mesure la longueur de la chaîne renvoyée par l'élément tableau[0] ;
    2. j'alloue l'espace correspondant ;
    3. Je trie mon tableau ;
    4. Je stocke la chaîne que me renvoie mon élément [0] dans l'espace alloué


    Alors le programme risquera de planter aussi puisque la chaîne renvoyée ne sera plus la même. Moralité : non seulement je ne peux pas systématiquement substituer une classe virtualisée par une classe dérivée à la compilation de façon déterministe, mais je ne peux pas non plus, même à l'exécution, substituer entre elles les différentes instances d'une même classe virtualisée. le LSP est enterré.

    Évidemment, ça ne veut pas dire qu'il faut se passer de virtualité en général : c'est à la fois un principe naturel aux utilisateurs et extrêmement utile en pratique. Mais je trouve qu'il est important de séparer ces notions et de laisser le choix au programmeur.

    Ça a également le mérite de mettre en évidence le fait que le LSP peut être, selon les cas, interprété de manières diamétralement opposées.

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

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 614
    Points : 30 626
    Points
    30 626
    Par défaut
    Citation Envoyé par Obsidian Voir le message
    Ben justement, moi je ne trouve pas. Au contraire, je trouve que c'est une des forces du C++ de faire clairement le distingo entre les deux et de permettre d'exprimer une approche objet d'un programme en limitant au minimum les contraintes résolues au runtime.
    Si tu veux qu'un objet qui "passe pour être" d'un type de base puisse avoir un comportement (aka: une propriété valide de la classe dérivée, héritée de la classe de base) adapté au type réel de l'objet, tu n'as pas le choix : tu passe d'office par la virtualité à un moment ou à un autre...
    Pas nécessairement.
    J'aurais peut etre pu dire éventuellement adaptés (pour faire la distinction entre les comportements hérités de la classe de base qui ne nécessitent aucune adaptation et ceux qui justement en nécessitent), en effet...
    Ben non. Si ton véhicule est réputé avoir des roues, le fait de le spécialiser en voiture n'implique pas forcément que tu vas modifier ses roues. Et étendre la classe ne supprime en rien l'existant.
    En fait, dans ma phrase, qui se voulait une comparaison, j'abandonnais le mode "conception objet" pour le mode "vie courante"...

    Il est vrai qu'après avoir parlé de classes voitures et véhicule, cela pouvait porter à confusion.

    Je reprend donc : supprime la virtualité du langage, et tu te retrouve avec n'importe quel objet de la vie courante "amputé" de ce qui lui permet de rendre les services que l'on en attend.
    Eh bien, pour moi, c'est justement le LSP qui va faire que la virtualité doit être forcément optionnelle et, dans ce cas, explicite (même si Java adopte l'approche inverse).
    Si C++ a décidé que la virtualité devait être explicite, c'est essentiellement pour une raison :

    La première, c'est parce que la règle générale en C++ est de ne pas payer pour ce que l'on utilise pas.

    La virtualité ayant un cout, et le comportement de toutes les fonctions n'ayant pas forcément besoin d'être adapté, il n'est pas normal, dans cette optique, de forcer les fonctions à être virtuelles.

    Cependant
    Le LSP est le principe qui impose que toute propriété qui est vraie pour tout objet de type A l'est forcément aussi pour tout objet d'un type B sous-type de A. Le principe de substitution dit clairement qu'on doit pouvoir remplacer un A par un B de façon complètement transparente. C'est particulièrement important lorsque les routines qui reçoivent en arguments des objets d'un type donné n'ont pas connaissance de l'existence de sous-classe de ce type. Et justement, en virtualisant des fonctions-membres, on brise cette transparence.
    Justement, non

    C'est l'utilisation de l'objet "passant pour etre" du type de base qui doit être transparente, et non sa conception!!!

    Je m'explique :

    Si tu as une classe de base Forme, spécialisée en Cercle, Rectangle et triangle.

    les comportements valides de cette classe de base pourraient parfaitement être "calculeAire", "claculePerimetre" ou encore "tracer".

    En tant qu'utilisateur de la classe Forme, tu es parfaitement en droit de récupérer, par exemple, une collection de pointeurs sur Forme et de les transmettre un à un à une fonction qui "fera quelque chose" des objets "passant pour etre " de type Forme qu'elle recevra.

    Tu vas donc écrire une boucle pouvant ressembler à (C++11 inside)
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    for( auto forme : tab)
    {
        function(forme);
    }
    En tant qu'utilisateur de la classe Forme, tu auras remarqué qu'elle expose les fonctions dont je viens de parler et tu es donc parfaitement ne droit d'espérer, si tu invoque n'importe lequel de ces comportements valides au départ d'un "objet passant pour être" de type Forme, qu'il sera, effectivement, adapté au type réel de l'objet transmis: Tu t'attends, en effet, à ce que le cercle, le triangle ou le rectangle soient correctement affichés et non qu'ils soient représentés sous la forme d'un hexagone, et tu t'attend à ce que le cacul de l'aire ou du périmètre renvoie une valeur correcte

    A charge du concepteur de la classe de faire en sorte que ces comportements s'adaptent effectivement au type réel.
    Ce n'est pas un détail parce que virtualiser des fonctions rend non déterministes des choses qui l'étaient pourtant au départ. Prenons l'exemple classique : une classe « forme » que je dérive en « cercle », « carré », « triangle », etc.
    Bien au contraire, ca rend les comportements que l'on peut attendre de tout objet "passant pour etre" du type de base pleinement déterministes en fonction du type réellement utilisé
    Code C++ : 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
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    #include <string>
    #include <iostream>
     
    using namespace std;
     
    class Forme
    {
        public:
            virtual string QueSuisJe ();
    };
     
    class Cercle : public Forme
    {
        public:
            virtual string QueSuisJe ();
    };
     
    class Carre : public Forme
    {
        public:
            virtual string QueSuisJe ();
    };
     
    class Triangle : public Forme
    {
        public:
            virtual string QueSuisJe ();
    };
     
     
    string Forme::QueSuisJe ()
    {
        return "Forme";
    }
     
    string Cercle::QueSuisJe ()
    {
        return "Cercle";
    }
     
    string Carre::QueSuisJe ()
    {
        return "Carré";
    }
     
    string Triangle::QueSuisJe ()
    {
        return "Triangle";
    }
     
    int main (void)
    {
        unsigned int i;
        Forme *  tableau [4];
        Forme    fo;
        Cercle   ce;
        Carre    ca;
        Triangle tr;
     
        tableau[0] = &fo;
        tableau[1] = &ce;
        tableau[2] = &ca;
        tableau[3] = &tr;
     
        for (i=0;i<sizeof tableau / sizeof (*tableau);++i)
        cout << "Position " << i << ", je suis un(e) " << tableau[i]->QueSuisJe() << endl;
     
        return 0;
    }

    Code shell : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    $ ./programme
    Position 0, je suis un(e) Forme
    Position 1, je suis un(e) Cercle
    Position 2, je suis un(e) Carré
    Position 3, je suis un(e) Triangle
    $ _
    Rassures moi, tu ne voudrais quand meme pas qu'un carré te réponde "je suis un triangle, quand même
    On pourrait déjà objecter que c'est le mauvais exemple parce que « Cercle », « Carré » et « Triangle » sont plus restrictives que « Forme » en général qui peut être n'importe quoi, mais c'est une question de définition.
    Mais c'est le but même de l'héritage!!!

    Le but de l'héritage est de pouvoir spécialiser des classes dans le respect des invariants!

    C'est d'ailleurs pour cela que Carre n'héritera jamais ni de Rectangle ni de Losange et qu'il ne sera jamais dérivé en l'une de ces classes!

    La propriété qui fait que l'on peut questionner une forme sur son aire, sur son périmètre ou qu'on peut lui demander de se tracer ont beau être des invariants, cela ne signifie absolument pas que cela ne souffre aucune adaptation, bien au contraire!
    Il s'agit de savoir si la classe Forme peut définir à elle seule n'importe quelle forme (avec une liste de points, par exemple), ou si elle ne sert qu'à indiquer qu'il s'agit d'une forme, sans plus de détails. Mais ce n'est pas ce qui nous intéresse.
    Justement, c'est ce qui nous intéresse!!!

    La classe Forme est là pour fournir une interface (comprend : des propriétés valides) commune à tout objet susceptible d'être considéré comme étant une forme.

    A ce titre, si tu as besoin d'une fonction permettant de déterminer ce qu'est ton objet, ta fonction QueSuisJe() a tout à fait sa place dans la classe Forme, mais il est purement et simplement normal que son comportement s'adapte en fonction du type réel de l'objet.

    Si le comportement ne s'adaptait pas, tu créerais une fonction qui ne sert strictement à rien, pour la simple et bonne raison que l'on se doute que, toute forme utilisée est... une forme

    Maintenant, imaginons que je compte allouer de la place pour stocker les chaînes renvoyées par les objets de mon tableau. Sans virtualité, je sais qu'ils sont tous les mêmes, et en plus je connais cette chaîne à l'avance. J'ai donc tout-à-fait le droit d'écrire par exemple :

    Code C++ : Sélectionner tout - Visualiser dans une fenêtre à part
        ptr = new char[tableau[0]->QueSuisJe().length() * sizeof tableau / (sizeof *tableau)];
    Mais quel intérêt aurais tu à avoir XXX fois la même chaine de caractères Ne crois tu pas qu'une seule fois serait amplement suffisante
    Par contre, je ne peux absolument le garantir à l'avance avec des fonctions virtuelles. Ça a même toutes les chances de planter puisque j'ai volontairement choisi le nom « Forme » plutôt que « Polygone » ou autre pour qu'il soit le plus court de tout les noms définis ici et que les autres provoquent systématiquement un dépassement de tableau. :-)
    Mais, encore une fois, à quoi pourrait t'avance d'avoir XXX fois la chaine "Forme", étant donné que, travaillant avec un tableau de pointeur sur Forme, tu sais parfaitement que tu travailles... avec des formes
    Pire encore : si j'admets ce qui vient d'être dit et que je ne m'en tiens qu'à un seul élément du tableau, si :
    1. Je mesure la longueur de la chaîne renvoyée par l'élément tableau[0] ;
    2. j'alloue l'espace correspondant ;
    3. Je trie mon tableau ;
    4. Je stocke la chaîne que me renvoie mon élément [0] dans l'espace alloué


    Alors le programme risquera de planter aussi puisque la chaîne renvoyée ne sera plus la même. Moralité : non seulement je ne peux pas systématiquement substituer une classe virtualisée par une classe dérivée à la compilation de façon déterministe, mais je ne peux pas non plus, même à l'exécution, substituer entre elles les différentes instances d'une même classe virtualisée. le LSP est enterré.
    C'est surtout que tu fais n'importe quoi, pour justifier une position totalement injustifiable!

    Parce que, dés le moment où tu dis que la fonction QueSuisje n'est pas virtuelle, dans le cas présent, elle pert purement et simplement toute utilité, et toi toute crédibilité
    Évidemment, ça ne veut pas dire qu'il faut se passer de virtualité en général : c'est à la fois un principe naturel aux utilisateurs et extrêmement utile en pratique. Mais je trouve qu'il est important de séparer ces notions et de laisser le choix au programmeur.
    Mais on laisse au programmeur le choix...

    Celui de déterminer si un comportement donné mérite (ou a besoin, ou vaut la peine) d'être redéfini, affiné, adapté, au type dérivé réellement utilisé.
    Ça a également le mérite de mettre en évidence le fait que le LSP peut être, selon les cas, interprété de manières diamétralement opposées.
    Bien au contraire, cela a juste le mérite de mettre ne évidence le fait que tu ne sembles absolument pas avoir compris le principe même de la conception orientée objet
    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

  12. #12
    Modérateur
    Avatar de Obsidian
    Homme Profil pro
    Développeur en systèmes embarqués
    Inscrit en
    Septembre 2007
    Messages
    7 369
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 47
    Localisation : France, Essonne (Île de France)

    Informations professionnelles :
    Activité : Développeur en systèmes embarqués
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Septembre 2007
    Messages : 7 369
    Points : 23 623
    Points
    23 623
    Par défaut
    Bien au contraire, cela a juste le mérite de mettre ne évidence le fait que tu ne sembles absolument pas avoir compris le principe même de la conception orientée objet
    Mouais, je pratique depuis suffisamment longtemps pour ne pas avoir besoin de relever ce genre de compliment. Je pense que c'est plutôt toi qui n'a pas compris où je voulais en venir.

    Ta vision des choses me laisse surtout penser que tu considères l'héritage comme le mécanisme des couches d'abstraction, ce qui n'est pourtant pas la même chose même si c'est une des principales utilisations que l'on en fait. Tu peux effectivement utiliser une classe composée de fonctions-membres virtuelles pures ou non pour implémenter une interface, mais cela reste deux notions bien distinctes.

    La définition même de l'héritage, c'est d'étendre une classe existante en lui ajoutant des attributs. De fait, une sous-classe B de A est implicitement une A munie d'éléments supplémentaires. Ça suffit à définir l'héritage, et non seulement ça n'implique pas forcément la virtualité, mais ça n'implique pas non plus la surcharge. Voici un exemple trivial :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Fiche
    {
        public:
            string Nom;
            string Prenom;
            string Age;
    };
     
    class FicheEtendue : public Fiche
    {
        public:
            string Adresse;
            string NumeroDeTelephone;
    };
    On est bien d'accord qu'il y a héritage, et qu'une « FicheEtendue » est sémantiquement une « Fiche ». Or, ici, non seulement il n'y a pas de virtualité mais il n'y a même pas de fonction-membre du tout. Sur ce point, il s'agit donc déjà de deux notions complètement distinctes. À l'usage, toute fonction écrite antérieurement à la rédaction de B peut continuer à fonctionner de manière transparente avec des objets de type B sans même le savoir. Et bien sûr, l'idée première est d'éviter d'avoir à réécrire le même code pour l'objet B que pour l'objet A quand il est officiellement avéré que c'est le même.

    On est bien d'accord également que dans ce cas précis, les fonctions qui traitent des objets A n'ont pas besoin d'avoir une vue, même indirecte, sur les nouveaux champs de B pour continuer à faire proprement leur travail.

    Maintenant, est-il nécessaire d'avoir virtualité lorsqu'il y a surcharge ? Pas forcément non plus. Pas parce que le travail reste le même d'une classe à l'autre, mais parce que le comportement à avoir face à l'appelant est censé, par défaut, être celui auquel il s'attend, ce qui est légitime comme concept. Voici un autre exemple : une fonction « Affiche() » qui afficherait à l'écran, ligne par ligne, les champs de l'objet.

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Fiche
    {void Affiche ();
    };
     
    class FicheEtendue : public Fiche
    {void Affiche ();
    };
    Est-ce que je dois virtualiser cette fonction ou pas ? Ça dépend du point de vue : soit je sais à l'avance que ma classe va être étendue et je veux récupérer tous les champs, soit je n'affiche les cinq champs que dans les fonctions qui utilisent explicitement « FicheEtendue » et je m'en tiens aux trois champs dans ceux qui utilisent « Fiche », ce qui est le comportement normal hors virtualisation.

    Par ailleurs, poser la virtualisation comme condition nécessaire de l'héritage implique également que les fonctions membres concernées soient déclarées comme telles dans la classe mère, ce qui t'empêcherait de fait d'hériter des classes écrites par des tiers et dont tu ne disposerait pas du code source. À tout le moins, tu pourrais les déclarer virtuelles à partir de tes propres classes, mais pas utiliser tes objets avec les bibliothèques initiales.

    Justement, non

    C'est l'utilisation de l'objet "passant pour etre" du type de base qui doit être transparente, et non sa conception!!!

    Je m'explique :

    Si tu as une classe de base Forme, spécialisée en Cercle, Rectangle et triangle.

    les comportements valides de cette classe de base pourraient parfaitement être "calculeAire", "claculePerimetre" ou encore "tracer".

    En tant qu'utilisateur de la classe Forme, tu es parfaitement en droit de récupérer, par exemple, une collection de pointeurs sur Forme et de les transmettre un à un à une fonction qui "fera quelque chose" des objets "passant pour etre " de type Forme qu'elle recevra.
    Le LSP est résumé en une définition courte et claire : http://fr.wikipedia.org/wiki/Princip...te-subtyping-1

    Si q(x) est une propriété démontrable pour tout objet x de type T, alors q(y) est vraie pour tout objet y de type S tel que S est un sous-type de T.

    Ainsi, la notion de sous-type telle que définie par Liskov et Wing est fondée sur la notion de substituabilité : si S est un sous-type de T, alors tout objet de type T peut être remplacé par un objet de type S sans altérer les propriétés désirables du programme concerné 3.

    Définition que l'on retrouve page 1 du document original :

    Subtype Requirement: Let Φ(x) be a property provable about objects x of type T. Then Φ(y) should be true for objects y of type S where S is a subtype of T.

    C'est bien ce qui est écrit : lorsque tu substitues un B à un A en ses lieu et place, il doit se comporter de la même façon, et les propriétés qui sont vraies pour A doivent être vraies pour B aussi et ce, dans ce contexte précis.

    D'où mon exemple. Il a beau être inutile, c'est la démonstration d'un cas où les objets B ne sont pas systématiquement substituables à A. Et ce parce qu'une propriété qui était toujours vraie pour A (taille de la chaîne renvoyée constante) ne l'est plus avec ses dérivés, ne serait-ce que parce qu'il en y a plusieurs. On note bien que la définition du LSP donnée ci-dessus est une proposition mathématique et que le terme « propriété » s'entend dans le sens logique du terme. On ne parle pas ici, en particulier, d'un membre d'une classe ou du résultat d'une de ses fonctions.

    Pour autant, ton exemple avec l'aire reste valide. S'il est établi dès le départ que CalculeAire() renvoie une aire, tu peux virtualiser ta fonction et lui faire également renvoyer une autre aire, même si elle complètement différente. Par contre :

    Mais quel intérêt aurais tu à avoir XXX fois la même chaine de caractères Ne crois tu pas qu'une seule fois serait amplement suffisante
    C'est le même problème : tu sais à l'avance que n objets de type A renverront toujours la même description et qu'il est inutile de la stocker plusieurs fois. Ça te permet par exemple d'éviter de rappeler la même fonction membre lorsque tu fais une itération sur un tableau de A. C'est une propriété démontrable de A. Si tu surcharges cette fonction membre, ça ne changera rien car c'est toujours la fonction membre de A qui sera appelée. Par contre, si tu la virtualises, le code devient invalide parce que l'invariant ne l'est plus.

    Alors évidemment, dans ce dernier cas, la propriété cesse d'exister avec A même, puisque la fonction membre doit déjà être déclarée virtuelle à ce stade. Il n'empêche qu'on retombe quand même sur mes deux assertions de départ :

    • L'héritage n'implique pas la virtualisation (pas plus qu'elle ne l'interdit) ;
    • Ce n'est certainement pas le LSP qui va t'imposer la virtualisation non plus. Au pire, ils seront contradictoires, au mieux indépendants. Mais en aucun cas nécessaires l'un à l'autre.

  13. #13
    En attente de confirmation mail

    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Août 2004
    Messages
    1 391
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 33
    Localisation : France, Doubs (Franche Comté)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Août 2004
    Messages : 1 391
    Points : 3 311
    Points
    3 311
    Par défaut
    Personnelement voila comment je vois les choses (en conception orienté objet) :

    • Un type définie un ensemble de service et d'invariants. La création d'un objet d'un certain type est l'association de la zone mémoire où est l'objet et de ces services.
    • Chaque service est défini par un comportements, des pré-conditions et des post-conditions.


    Ensuite vient la notion de sous-type :
    • Elle est définie par le LSP.
    • Elle est mise en place par l'héritage publique.


    Le premier n'est qu'une définition pure et simple de la notion de sous-type, cependant elle implique que dans un langage objet l'offrant on doit pouvoir effecter un traitement sur un objet d'un sous-type du type attendu. Or (sans l'héritage), le typage fort du C++ ne permet pas une telle chose, l'héritage publique [1] n'est qu'un moyen pour offir ce comportement.

    Une autre solution est le duck typing, elle a d'ailleurs un avantage qui est de faciliter la mise en place des retour co-variant et des pramètres contra-variant (qui n'existe pas en C++, c'est un des défaut du modèle OO du C++). Ceci implique entre autre que si le LSP n'est pas respecté il n'y a pas à avoir d'héritage publique.

    Reprenons le LSP qui nous dit donc que chaque propriété démontrable sur un objet de type mère doit l'être sur un objet de type fille. Or nos types étant définient en terme de services, d'invariants, de pre-conditions et de post-conditions, cella implique que le type fille définisent au moins les même services, au moins les même invariants, des pré-conditions identiques ou plus faibles et des post-conditions identiques ou plus fortes [2].

    Passons maintenant à la virtualité. Pour le moment on a juste définient les conditions [3] que doivent remplir deux types pour être "parent". La syntaxe proposée par le C++ pour définir des services ce sont les fonctions publiques [4]. Les fonctions définissant des services, l'identifiant de la fonction est donc le nom du service, et ainsi les services commun à deux classes parentes doivent avoir le même nom. Cependant doivent-elle faire la même chose (ie avoir le même corps) ?
    1. Si c'est le cas, alors le système d'héritage du C++ nous permet de n'avoir à déclarer et définir cette fonction uniquement dans le type mère, il sera accessible "depuis" le type fille.
    2. Si ce n'est pas le cas, alors le C++ nous permet deux choses différentes, la surcharge et le masquage :
      • Un service étant définie par un nom, il s'en suit que si plusieurs fonctions (avec des signatures différentes) possèdent le même nom elles définissent quand même le même service. En effet on a vu que les pré-condtion d'un service pouvaient être plus faible pour le type fille, un moyen d'affaiblir ces pré-condition [5] c'est d'offrir une nouvelle fonction avec une autre signature. Si le comportement pour l'ensemble des pré-condtions communes est le même pour les deux types alors il y aura une déclaration et sa définition dans la classe mère et une déclaration et une définition dans la classe fille [6].
      • Maintenant disons que les services ont les même pré-conditions dans ce cas la signature sera la même [7]. Or le comportement doit bien différer si l'objet est d'un type ou de l'autre. On retrouve ici les limites de typage fort, limite contourné par le C++ en introduisant la virtualité qui permet d'avoir un comportement en fonction du type de l'objet (donc le comportement attendu [8]) et pas en fonction du type vue par le compilateur.


    __________


    Pour résumer, dans une situation de conception objet :
    • Le LSP est une condition nécessaire à l'héritage publique. En allant jusqu'au bout des choses on peut même la considérer suffisante, cependant on en a pas nécessairement le besoin (ie on introduit des héritages parce qu'on en a besoin, pas juste parce que le LSP est respecté). Un langage duck typing offre par nature l'implication.
    • La virtualité est une solution technique pour mettre en place deux comportements distincts d'un service ayant les même pré-condtions dans les deux types.


    Donc je suis d'accord sur le fait que ni le LSP ni l'héritage n'implique la virtualisation [9], cependant elle le sera dès que l'on désire avoir (ou pouvoir avoir) deux comportements différents pour un service avec les même pré-conditions. Je pense que ca représente quand même un grand nombre de situations où l'on a le LSP-héritage publique.


    __________


    PS: Ceci n'est qu'un avis personnel.

    __________


    [1] Je me place exclusivement dans le code métier, des besoins purement technique peuvent amener à des héritages publiques sans sémantique sous-jacente.

    [2] Ca se démontre sans problème :
    • La propriété "Le service X existe" avec X un service du type mère doit rester vrai pour la classe fille.
    • Même démonstration pour les invariants et les post-conditions.
    • Pour la dernière procédons par l'absurde, supposons qu'on ai un service X avec des pré-condtions plus fortes pour le type fille, ainsi il existe un contexte dans lequel les pré-conditions sont remplit pour le service du point de vue du type mère mais pas du point de vue du type fille. Considérons la propriété "Le service X a ses pré-conditions remplient", ainsi il existe une propriété que est valide pour le type mère mais pais pour le type fille, donc le type fille n'est pas un sous-type du type mère. Contradiction.


    [3] C'est le LSP dont j'ai donné la formulation PBC au passage (B. Meyer)

    [4]
    • J'exclue volontairement le reste, qui peuvent aussi définir des services (les données membres publiques entre autre), ils n'apportent rien au sujet de la virtualité.
    • "Publique" Dans le contexte on parle de conception, il est naturel d'exclure ce qui concerne l'interface. Cependant dans on peut retrouver certains raisonnement à d'autre échelle (intra-hiérarchie avec l'héritage protégé par exemple). Dans "Publique" s'inclue aussi l'ensemble des fonctions appartenant au même namespace que celui du type, cependant ca n'apporte rien pour la discussion sur la virtualité.


    [5] Une autre sont les retour contra-variant, qui n'existent pas en C++

    [6] Il y a masquage, il y a une question qui se pose qui est de savoir si l'on doit pouvoir accéder au service pour l'ensemble des conditions possible ou juste à celui pour lequel le type déclare le service. Si non alors il n'y a rien d'autre à faire, si oui il faut "démasquer" la partie du service déclaré dans la classe mère avec using.

    [7] La possibilité de varier le type de retour (lié aux post-conditions) est permis en C++ grâce aux retour co-variant. La mécanisme symétrique sur les paramètres serait de la contra-variance, qui n'existe pas en C++.

    [8] En effet si l'on définit des services et des types c'est bien pour avoir les comportements que l'on définit et pas d'autre.

    [9] Or destructeur (si publique) comme l'a rappelé 3DArchi.

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

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 614
    Points : 30 626
    Points
    30 626
    Par défaut
    Citation Envoyé par Obsidian Voir le message
    Mouais, je pratique depuis suffisamment longtemps pour ne pas avoir besoin de relever ce genre de compliment. Je pense que c'est plutôt toi qui n'a pas compris où je voulais en venir.

    Ta vision des choses me laisse surtout penser que tu considères l'héritage comme le mécanisme des couches d'abstraction, ce qui n'est pourtant pas la même chose même si c'est une des principales utilisations que l'on en fait. Tu peux effectivement utiliser une classe composée de fonctions-membres virtuelles pures ou non pour implémenter une interface, mais cela reste deux notions bien distinctes.

    La définition même de l'héritage, c'est d'étendre une classe existante en lui ajoutant des attributs. De fait, une sous-classe B de A est implicitement une A munie d'éléments supplémentaires. Ça suffit à définir l'héritage, et non seulement ça n'implique pas forcément la virtualité, mais ça n'implique pas non plus la surcharge. Voici un exemple trivial :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Fiche
    {
        public:
            string Nom;
            string Prenom;
            string Age;
    };
     
    class FicheEtendue : public Fiche
    {
        public:
            string Adresse;
            string NumeroDeTelephone;
    };
    On est bien d'accord qu'il y a héritage, et qu'une « FicheEtendue » est sémantiquement une « Fiche ». Or, ici, non seulement il n'y a pas de virtualité mais il n'y a même pas de fonction-membre du tout. Sur ce point, il s'agit donc déjà de deux notions complètement distinctes. À l'usage, toute fonction écrite antérieurement à la rédaction de B peut continuer à fonctionner de manière transparente avec des objets de type B sans même le savoir. Et bien sûr, l'idée première est d'éviter d'avoir à réécrire le même code pour l'objet B que pour l'objet A quand il est officiellement avéré que c'est le même.
    Je comprends ton point de vue et je t'accorde tout ce que tu dit, cependant...

    Le seul fait que le destructeur par défaut soit public et non virtuel rend déjà tout ton raisonnement caduque.

    En effet, si tu places indifféremment des pointeurs sur Fiche et sur FicheEtendue dans une collection, lorsque tu voudra détruire tes objets, l'absence de destructeur virtuel fera que les objets de type FicheEtendue "passant pour être" de type Fiche ne seront pas correctement détruits.


    Tout le raisonnement qui suit a beau présenter une certaine logique, il n'est pas moins totalement erroné du seul fait que tu présente des classes ayant sémantique de valeur alors que l'héritage n'est correctement concevable qu'avec des classes ayant sémantique d'entité.

    J'aurais éventuellement pu admettre le reste si le destructeur (non virtuel) avait été protégé ou privé, mais le problème serait resté que, pour que ton objet de type FicheEtendue soit correctement détruit, tu aurais alors eu besoin de savoir que tu avais effectivement affaire à un objet de ce type particulier, ce qui met bel et bien en évidence que tu place une énorme limite au concept de substituabilité, étant donné que la destruction fait partie des comportements incontournables et peut donc être considéré comme "une propriété valide" en terme de LSP.
    C'est le même problème : tu sais à l'avance que n objets de type A renverront toujours la même description et qu'il est inutile de la stocker plusieurs fois. Ça te permet par exemple d'éviter de rappeler la même fonction membre lorsque tu fais une itération sur un tableau de A. C'est une propriété démontrable de A. Si tu surcharges cette fonction membre, ça ne changera rien car c'est toujours la fonction membre de A qui sera appelée. Par contre, si tu la virtualises, le code devient invalide parce que l'invariant ne l'est plus.
    C'est typiquement le genre de raisonnement que tu peux, encore une fois, avoir avec des classes ayant sémantique de valeur, mais qui devient caduque avec les classes ayant sémantique d'entité.
    Alors évidemment, dans ce dernier cas, la propriété cesse d'exister avec A même, puisque la fonction membre doit déjà être déclarée virtuelle à ce stade. Il n'empêche qu'on retombe quand même sur mes deux assertions de départ :

    • L'héritage n'implique pas la virtualisation (pas plus qu'elle ne l'interdit) ;
    • Ce n'est certainement pas le LSP qui va t'imposer la virtualisation non plus. Au pire, ils seront contradictoires, au mieux indépendants. Mais en aucun cas nécessaires l'un à l'autre.
    Comme je te l'ai dit plus haut, tout ton raisonnement est caduque du seul fait d'un destructeur public et non virtuel pour ta classe de base, ce qui rend de facto ta classe Fiche non héritable, l'une des propriétés valide incontournable de cette classe n'étant pas valide pour les classes dérivées.

    Il ne faut pas oublier qu'il y a quatre comportements incontournables en programmation objet : ceux mis en évidence par les différentes formes de Coplien.

    Or, de ces différentes formes, il y en a une qui n'est clairement pas destinée à permettre la création de classes héritable, et c'est justement celle que tu utilises dans ton raisonnement

    Dés lors, je persiste et signe : la virtualité a beau être optionnelle en C++, elle se pose comme une condition sine qua non, ne serait-ce que pour ce qui concerne un destructeur laissé public, dés le moment où un héritage est envisagé / envisageable.

    Pour un destructeur public, la virtualité est le seul moyen d'assurer le fait que la propriété "destructible" valide pour la classe parent soit également valide pour la classe enfant.

    Il en va, d'ailleurs, de même pour tout comportement susceptible d'être redéfini dans les classes dérivées car un comportement public de la classe de base représente une propriété valide de celle-ci et doit donc rester valide pour ses sous types:

    Si je décide de redéfinir une fonction héritée de la classe de base pour une classe que je crées moi-même, je suis malgré tout en droit d'espérer que, meme dans le cas d'une substitution, le comportement de cette fonction soit cohérent avec le type réel de l'objet.

    Redéfinir une fonction non virtuelle est, effectivement, possible, mais a l'effet pervers d'obtenir un comportement imprédictible et différent (donc potentiellement dangereux) en fonction du fait que tu utilises l'objet comme étant son type réel ou comme passant pour etre du type dérivé.

    Au delà de cela, j'ai peut etre pris des raccourcis honteux dans mon intervention, j'ai peut etre laissé passer des amalgames, mais le fait persiste malgré tout

    Flob90 ==> je suis entièrement d'accord avec ce que tu écris, sauf pour ce qui concerne le (1)... :

    Si tu en viens, pour une raison technique, à envisager l'héritage public sans sémantique sous-jacente, tu as largement intérêt à envisager un autre type de relation (héritage privé et utilisation des directives usign ou agrégation par exemple)
    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

  15. #15
    Expert éminent sénior
    Avatar de Médinoc
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Septembre 2005
    Messages
    27 369
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 40
    Localisation : France

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Septembre 2005
    Messages : 27 369
    Points : 41 519
    Points
    41 519
    Par défaut
    HS: "Typage de duc"? Je connaissais le "typage de canard", mais pas ça...
    SVP, pas de questions techniques par MP. Surtout si je ne vous ai jamais parlé avant.

    "Aw, come on, who would be so stupid as to insert a cast to make an error go away without actually fixing the error?"
    Apparently everyone.
    -- Raymond Chen.
    Traduction obligatoire: "Oh, voyons, qui serait assez stupide pour mettre un cast pour faire disparaitre un message d'erreur sans vraiment corriger l'erreur?" - Apparemment, tout le monde. -- Raymond Chen.

  16. #16
    En attente de confirmation mail

    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Août 2004
    Messages
    1 391
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 33
    Localisation : France, Doubs (Franche Comté)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Août 2004
    Messages : 1 391
    Points : 3 311
    Points
    3 311
    Par défaut
    @Médinoc: Erreur de frappe (2 fois en plus ...) de ma part, corrigé.

    @koala01: Mon point [1] faisait référence, entre autre, aux classes de politique et aux techniques de méta-programmation template.

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

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 614
    Points : 30 626
    Points
    30 626
    Par défaut
    Citation Envoyé par Flob90 Voir le message
    @koala01: Mon point [1] faisait référence, entre autre, aux classes de politique et aux techniques de méta-programmation template.
    Mais, à ce moment là, nous sommes non plus dans une conception orientée objet mais, surtout, dans une conception générique, et la différence est suffisamment importante pour autoriser certaines exceptions dans le sens où les politiques et les traits ne portent pas "réellement" sur une hiérarchie de classe au sens de l'héritage OO : Même si tu as une politique qui est classe de base, il faut que le paramètre template soit identique pour que tu aies, effectivement, un héritage au sens OO (tu ne peux pas substituer un objet dérivant de Policy<Type> à un objet dérivant de Policy<AutreType> )
    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

  18. #18
    En attente de confirmation mail

    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Août 2004
    Messages
    1 391
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 33
    Localisation : France, Doubs (Franche Comté)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Août 2004
    Messages : 1 391
    Points : 3 311
    Points
    3 311
    Par défaut
    @koala01: Je pense qu'on est d'accord, c'était du moins l'idée que j'avais en écrivant cette note : rappeler qu'on est dans le contexte orienté objet et pas ailleurs. Après relecture, en effet ce n'est pas super clair formulé ainsi.

  19. #19
    Modérateur
    Avatar de Obsidian
    Homme Profil pro
    Développeur en systèmes embarqués
    Inscrit en
    Septembre 2007
    Messages
    7 369
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 47
    Localisation : France, Essonne (Île de France)

    Informations professionnelles :
    Activité : Développeur en systèmes embarqués
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Septembre 2007
    Messages : 7 369
    Points : 23 623
    Points
    23 623
    Par défaut
    Hello,

    Citation Envoyé par koala01 Voir le message
    Je comprends ton point de vue et je t'accorde tout ce que tu dit, cependant... Le seul fait que le destructeur par défaut soit public et non virtuel rend déjà tout ton raisonnement caduque.
    Ah bon ? Moi j'aurais plutôt tendance à penser que Bjarne Stroustrup était de mon côté. :-) Sinon le destructeur aurait été automatiquement virtuel par défaut.

    En effet, si tu places indifféremment des pointeurs sur Fiche et sur FicheEtendue dans une collection, lorsque tu voudra détruire tes objets, l'absence de destructeur virtuel fera que les objets de type FicheEtendue "passant pour être" de type Fiche ne seront pas correctement détruits.
    Non, car dans le cas d'un container, soit tes objets y sont stockés par copie et dans ce cas, seule la partie « Fiche » sera dupliquée, soit tes objets y sont ajoutés uniquement par référence (ou via des pointeurs) et là, ils n'ont pas vocation à être détruits par le container puisqu'ils peuvent être référencés ailleurs.

    On rappelle que, depuis le début, on parle bien « d'héritage » en général et pas de « polymorphisme » en particulier.

    En outre, dans cet exemple, il n'y a pas de destructeur non plus ! Alors, effectivement, il serait nécessaire pour aller nettoyer mes strings mais si les attributs de ma classe n'étaient composés que de « int », par exemple, la seule action nécessaire serait de libérer l'espace alloué à l'objet entier.

    D'autre part, il me semble que la discussion portait sur l'héritage en programmation objet en général et pas seulement en C++. Dans cette optique, on retrouve des relations d'héritage même en dehors des langages de programmation : par exemple, certains SGBD tels que PostgreSQL permettent de faire de l'héritage de table. C'est bien le même concept, mais complètement débarrassé de toute notion d'exécution de programme.

    Tout le raisonnement qui suit a beau présenter une certaine logique, il n'est pas moins totalement erroné du seul fait que tu présente des classes ayant sémantique de valeur alors que l'héritage n'est correctement concevable qu'avec des classes ayant sémantique d'entité.
    Là, j'avoue que j'ai du mal à voir où tu veux en venir. Si je m'en tiens à la définition de la FAQ, une « Fiche », étendue en « FicheEtendue » ou pas, a clairement sémantique d'entité, ne serait-ce que parce que dans le cas présent, elles peuvent désigner des homonymes. Dans ce cas, non seulement les instances d'une même classe sont bien distinctes (tu pourrais éventuellement coller un numéro de fiche dans la classe mère) mais les personnes dont elles contiennent les informations sont elles-mêmes distinctes.

    J'aurais éventuellement pu admettre le reste si le destructeur (non virtuel) avait été protégé ou privé, mais le problème serait resté que, pour que ton objet de type FicheEtendue soit correctement détruit, tu aurais alors eu besoin de savoir que tu avais effectivement affaire à un objet de ce type particulier, ce qui met bel et bien en évidence que tu place une énorme limite au concept de substituabilité,
    Je ne place pas une limite, j'essaie de démontrer qu'ils s'agit de concepts distincts et que l'un n'implique pas automatiquement l'autre.

    étant donné que la destruction fait partie des comportements incontournables et peut donc être considéré comme "une propriété valide" en terme de LSP.
    Ça se discute également car :

    1. Ça n'est vrai qu'à partir du moment où tu inclus le destructeur dans l'interface ainsi formée pour pouvoir l'appeler depuis un code qui n'a pas forcément connaissance des classes dérivées. C'est légitime mais le problème est le même que celui du départ et ne diffère en rien des fonctions membres virtuelles ordinaires ;
    2. Le seul service rendu par le destructeur de A, c'est faire le nettoyage nécessaire avant la libération de l'espace d'un objet de type A. En ce sens, c'est toujours le cas, et le LSP est respecté puisque le comportement observé sera rigoureusement le même dans les deux cas. Évidemment, les éléments propres à B resteront dans les limbes, mais ça, c'est un effet de bord. Ce n'est pas la faute de A, ni du LSP ;
    3. Il n'est même pas nécessaire d'utiliser une fonction virtuelle pour libérer l'espace proprement dit puisque si on n'a pas connaissance des classes dérivées, on a forcément connaissance des classes parentes et que, donc, le pointeur this reste le même.

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

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 614
    Points : 30 626
    Points
    30 626
    Par défaut
    Citation Envoyé par Obsidian Voir le message
    Ah bon ? Moi j'aurais plutôt tendance à penser que Bjarne Stroustrup était de mon côté. :-) Sinon le destructeur aurait été automatiquement virtuel par défaut.
    Justement, non...

    Son principe a été "on ne paye pas pour ce que l'on n'utilise pas" et, comme l'héritage est "facultatif" (on n'est pas obligé de faire dériver une classe d'une autre, ni même de prévoir qu'elle puisse l'être, si la classe a sémantique de valeur ), il est "normal" que l'on ne paye pas pour le mécanisme qui permet de détruire correctement un objet dérivé (qui est facultatif) en passant par l'objet de base

    Non, car dans le cas d'un container, soit tes objets y sont stockés par copie et dans ce cas, seule la partie « Fiche » sera dupliquée,
    ... Et tu perds une partie des informations de type FicheEtendue, ce qui est inacceptable
    soit tes objets y sont ajoutés uniquement par référence (ou via des pointeurs) et là, ils n'ont pas vocation à être détruits par le container puisqu'ils peuvent être référencés ailleurs.
    Mais si tu te base sur le contenu de ton conteneur pour les détruire (par exemple, au moment du grand "clean up" avant de quitter l'application), ce sera ce qui est pointé par des... pointeurs sur le type de base que tu essayeras de détruire, et, si le destructeur est public et non virtuel, seuls les membres correspondant à la classe de base seront correctement détruits
    On rappelle que, depuis le début, on parle bien « d'héritage » en général et pas de « polymorphisme » en particulier.
    J'en suis conscient, mais la destruction d'un objet se doit, malheureusement, s'adapter au type réel de l'objet.

    Le polymorphisme, qui est une chose qui n'est permise que grâce au mécanisme d'héritage, est donc indissociable du mécanisme en lui-même, ne serait ce que pour le comportement de "destructibilité"

    En outre, dans cet exemple, il n'y a pas de destructeur non plus ! Alors, effectivement, il serait nécessaire pour aller nettoyer mes strings mais si les attributs de ma classe n'étaient composés que de « int », par exemple, la seule action nécessaire serait de libérer l'espace alloué à l'objet entier.
    Ce n'est pas parce que tu ne crées pas explicitement un destructeur qu'il n'y en a pas, bien au contraire:

    Le compilateur en rajoute d'office un qui est public et non virtuel

    Tout comme le compilateur rajoute un constructeur par défaut, un constructeur par copie et un opérateur d'affectation si l'on ne prend pas les dispositions pour l'en empêcher

    C'est, justement, pour prendre cette particularité en compte que la notion de POD a été revue dans la norme
    Là, j'avoue que j'ai du mal à voir où tu veux en venir. Si je m'en tiens à la définition de la FAQ, une « Fiche », étendue en « FicheEtendue » ou pas, a clairement sémantique d'entité, ne serait-ce que parce que dans le cas présent, elles peuvent désigner des homonymes. Dans ce cas, non seulement les instances d'une même classe sont bien distinctes (tu pourrais éventuellement coller un numéro de fiche dans la classe mère) mais les personnes dont elles contiennent les informations sont elles-mêmes distinctes.
    Mais, justement, il te faut un facteur discriminant permettant d'identifier de manière unique et non ambigüe une fiche particulière parmis les différents homonymes qu'il peut y avoir...

    Si, voulant modifier la fiche de Gerard Lambert de Calais, boucher de son état, tu en viens à modifier la ficher de Gerard Lambert de Paris, Carrossier de son état, il y aura un problème...

    Nous sommes donc bel et bien face à la définition d'une classe ayant sémantique d'entité : deux instances de la classes peuvent parfaitement avoir des attributs strictement identiques mais représenter des entités diffférentes, et donc seront considérées différentes si l'adresse mémoire à laquelle elles se trouvent sont différentes
    Je ne place pas une limite, j'essaie de démontrer qu'ils s'agit de concepts distincts et que l'un n'implique pas automatiquement l'autre.
    Avec le destructeur public et non virtuel, j'ai déjà démontré que le comportement de destruction n'est pas correct dans certaines situations.

    Tu pourrais, d'un autre coté, éviter la virtualité si le destructeur de Fiche était protégé, mais

    Si le destructeur de Fiche est protétgé, le compilateur t'envoie une erreur sur un code aussi simple que
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    void foo()
    {
        Fiche mafiche;
        /* ... */
    } //Erreur ici : tentative d'utiliser le destructeur de mafiche qui est protégé dans ce contexte
    ce qui fait que tu ne peux pas créer d'instance de Fiche, vu que tu ne peux pas la détruire.

    Si ce n'est pas mettre une restriction forte à l'utilisation de Fiche, dis moi ce que c'est

    Ça se discute également car :

    [LIST="1"][*]Ça n'est vrai qu'à partir du moment où tu inclus le destructeur dans l'interface ainsi formée pour pouvoir l'appeler depuis un code qui n'a pas forcément connaissance des classes dérivées. C'est légitime mais le problème est le même que celui du départ et ne diffère en rien des fonctions membres virtuelles ordinaires ;
    Si ce n'est que le destructeur fait partie des fonctions automatiquement générée par le compilateur si tu ne lui donne pas de raison de ne pas le faire, et qu'il est alors public et non virtuel
    [*]Le seul service rendu par le destructeur de A, c'est faire le nettoyage nécessaire avant la libération de l'espace d'un objet de type A. En ce sens, c'est toujours le cas, et le LSP est respecté puisque le comportement observé sera rigoureusement le même dans les deux cas. Évidemment, les éléments propres à B resteront dans les limbes, mais ça, c'est un effet de bord. Ce n'est pas la faute de A, ni du LSP ;
    Si ce n'est, encore une fois, que tes deux objets ne sont pas substituables en terme de LSP vu que tu ne peux pas faire passer un objet de type FicheEtendue comme étant de type Fiche et obtenir un comportement cohérent pour ce comportement de destruction (vu que la destruction est partielle)
    [*]Il n'est même pas nécessaire d'utiliser une fonction virtuelle pour libérer l'espace proprement dit puisque si on n'a pas connaissance des classes dérivées, on a forcément connaissance des classes parentes et que, donc, le pointeur this reste le même.
    Tant que tu travailles avec des objets de type Fiche...

    Mais, dés que tu "fais passer" un objet de type FicheDerivee, que ce soit en utilisant une référence ou en utilisant un pointeur, il en va tout autrement :Les comportements valide de la classe de base doivent etre valides pour la classe dérivée, et ce n'est pas le cas du destructeur
    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. [JMerise] Version 0.3.9.7 - Héritage sans contrainte
    Par aidefz dans le forum JFreesoft
    Réponses: 3
    Dernier message: 14/09/2015, 15h31
  2. Modéliser héritage sans exclusion
    Par Lystik dans le forum UML
    Réponses: 3
    Dernier message: 31/07/2013, 17h24
  3. Implémentation héritage sans relation cyclique
    Par Driss35 dans le forum Diagrammes de Classes
    Réponses: 6
    Dernier message: 03/08/2012, 19h50
  4. [modelisation] Héritage sans nouvelles spécifications
    Par poukill dans le forum Diagrammes de Classes
    Réponses: 8
    Dernier message: 21/06/2007, 14h15
  5. Réponses: 3
    Dernier message: 04/06/2007, 08h34

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