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

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

C++ Discussion :

Redéfinition de méthode et principe de Liskov


Sujet :

C++

  1. #1
    Membre averti Avatar de Ekinoks
    Profil pro
    Étudiant
    Inscrit en
    Novembre 2003
    Messages
    687
    Détails du profil
    Informations personnelles :
    Âge : 37
    Localisation : France, Bouches du Rhône (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Novembre 2003
    Messages : 687
    Points : 358
    Points
    358
    Par défaut Redéfinition de méthode et principe de Liskov
    Salut !

    J'aurais quelque questions sur des problèmes de redéfinition de méthode en C++. Comme je n'ai pas réussi à trouver de solution (il en existe peu être pas), je vous soumet mes questions ^^;

    1> Il m'arrive fréquemment de vouloir empêcher la redéfinition d'une méthode pour les objet qui hérite de ma classe. Il me semble qu'en Java il existe le mot clé "final" pour faire ca... Es qu'il existe un équivalant en C++ ?

    2> Es qu'il existe un moyen de forcer l'exécution d'une méthode même si celle ci a était redéfinie et ce a partir de la classe mère ?
    Exemple :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    struct A {
       virtual void f() {
          cout<<"code A"<<endl;
       }
    };
     
    struct B : public A {
       virtual void f() {
          A::f(); // Existe t'il un moyen de faire cette appelle automatiquement sans que la classe fille est a le faire ?
          cout<<"code A"<<endl;
       }
    };
    3> Existe t'il un moyen de redéfinir des méthodes templates dans une classe fille ?



    Merci pour votre aide.

  2. #2
    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,

    1- Si une méthode ne doit pas être redéfinie, il "suffit" de ne pas la déclarer comme virtuelle

    2- non, si tu veux qu'une méthode de la classe mère redéfinie dans la classe fille, il faut l'invoquer de manière explicite.

    Par contre, il est possible d'utiliser le concept de l'interface non virtuelle pour t'assurer que certaines parties soient effectuées de manière systématique:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    class Base
    {
        public:
            void foo()
            {
                before();
                virtualCall();
                after();
            }
        protected:
            virtual void virtualCall()
            {
                /* partie virtuelle */
            }
        private:
            void before()
            {
                /* pré conditions */
            }
            void after()
            {
                /* post conditions */
            }
    };
    class Derivee : public Base
    {
        /*...*/
        protected:
            virtual void virtualCall()
            {
                /* comportement adapté à la classe dérivée */
            }
    };
    De cette manière, le comportement polymorphe se limite à ce qui est *réellement* à adapter au type dérivé

    3- non... une fonction template (ou une méthode de classe template) est adaptée au type utilisé au moment de la compilation

    Tu peux donc envisager une spécialisation partielle ou totale de la classe template de manière à adapter le fonctionnement au type réel, mais tu ne peux pas redéfinir cette fonction.

    Enfin, tu ne peux pas la redéfinir dans le sens polymorphe du terme, mais tu peux envisager de la redéfinir dans le sens d'un "recouvrement" de la fonction (la version dérivée de la fonction cachant la version de la classe de base).

    Cela implique que, si tu travaille sur des objets se faisant passer pour des instance de la classe de base, et que tu invoque la fonction, ce sera la version de la classe de base qui sera effectivement invoquée.
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

  3. #3
    Membre averti Avatar de Ekinoks
    Profil pro
    Étudiant
    Inscrit en
    Novembre 2003
    Messages
    687
    Détails du profil
    Informations personnelles :
    Âge : 37
    Localisation : France, Bouches du Rhône (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Novembre 2003
    Messages : 687
    Points : 358
    Points
    358
    Par défaut
    Merci pour ta reponse koala


    Citation Envoyé par koala01 Voir le message
    1- Si une méthode ne doit pas être redéfinie, il "suffit" de ne pas la déclarer comme virtuelle
    Ha oui j'ai oublié de préciser, c'est dans le cas ou la méthode est déjà déclaré virtuelle dans un ancêtre... je cherche donc a arrêter la propagation du "virtual"


    Citation Envoyé par koala01 Voir le message
    2- non, si tu veux qu'une méthode de la classe mère redéfinie dans la classe fille, il faut l'invoquer de manière explicite.

    Par contre, il est possible d'utiliser le concept de l'interface non virtuelle pour t'assurer que certaines parties soient effectuées de manière systématique
    Ok, c'est bien cette méthode que j'utilise pour contourner le problème.
    Mais elle a quand meme le désavantage de devoir trouver des nom diffirent.
    Pour peu que cette astuce soit réaliser plusieurs foi sur un même méthode et on se retrouver avec une liste de nom : "Call", "virtualCall", "virtualCall2", "virtualCall3"...

    Mais bon, si c'est la seul solution, on va faire avec :p


    Citation Envoyé par koala01 Voir le message
    3-
    Erf, ok... c'est bien ce que je craignais :'(

  4. #4
    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 Ekinoks Voir le message
    Merci pour ta reponse koala



    Ha oui j'ai oublié de préciser, c'est dans le cas ou la méthode est déjà déclaré virtuelle dans un ancêtre... je cherche donc a arrêter la propagation du "virtual"
    Ce que tu peux faire, c'est partir sur une interface non virtuelle, comme indiqué plus haut, et la redéfinition de l'accessibilité de la méthode qui ne doit plus être réimplémentée de manière à la rendre privée

    De cette manière, la méthode virtuelle devient inaccessible au départ de "l'ensemble du code" lorsque l'on dispose de la classe dérivée, et n'est accessible que "depuis les méthodes de la classe" (de base ou dérivée) de manière générale.

    N'oublie pas que, bien qu'il soit possible de définir un nombre important d'héritage en cascade, il est généralement "cohérent" d'essayer de limiter les niveaux d'héritage à trois ou quatre (il y a eu une discussion sur le sujet il y a quelques mois... une recherche sur le forum devrait te permettre de la retrouver )

    Ok, c'est bien cette méthode que j'utilise pour contourner le problème.
    Mais elle a quand meme le désavantage de devoir trouver des nom diffirent.
    Pour peu que cette astuce soit réaliser plusieurs foi sur un même méthode et on se retrouver avec une liste de nom : "Call", "virtualCall", "virtualCall2", "virtualCall3"...
    En toute logique, tu ne devrais pas te retrouver face à la situation d'avoir plus de trois parties "distinctes" dans ta méthode de base:
    • une partie qui concerne la gestion des "pré conditions", une partie qui concerne la gestion des "post conditions" et une partie, potentiellement polymorphe, qui concerne la gestion particulière au type en cours d'utilisation qui se trouve entre les deux premières citées


    Si tu envisage une méthode doSomething(), par exemple, tu peux donc "facilement" décider que les différentes parties seront nommée
    doSomethingFirst() (ou doSomethingBefore() ou... n'importe quel terme indiquant le fait que cela survient... au début), doSomethingImpl, pour l'implémentation polymorphe de la fonction et doSomethingLast() (ou doSomethingAfter() ou... n'importe quel terme indiquant le fait que cela survient... à la fin)

    S'agissant de méthodes qui ne seront invoquées que dans une seule méthode, du moins pour deux des trois méthodes, le fait qu'elles se retrouvent avec des noms "à charnières et à rallonges" ne posera pas trop de problème

    N'oublie pas qu'ici, j'ai décidé qu'il y aurait des pré et des post conditions à vérifier, pour donner un exemple complet de ce qui peut se faire, mais que, dans la réalité des faits, il ne sera pas rare de n'avoir que des pré ou que des post conditions, ce qui limitera d'autant le nombre de "sous méthodes" pour lesquelles il faudra trouver un nom

    N'oublie pas non plus que, idéalement, le terme utilisé comme nom de fonction devrait représenter ne serait-ce que succintement le but poursuivi (ou le role joué) par cette fonction, raison pour laquelle je te conseille de simplement rajouter "before","impl"(émentation) ou "after" au nom de la fonction
    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 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
    une partie qui concerne la gestion des "pré conditions", une partie qui concerne la gestion des "post conditions" et une partie, potentiellement polymorphe, qui concerne la gestion particulière au type en cours d'utilisation qui se trouve entre les deux premières citées
    Le gros malheur de cette méthode, c'est qu'elle ne permet pas d'étendre les préconditions... (le require_else de eiffel)

    Un de mes gros griefs à C++, toujours pas adressé par C++0x.

  6. #6
    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 white_tentacle Voir le message
    Le gros malheur de cette méthode, c'est qu'elle ne permet pas d'étendre les préconditions... (le require_else de eiffel)

    Un de mes gros griefs à C++, toujours pas adressé par C++0x.
    En toute logique, si tu as des pré (ou post) conditions valides pour une classe de base, elles doivent rester valides pour les classes dérivées (extension du principe de substitution).

    Si tu dois étendre ces conditions pour l'une des classes dérivées, tu reste tout à fait libre de les étendre dans le comportement polymorphe

    Je ne vois donc pas vraiment quel grief tu peux faire à ce 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

  7. #7
    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 koala01 Voir le message
    En toute logique, si tu as des pré (ou post) conditions valides pour une classe de base, elles doivent rester valides pour les classes dérivées (extension du principe de substitution).
    Peut être veut-il dire que ce mécanisme de pré/post condition ne peut s'enrichir en descendant : A<-B<-C. A::MaMethode est coupé en A:: PreMaMethode, A:: PostMaMethode et A::MaMethodeImpl qui est virtuelle. Comment faire pour B:: PreMaMethode, B:: PostMaMethode et B::MaMethodeImpl sans passer par des usines à gaz ?

  8. #8
    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
    J'allais justement éditer mon dernier message pour apporter une nuance dans ce que je dis et "couper l'herbe sous le pied" à cette remarque

    En effet, bien que, dans les messages que j'ai écrits précédemment, je laisse à penser que les comportements vérifiant les conditions (pré et post) ne sont pas polymorphes, rien n'empêche non plus de les rendre polymorphes si nous n'arrivons décidément pas à trouver de plus petit comportement commun à définir dans la classe de base

    Après tout, tu peux très bien en arriver à quelque chose comme
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    class Base
    {
        public:
            void doSomething()
            {
                doSomethingBegin();
                doSomethingImpl();
                doSomethingEnd();
            }
        protected:
            virtual void doSomethingBegin()
            {
                /* ne fait rien car il n'y a pas de données à vérifier */
            }
            virtual void doSometingImpl()
            {
                /* ne fait rien car il n'y a pas de données à manipuler*/
            }
            virtual void doSomethingEnd()
            {
                /* ne fait rien car il n'y a pas de données à vérifier */
            }
    };
    class Derivee : public Base
    {
        protected:
            virtual void doSomethingBegin()
            {
                /* vérifie les données */
            }
            virtual void doSometingImpl()
            {
                /* manipule les données*/
            }
            virtual void doSomethingEnd()
            {
                /*  vérifie les données */
            }
    };
    Simplement, tu veille à ce que les différents comportements puissent rester "atomiques"
    [EDIT]il faut juste prendre en compte le fait que les méthodes virtuelles ne devraient pas être implémentées dans une définition de classe... je l'ai fait ainsi par "paresse naturelle"
    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

  9. #9
    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
    En toute logique, si tu as des pré (ou post) conditions valides pour une classe de base, elles doivent rester valides pour les classes dérivées (extension du principe de substitution).
    Pas tout à fait. Relis OOSC .

    Soit A::f(int i) qui requiert i > 0

    Soit B dérive de A. Tu peux avoir B::f(int i) qui requiert seulement i >= 0

    C'est parfaitement LSP-compatible. Si E(A) est l'ensemble des entrées valides de A, alors E(B) est un ensemble contenant au moins E(A). Inversement, S(B) est un sous-ensemble de S(A).

    Je ne connais pas de moyen de garantir ça en C++ (et il me semble avoir lu de James Kanze que ce n'est pas possible).

  10. #10
    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 white_tentacle Voir le message
    Pas tout à fait. Relis OOSC .

    Soit A::f(int i) qui requiert i > 0

    Soit B dérive de A. Tu peux avoir B::f(int i) qui requiert seulement i >= 0

    C'est parfaitement LSP-compatible. Si E(A) est l'ensemble des entrées valides de A, alors E(B) est un ensemble contenant au moins E(A). Inversement, S(B) est un sous-ensemble de S(A).

    Je ne connais pas de moyen de garantir ça en C++ (et il me semble avoir lu de James Kanze que ce n'est pas possible).
    Dans ce cas, je te ramène à mon dernier message, qui a croisé le tien

    EDIT: En outre, il pourrait s'avérer intéressant d'envisager d'inverser la relation d'héritage (ce que l'on peut toujours faire) pour que la condition la moins stricte ( i >= 0) soit vérifiée dans la classe la moins dérivée.

    Il est en effet faux de croire que l'héritage n'a pour seul objectif de permettre la spécialisation de la classe de base... Il est des cas (rares, mais qui peuvent se présenter quand même) ou l'héritage aura pour objectif une généralisation de quelque chose de plus spécialisé
    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

  11. #11
    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
    Dans le cas i>0 et i>=0, j'aurais aussi tendance à penser que l'héritage est inversé puisque le strict cas est inclus dans inférieur ou égal.
    A mon avis, c'est aussi embêtant si tu veux être cumulatif : A:: Pre(i>=0), A<-B:: Pre(i>5), B<-C:: Pre(i>10)...

  12. #12
    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
    Dans ce cas, je te ramène à mon dernier message, qui a croisé le tien
    Le problème, c'est que si tu autorises à redéfinir les préconditions de manière "brute", tu peux supprimer des préconditions. Et ça, c'est contraire au LSP.

    Dans le cas i>0 et i>=0, j'aurais aussi tendance à penser que l'héritage est inversé puisque le strict cas est inclus dans inférieur ou égal.
    Si tu inverses l'héritage, tu ne suis plus le LSP. La spécialisation vient du fait qu'on travaille sur un B et plus un A. Simplement, un B étant un A, l'appelant doit pouvoir, en vérifiant les préconditions nécessaires pour un A, être sûr qu'il peut appeler la méthode. Si l'appelant sait qu'il travaille sur un B, il peut se contenter de vérifier les préconditions de B, qui ne peuvent être que identiques ou moins strictes.

    A mon avis, c'est aussi embêtant si tu veux être cumulatif : A:: Pre(i>=0), A<-B:: Pre(i>5), B<-C:: Pre(i>10)...
    Là tu ne suis plus le LSP.

  13. #13
    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 3DArchi Voir le message
    Dans le cas i>0 et i>=0, j'aurais aussi tendance à penser que l'héritage est inversé puisque le strict cas est inclus dans inférieur ou égal.
    A mon avis, c'est aussi embêtant si tu veux être cumulatif : A:: Pre(i>=0), A<-B:: Pre(i>5), B<-C:: Pre(i>10)...
    Mais tu remarquera que les conditions restent cohérentes et, si C est plus grand que 10 (mettons qu'il vaille 11 ), il est, effectivement plus grand que 5 et plus grand ou égal à 0

    Tu peux donc parfaitement envisager de faire les trois vérifications, même si elles prennent du temps "inutile" de travail, car seule la vérification "finale" (i>10 pour un type C ) s'avère intéressante
    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

  14. #14
    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
    On rentre en mode chipotage, mais j'ai compris qu'à la base c'était le problème cumulatif des pré/post-conditions qui devenait difficile à exprimer. Si à chaque niveau, tu rajoutes une condition orthogonale aux précédentes, tu affines sans redondance.

  15. #15
    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
    Mais tu remarquera que les conditions restent cohérentes et, si C est plus grand que 10 (mettons qu'il vaille 11 ), il est, effectivement plus grand que 5 et plus grand ou égal à 0
    Non, c'est très très embêtant justement.

    suppose le pseudo-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
    class A { 
      virtual void f(int i) require i > 0 {...}
    };
     
    class B : public A {
      virtual void f(int i) require i > 5 {...}
    }
     
    void g(A& param1, int param2)
    {
      if(param2 > 0) // je vérifie que je respecte le contrat pour appeler f
        param1.f(param2);
    }
     
    int main(int,char**)
    {
       B monB;
       g(monB, 3); // plantage au runtime, car le contrat n'est pas respecté *pour un B*
    }
    Le problème, c'est que j'ai vérifié mon contrat dans g, donc le bug n'est pas dans g. Il est dans B.f, qui ne respecte pas le contrat de A.f, dont elle hérite. B.f a le droit d'accepter plus que A.f, mais pas moins (sinon, elle est inutilisable car on a besoin de toujours connaître le type dynamique pour vérifier le contrat)

  16. #16
    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 white_tentacle Voir le message
    B.f a le droit d'accepter plus que A.f, mais pas moins (sinon, elle est inutilisable car on a besoin de toujours connaître le type dynamique pour vérifier le contrat)
    C'est diablement convainquant. Mais, à ce moment, tu ne risque pas de violer le principe de substitution de Liskov ? Tu ne pourras remplacer un A par B car il ne respecterait pas la pré-condition plus stricte sur i ?

    [EDIT]
    Après lecture, effectivement on trouve sur wiki :
    * Les préconditions ne peuvent pas être renforcées dans une sous-classe. Cela signifie que vous ne pouvez pas avoir une sous-classe avec des préconditions plus fortes que celles de sa superclasse.
    * Les postconditions ne peuvent pas être affaiblies dans une sous-classe. Cela signifie que vous ne pouvez pas avoir une sous-classe avec des postconditions plus faibles que celles de sa superclasse.

  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 white_tentacle Voir le message
    Non, c'est très très embêtant justement.

    suppose le pseudo-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
    class A { 
      virtual void f(int i) require i > 0 {...}
    };
     
    class B : public A {
      virtual void f(int i) require i > 5 {...}
    }
     
    void g(A& param1, int param2)
    {
      if(param2 > 0) // je vérifie que je respecte le contrat pour appeler f
        param1.f(param2);
    }
     
    int main(int,char**)
    {
       B monB;
       g(monB, 3); // plantage au runtime, car le contrat n'est pas respecté *pour un B*
    }
    Le problème, c'est que j'ai vérifié mon contrat dans g, donc le bug n'est pas dans g. Il est dans B.f, qui ne respecte pas le contrat de A.f, dont elle hérite. B.f a le droit d'accepter plus que A.f, mais pas moins (sinon, elle est inutilisable car on a besoin de toujours connaître le type dynamique pour vérifier le contrat)
    N'oublie pas que le polymorphisme s'applique aussi lorsque l'on utilise les références

    Dans l'exemple que tu donne, si tu a décidé que le paramètre ne devait pas être inférieur à 5 pour un B, tu commet une erreur en voulant appeler la fonction en lui passant 3, et il est donc normal, considérant que tu avais une raison valable de dire que le paramètre devait valoir minimum 5 que cela t'emmene vers une issue de non exécution
    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
    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
    C'est diablement convainquant.
    Ce n'est pas moi qui l'ai inventé .

    Mais, à ce moment, tu ne risque pas de violer le principe de substitution de Liskov ? Tu ne pourras remplacer un A par B car il ne respecterait pas la pré-condition plus stricte sur i ?
    C'est exactement ça. C'est pour cela que la précondition de B doit être moins stricte que celle de A, et les post-conditions plus strictes. Les règles d'héritage de contrat te garantissent le LSP.

    il est donc normal, considérant que tu avais une raison valable de dire que le paramètre devait valoir minimum 5 que cela t'emmene vers une issue de non exécution
    Oui. Mon problème, c'est que g prend un A&. Ce A& peut être un B, mais aussi un C ou un D, et ça g n'en sait rien du tout. Du coup, dans G, je ne suis sûr que d'une chose, c'est que j'ai un A. Je ne peux vérifier le contrat que par rapport à celui de A::f(). Je suis totalement incapable de vérifier autre chose.

  19. #19
    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 white_tentacle Voir le message
    Oui. Mon problème, c'est que g prend un A&. Ce A& peut être un B, mais aussi un C ou un D, et ça g n'en sait rien du tout. Du coup, dans G, je ne suis sûr que d'une chose, c'est que j'ai un A. Je ne peux vérifier le contrat que par rapport à celui de A::f(). Je suis totalement incapable de vérifier autre chose.
    Tu est parfaitement capable de vérifier que le contrat est respecté, si tu applique les bonnes pratiques pour la vérification de tes conditions

    Ainsi, si tu prévois une exception mettons BadRange qui sera lancée si la condition n'est pas respectée, tu peux en arriver à un code proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    void g(A& param, int p2)
    {
        /* ce qui peut éventuellement être fait d'office */
        try
        {
            /* tout ce qui risque de lancer une exception, dont */
            param.f(p2);
        }
        catch(BadRange& e)
        {
            /* ce qu'il faut faire si l'exception a été lancée */
        }
        /* ce qui peut être fait, même si une exception a été lancée */
    }
    Pour en revenir au principe de liskov, il s'applique aux comportements non polymorphes, car, sinon, il est clair qu'il n'est jamais respecté
    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

  20. #20
    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
    Tu est parfaitement capable de vérifier que le contrat est respecté, si tu applique les bonnes pratiques pour la vérification de tes conditions

    Ainsi, si tu prévois une exception mettons BadRange qui sera lancée si la condition n'est pas respectée, tu peux en arriver à un code proche de
    C'est autre chose, et n'a plus à voir avec la programmation par contrats ;-). Cela dit, tu ne vérifies pas dans l'appelant ton contrat, tu appelles sans rien vérifier, et tu laisses l'appelé faire du defensive programming et vérifier que tu l'as appelé correctement. Ça marche parce que tu le fais bien, mais que se passe-t-il si une classe E dérivant de A décidait, parce qu'elle en a envie, de ne plus lancer BadRange, mais InvalidParameterValue ?

    Pour en revenir au principe de liskov, il s'applique aux comportements non polymorphes
    Là je ne te comprends pas du tout. Où est la notion de "substitution", si tu enlèves le polymorphisme ?

    car, sinon, il est clair qu'il n'est jamais respecté
    Si tu fais de la programmation par contrats, il est obligatoirement respecté !

Discussions similaires

  1. redéfinition de méthode
    Par greg08 dans le forum Langage
    Réponses: 8
    Dernier message: 28/10/2008, 14h59
  2. Réponses: 8
    Dernier message: 01/07/2008, 23h15
  3. héritage et redéfinition de méthodes
    Par Braillane dans le forum Langage
    Réponses: 4
    Dernier message: 07/01/2007, 18h47
  4. redéfinition de méthode et invocation
    Par magedo dans le forum Langage
    Réponses: 2
    Dernier message: 05/01/2007, 19h00
  5. [JPanel][LayoutManager] redéfinition de méthodes
    Par _KB_ dans le forum AWT/Swing
    Réponses: 4
    Dernier message: 06/06/2006, 14h22

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