Developpez.com - Rubrique C++

Le Club des Développeurs et IT Pro

Débat : Redéfinition de méthode et principe de substitution de Liskov

Le 2009-02-03 20:29:42, par Ekinoks, Membre averti
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 :
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.
  Discussion forum
68 commentaires
  • koala01
    Expert éminent sénior
    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 :
    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.
  • Ekinoks
    Membre averti
    Merci pour ta reponse koala

    Envoyé par koala01
    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"

    Envoyé par koala01
    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

    Envoyé par koala01
    3-
    Erf, ok... c'est bien ce que je craignais :'(
  • koala01
    Expert éminent sénior
    Envoyé par Ekinoks
    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
  • white_tentacle
    Membre émérite
    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.
  • koala01
    Expert éminent sénior
    Envoyé par white_tentacle
    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
  • 3DArchi
    Rédacteur
    Envoyé par koala01
    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 ?
  • koala01
    Expert éminent sénior
    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 :
    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"
  • white_tentacle
    Membre émérite
    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).
  • koala01
    Expert éminent sénior
    Envoyé par white_tentacle
    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é
  • 3DArchi
    Rédacteur
    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)...