IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
logo
Sommaire > L'héritage
        Pourquoi mettre en œuvre un héritage ?
        Quand dois-je faire un héritage public ? protégé ? privé ?
        Qu'est-ce que le LSP ?
        Héritage EST-UN et programmation par contrat.
        Pourquoi le destructeur d'une classe de base doit être public et virtuel ou protégé et non virtuel ?
        Qu'est-ce qu'une classe abstraite ?
        Qu'est-ce que l'héritage virtuel et quelle est son utilité ?
        Dans quel ordre sont construits les différents composants d'une classe ?
        Qu'est-ce que le polymorphisme ?
        Mes fonctions virtuelles doivent-elles être publiques, protégées, ou privées ? Le pattern NVI.
        Comment varier le comportement au moment de l'exécution par le polymorphisme d'inclusion ?
        Puis-je appeler des fonctions virtuelles dans le constructeur (ou le destructeur) ?
        Que sont le typage statique et le typage dynamique ? Question subsidiaire : qu'est-ce que l'inférence de type ?



Pourquoi mettre en œuvre un héritage ?
Créé le 15/10/2009[haut]
auteur : 3DArchi
On trouve 2 sémantiques liées à l'héritage :

  • EST-IMPLEMENTE-EN-TERMES-DE (IS-IMPLEMENTED-IN-TERM-OF) :
    Ce type d'héritage permet à la classe dérivée de tirer profit de l'implémentation de la classe de base. Si B dérive de A avec cette sémantique, alors toutes les fonctions de B peuvent appeler les fonctions de A. Le service apporté par la classe A est disponible dans la classe B. Une alternative à l'héritage 'EST-IMPLEMENTE-EN-TERMES-DE' est la composition. B possède un membre de type A et invoque ses fonctions au besoin. On peut préférer l'héritage lorsque la classe de base est vide (sans attribut) et que l'on souhaite bénéficier de l'optimisation des classes de base vides, ou lorsqu'il est nécessaire de redéfinir une fonction virtuelle de la classe de base. La composition permet de varier l'implémentation plus facilement.

  • EST-UN (IS-A) :
    La sémantique de cet héritage découle de la définition du sous-type par Liskov et est donc intrinsèquement lié au principe faq  LSP (Liskov substitution principle).
    Si le qualificatif 'EST-UN' est assez explicite, comprendre ses implications est parfois moins évident. "B dérive de A" avec cette sémantique a diverses conséquences. D'abord, on dit que B est un sous-type de A. Ensuite, tout objet de type A dans une expression valide peut être remplacé par un objet de type B : B doit garantir que l'expression reste valide ET qu'elle possède la même sémantique. Enfin, définir un sous-type n'est pas définir un type plus restrictif : B doit respecter tout ce que respecte A mais peut faire des choses en plus ou différemment.

A noter que les deux types d'héritage ne sont pas mutuellement exclusifs : B peut dériver de A à la fois car il EST-UN A et à la fois car il EST-IMPLEMENTE-EN-TERMES-DE A.

lien : faq Qu'est-ce que le LSP ?

Quand dois-je faire un héritage public ? protégé ? privé ?
Créé le 15/10/2009[haut]
auteur : 3DArchi
  • public : uniquement si l'héritage porte une sémantique EST-UN ;
  • privé : lorsque l'héritage porte une sémantique EST-IMPLEMENTE-EN-TERMES-DE et ne supporte pas la sémantique EST-UN ;
  • protégé : si vous avez une bonne raison, la rédaction sera curieuse de la connaître.
L'héritage public est le plus problématique car il doit respecter le LSP et notamment ses implications en termes de contrat.

lien : faq Qu'est-ce que le LSP ?
lien : faq Pourquoi mettre en œuvre un héritage ?

Qu'est-ce que le LSP ?
Créé le 15/10/2009[haut]
auteur : Davidbrcz
Le LSP (pour Liskov substitution principle) est un principe général de programmation s'énonçant de la façon suivante :

Partout où un objet x de type T est attendu, on doit pouvoir passer un objet y type U, avec U héritant de T.
En reformulant en français cette proposition, cela veut dire que l'on doit pouvoir remplacer les objets d'une classe T par n'importe quel objet d'une sous-classe de T.
Comment cela se traduit-il sur le contrat de la classe ?
Sur les invariants des fonctions membres, les préconditions doivent être plus faibles et les postconditions doivent être plus fortes.
En effet, l'héritage est l'exposition d'une interface que les sous-classes vont affiner. Dès lors, toutes les fonctions membres d'une sous-classe doivent pouvoir travailler sur des objets acceptés selon l'interface de la classe parente et fournir un résultat au moins aussi sûr que celui de la classe parente.
On peut faire une analogie avec un sous-traitant. Un sous-traitant doit accepter tous les travaux que vous acceptez (et même plus s'il le veut) et doit rendre un travail au moins aussi sûr que le votre et même plus s'il le veut.
Sur les invariants de la classe, cela se taduit par le fait qu'une classe dérivée ne peut qu'ajouter des invariants à sa définition.
En effet, l'héritage public est une relation EST-UN. Quand Y dérive de X, Y EST-UN X. Dès lors, il doit vérifier tous les invariants de X plus ceux qui lui sont propres.
A ce titre examinons le code suivant :
class rectangle 
{
protected:
        double width;
        double height;

public:
    virtual void set_width(double x){width=x;}
    virtual void set_height(double x){height=x;}
    double area() const {return width*height;}
   
};

class square : public rectangle
{
/*L'invariant d'un carré est que à tout moment, width=height*/
    void INVARIANT() {assert(width==height);}
public:
    void set_width(double x){
        INVARIANT(); 
        rectangle::set_width(x);
    rectangle::set_height(x);  
        INVARIANT();
        }
    void set_height(double x)
        {
        INVARIANT();
        rectangle::set_width(x);
    rectangle::set_height(x);
        INVARIANT();
        }
};

void foo(rectange& r)
{
 r.set_height(4);
 r.set_width(5);
 assert(r.area() == 20);
}
Si nous passons un carré à foo, l'assertion est fausse, le contrat est rompu, le LSP est bafoué. Que s'est-il passé ?
Un carré est bien est un rectangle d'un point de vue mathématique mais pas sur le plan du comportement logiciel. Et c'est ce qui importe dans la programmation. Le comportement d'un carré N'EST PAS identique à celui d'un rectangle. On peut supposer que dans un rectangle, longeur et largeur vont varier indépendamment l'une de l'autre. Changer l'une ne doit pas changer l'autre, ce qui est intrinsèquement faux pour un carré.
Le carré n'est pas substituable au rectangle, il ne devrait donc pas hériter de la classe rectangle.
Il se passe le même problème entre une liste et une liste ordonée. Les deux classes n'ont pas les mêmes invariants pour l'insertion.
Dans une liste, on peut insérer un objet où l'on veut, pas dans une liste triée. Des assertions valides pour la liste ne le sont plus pour une liste triée.

lien : faq Pourquoi mettre en œuvre un héritage ?

Pourquoi le destructeur d'une classe de base doit être public et virtuel ou protégé et non virtuel ?
Créé le 15/10/2009[haut]
auteur : 3DArchi
C'est un peu la question symétrique de la sémantique d'héritage, vue de la classe de base.
Avoir un destructeur public signifie qu'un objet de type statique A peut être détruit alors que son type dynamique est B, un sous-type de A :
class A
{// Déclaration de A...
};
class B : public A
{// Déclaration de B...
};

//...
// Quelque part dans le code :
{
   std::auto_ptr<A> ptr_a(new B);
}// Destruction à partir du type statique A avec un type dynamique B !
Alors pour que cette faq destruction soit correcte, le destructeur de A doit être virtuel. A partir du moment où une classe peut être dérivée et que vous ne savez pas à l'avance comment elle sera ensuite utilisée, vous devez traiter ce problème. Or il n'existe que deux solutions :

  • le destructeur est public et virtuel : vous autorisez sans risque qu'un objet dérivé soit détruit à partir d'une variable de type statique de l'objet de base.
  • le destructeur est protégé et non virtuel : le destructeur d'un objet de type de base ne peut être appelé que par le destructeur du type dérivé. Plus de risque qu'un objet de type statique A tente de détruire un objet de type dynamique B.
lien : faq Pourquoi et quand faut-il créer un destructeur virtuel ?

Qu'est-ce qu'une classe abstraite ?
Mise à jour le 22/10/2004[haut]
auteurs : LFE, Aurélien Regat-Barrel
Une classe abstraite est une classe qui possède au moins une fonction membre virtuelle pure (lire Qu'est-ce qu'une fonction virtuelle pure ?). Cette fonction devant être supplantée, ce type de classe ne peut pas être instancié, et est donc destiné à être dérivé pour être spécialisé. La ou les classes filles doivent supplanter l'ensemble des fonctions virtuelles pures de leurs parents. On dit alors que les classes filles concrétisent la classe abstraite.
class Bienvenue // classe abstraite
{
public:
    // le "= 0" à la fin indique que c'est
    // une fonction virtuelle pure
    virtual void Message() = 0;
};

class BienvenueEnFrancais : public Bienvenue
{
public:
    void Message()
    {
        std::cout << "Bienvenue !\n";
    }
};

class BienvenueEnAnglais : public Bienvenue
{
public:
    void Message()
    {
        std::cout << "Welcome !\n";
    }
};
lien : Qu'est-ce qu'une fonction virtuelle pure ?

Qu'est-ce que l'héritage virtuel et quelle est son utilité ?
Créé le 15/10/2009[haut]
auteur : Davidbrcz
Le C++ est un langage qui autorise l'héritage multiple, c'est-à-dire qu'une classe peut avoir plus d'un parent. Exemple :
class A{};
class B{};
class C: public A,public B{};
Les classes parentes de C sont A et B.
Imaginons le cas suivant :
class V { protected:int i;/* ... */ };
class A :  public V { /* ... */ };
class B :  public V { /* ... */ };
class C : public A, public B { /* ... */ };
Le schéma d'héritage est le suivant :

Héritage en V
La classe A contient une variable membre i. Il en va de même pour la classe B. En ce qui concerne la classe C, elle possède 2 variables membres i, l'une par l'héritage par A, l'autre selon l'héritage de B, c'est ce qui provoque une erreur si on tente d'accéder à i dans C, le compilateur ne sait pas s'il doit regarder A::i ou B::i.
Pour vous convaincre de la présence de deux variables i dans la classe C, compilez le code suivant :
void C::f(){std::cout<<&(A::i)<<"/"<<&(B::i)<<std::endl;}
Dans bon nombre de cas, il va être génant (selon le point de vue de l'utilisation mémoire) d'avoir tous les membres dédoublés.
La solution que propose le C++ est alors l'héritage virtuel. A hérite virtuellement de V et pour B il en va de même. Exemple :
class V 
{ 
   protected:int i;
   /* ... */ 
};
class A : virtual public V 
{ /* ... */ };
class B : virtual public V 
{ /* ... */ };
class C : public A, public B 
{ /* ... */ };
Le fonctionnement de A ou B n'est pas changé, ils héritent toujours de V mais il n'y a plus qu'un seul objet V dans C.
Pour preuvre :
void C::f(){std::cout<<&(A::i)<<"/"<<&(B::i)<<std::endl;}
affiche deux fois la même chose et il n'y a plus d'ambiguité sur i. L'héritage est alors (on parle d'héritage en losange ou en diamant):

Héritage en diamant
Notez par contre qu'on peut toujours introduire un nouvel objet V dans C de la façon suivante :
class C : public A, public B, public V 
{ /* ... */ };
Dans ce cas, la structure de l'héritage est la suivant :

Héritage en diamant et en V

Dans quel ordre sont construits les différents composants d'une classe ?
Créé le 15/10/2009[haut]
auteur : 3DArchi
Les constructeurs sont appelés dans l'ordre suivant :

  1. le constructeur des classes de base héritées virtuellement en profondeur croissante et de gauche à droite ;
  2. le constructeur des classes de base héritées non virtuellement en profondeur croissante et de gauche à droite ;
  3. le constructeur des membres dans l'ordre de leur déclaration ;
  4. le constructeur de la classe.
 
#include <iostream>
#include <string>
 
struct MembreA{
   MembreA(){std::cout<<"MembreA"<<std::endl;}
};
struct A {
   A(){std::cout<<"A"<<std::endl;} 
   MembreA m;
};
struct MembreB{
   MembreB(){std::cout<<"MembreB"<<std::endl;}
};
struct B : A {
   B(){std::cout<<"B"<<std::endl;} 
   MembreB m;
};
struct MembreC{
   MembreC(){std::cout<<"MembreC"<<std::endl;}
};
struct C : A {
   C(){std::cout<<"C"<<std::endl;}
   MembreC m;
};
struct MembreD{
   MembreD(){std::cout<<"MembreD"<<std::endl;}
};
struct D : B, C {D(){
   std::cout<<"D"<<std::endl;}
   MembreD m;
};
struct MembreE{MembreE(){
   std::cout<<"MembreE"<<std::endl;}
};
struct E : virtual A {E(){
   std::cout<<"E"<<std::endl;}
   MembreE m;
};
struct MembreF{MembreF(){
   std::cout<<"MembreF"<<std::endl;}
};
struct F : virtual A {
   F(){std::cout<<"F"<<std::endl;}
   MembreF m;
};
struct MembreG{
   MembreG(){std::cout<<"MembreG"<<std::endl;}
};
struct G {
   G(){std::cout<<"G"<<std::endl;}
   MembreG m;
};
struct MembreH{
   MembreH(){std::cout<<"MembreH"<<std::endl;}
};
struct H : G, F {
   H(){std::cout<<"H"<<std::endl;}
   MembreH m;
};
struct MembreI{
   MembreI(){std::cout<<"MembreI"<<std::endl;}
};
struct I : E, G, F {
   I(){std::cout<<"I"<<std::endl;}
   MembreI m;
};
 
 
template<class T> void Creation()
{
   std::cout<<"Creation d'un "<<typeid(T).name()<<" : "<<std::endl;
   T t;
}
 
int main()
{
   Creation<A>();
   Creation<B>();
   Creation<C>();
   Creation<D>();
   Creation<E>();
   Creation<F>();
   Creation<G>();
   Creation<H>();
   Creation<I>();
   return 0;
}
 
Ce code produit comme sortie :
 
Creation d'un struct A :
MembreA
A
Creation d'un struct B :
MembreA
A
MembreB
B
Creation d'un struct C :
MembreA
A
MembreC
C
Creation d'un struct D :
MembreA
A
MembreB
B
MembreA
A
MembreC
C
MembreD
D
Creation d'un struct E :
MembreA
A
MembreE
E
Creation d'un struct F :
MembreA
A
MembreF
F
Creation d'un struct G :
MembreG
G
Creation d'un struct H :
MembreA
A
MembreG
G
MembreF
F
MembreH
H
Creation d'un struct I :
MembreA
A
MembreE
E
MembreG
G
MembreF
F
MembreI
I
 
Remarque 1 : La subtilité réside dans la primauté accordée à l'héritage virtuel sur l'héritage non virtuel.
Remarque 2 : L'ordre de construction est fixé par la norme et ne dépend pas des listes d'initialisation du code :
 
struct Membre1
{
   Membre1(){std::cout<<"Membre1"<<std::endl;}
};
struct Membre2
{
   Membre2(){std::cout<<"Membre2"<<std::endl;}
};
struct A {
   A(){std::cout<<"A"<<std::endl;} 
};
struct B {
   B(){std::cout<<"B"<<std::endl;} 
};
struct C : A,B {
   C()
      :m2(),B(),m1(),A()
   {std::cout<<"C"<<std::endl;}
   Membre1 m1;
   Membre2 m2;
};
int main()
{
   C c;
   return 0;
}
 
Ce code produit :
 
A
B
Membre1
Membre2
C
 
Certains compilateurs peuvent sortir un avertissement lorsque la liste d'initialisation ne suit pas l'ordre de déclaration mais ce n'est pas toujours le cas. Pour les listes d'initialisation, une bonne pratique est de toujours suivre l'ordre défini par la norme pour éviter tout risque de confusion.
Remarque 3 : Pour l'héritage virtuel, le constructeur appelé est celui spécifié par le type effectivement instancié et non par celui spécifié par le type demandant l'héritage. Si le type instancié ne spécifie pas de constructeur, alors c'est celui par défaut :
 struct A
{
   A(std::string appelant_="defaut")
   {
      std::cout<<"A construit par "<<appelant_<<std::endl;
   }
};
 
struct B : virtual A
{
   B()
      :A("B")
   {
   }
};
 
struct C : B
{
   C()
   {
   }
};
 
struct D : B
{
   D()
      :A("D")
   {
   }
};
 
template<class T> void Creation()
{
   std::cout<<"Creation d'un "<<typeid(T).name()<<" : "<<std::endl;
   T t;
}
 
int main()
{
   Creation<B>();
   Creation<C>();
   Creation<D>();
   return 0;
} 
Ce code produit :
 Creation d'un struct B :
A construit par B
Creation d'un struct C :
A construit par defaut
Creation d'un struct D :
A construit par D 
Conclusion :
Le constructeur d'une classe doit monter sa liste d'initialisation suivant cet ordre :

  1. les constructeurs des classes héritées virtuellement dans tout l'arbre d'héritage en profondeur croissante et de gauche à droite ;
  2. les constructeurs des classes de base directement héritées dans l'ordre de gauche à droite ;
  3. les membres dans l'ordre de leur déclaration.
Ceci a comme conséquences :

  • Ce sont d'éventuelles contraintes dans l'ordre de construction qui imposeront l'ordre d'héritage (et non des approches de type d'abord le public, puis le privé).
  • Toute dépendance de construction entre les variables membres devra être explicitement commentée à défaut de pouvoir être évitée. Par cette documentation, les lecteurs du code sont avertis qu'il s'agit d'un comportement compris et maîtrisé par le développeur : la fiabilitié est accrue et la maintenance est facilitée.

Qu'est-ce que le polymorphisme ?
Créé le 15/10/2009[haut]
auteurs : Jean-Marc.Bourguet, 3DArchi
Le polymorphisme, c'est la capacité d'une expression à être valide quand les valeurs présentes ont des types différents. On trouve différents types de polymorphismes :

  • ad-hoc : surcharge et coercition ;
  • universel (ou non ad-hoc) : paramétrique et d'inclusion.
Ces deux distinctions segmentent le polymorphisme suivant l'axe de réutilisabilité face à un nouveau type : le polymorphisme ad-hoc nécessite une nouvelle définition pour chaque nouveau type alors que le polymorphisme universel recouvre un ensemble potentiellement illimité de types.

lien : en  On Understanding Types,Data Abstraction, and Polymorphism, de Luca Cardelli
lien : fr  Types et polymorphisme, de Jean-Marc Bourguet
lien : fr  Connaissance des langages de programmation, de Ph. Narbel (Cours de MASTER 1 BORDEAUX 1 (2006-2007) ),
lien : faq Qu'est-ce que la coercition ?
lien : faq Qu'est-ce que la surcharge ?
lien : faq Qu'est-ce que le polymorphisme paramétrique ?
lien : faq Qu'est-ce que le polymorphisme d'inclusion ?

Mes fonctions virtuelles doivent-elles être publiques, protégées, ou privées ? Le pattern NVI.
Créé le 15/10/2009[haut]
auteur : 3DArchi
Vous avez sans doute déjà croisé des bibliothèques - ou en avez fait vous-même - proposant une interface à base de fonctions virtuelles :

class IInterface
{
public :
   virtual void Action(); // éventuellement pure (=0)
};
 
A charge pour le client de proposer une classe concrète spécialisant l'interface en redéfinissant ces fonctions virtuelles.
Le pattern NVI - Non Virtual Interface - propose une autre approche pour la définition de telles interfaces dont le principe est : l'interface est proposée en fonction non virtuelle publique; la variabilité est encapsulée dans les fonctions virtuelles privées :

class IInterface
{
public :
   void Action();
 
private:
   virtual void DoAction(); // =0
};
Action appelle DoAction à un moment pour rendre le service attendu.
Une fonction virtuelle privée ne peut être appelée par les classes dérivées. En revanche, une classe dérivée peut redéfinir la fonction pour adapter le comportement. La nécessité par la classe dérivée d'appeler l'implémentation de la classe parente est de part le pattern assez exceptionnelle. C'est pourquoi la fonction est privée et non protégée. Dans les cas exceptionnels où la classe de base peut proposer un comportement intéressant pour les classes dérivées, DoAction peut être protected.
La réponse à notre question première, "Mes fonctions virtuelles doivent-elles être publiques, protégées, ou privées ?", devient avec ce pattern :

  • publique : jamais ;
  • protégées : exceptionnellement ;
  • privées : par défaut.
Ceci ne concerne pas le destructeur dont la problématique est envisagée dans une autre question (cf faq  pourquoi le destructeur d'une classe de base doit être public et virtuel ou protégé et non virtuel ?).
"Quel est l'intérêt ?" est la première question qui se pose !
D'abord, il faut comprendre que la définition d'une interface s'adresse en fait à deux interlocuteurs bien distincts : la classe client qui utilise IInterface pour son service Action et la classe concrète qui dérive de IInterface et réalise Action en la redéfinissant. On voit donc par là qu'avec la première approche, Action se voit conférer deux rôles distincts. Séparer les deux fonctions permet ainsi d'indiquer clairement à chacun des deux interlocuteurs sa responsabilité. Il faut se souvenir qu'une bonne conception ne donne qu'une responsabilité bien définie à un élément. Octroyer de multiples responsabilités est souvent source de rigidité (problème de réutilisabilité), de confusion (pensez toujours à la maintenance) et de bugs (maîtrise de la complexité).
Dans une approche par contrat, IInterface passe un contrat avec le client sur la fonction Action : le client assure les préconditions s'il veut obtenir les postconditions. Et réciproquement, IInterface assure les préconditions de DoAction pour obtenir les postconditions :
void IInterface::Action()
{
   // mise en place des préconditions de DoAction
  // éventuellement en mode debug, tests des préconditions
  DoAction();
  // éventuellement en mode debug, tests des postconditions
  // traitements supplémentaires si besoin pour garantir les postconditions de Action()
}
En fait, ce découpage assure que les préconditions et postconditions pour Action ne peuvent être dévoyées par la classe dérivée. En effet, le client s'adresse toujours à la fonction de la classe de base, et a donc comme contrat celui proposé par IInterface. La classe dérivée ne passe contrat qu'avec la classe mère via DoAction. Le développeur d'une classe dérivée ne peut pas modifier ces pré/postconditions offertes au client puisqu'elles restent garanties par la classe de base IInterface::Action.
En assignant à chacun sa responsabilité, cette séparation accroit la souplesse de l'interface face aux évolutions :

  • Le service offert au client via Action peut évoluer de façon plus lâche par rapport à l'implémentation proposée par la classe concrète via DoAction.
  • L'implémentation de DoAction dans la classe concrète peut évoluer en réduisant les impacts sur les clients de Action. Les détails d'implémentation peuvent évoluer par la spécialisation ou par la mise en oeuvre d'autres mécanismes (pimpl idiom, patron de conception pont, etc.) sans que cela n'affecte le client.
Cette séparation crée aussi un endroit idéal pour l'instrumentation d'un code. La fonction non virtuelle Action peut accueillir les demandes de trace, peut surveiller les performances, etc. En mode debug, Action peut aussi tester les invariants, s'assurer des préconditions et garantir les postconditions.

void IInterface::Action()
{
  Log::Report("Entrée IInterface::Action");
  Duree temps(Duree::maintenant);
  DoAction1();
  temps.Stop();
  Log::Report("Temps d'exécution : ", temps.LireDuree());
}
De façon connexe, le patron de conception Patron de méthode (design pattern template) s'appuie sur les fonctions virtuelles pour paramétrer un comportement. De façon encore plus évidente que pour une interface, les fonctions virtuelles doivent se trouver en zone privée. Ici, ces fonctions ne relèvent pas du contrat de la classe avec son client mais bien des détails d'implémentation.

void IInterface::Action()
{
  // Première partie de traitements
  DoAction1();
  // Traitements suivants
  DoAction2();
  // Traitements suivants..
  DoAction3();
  // fin des traitements
}
avec alors :

class IInterface
{
public :
   void Action();
 
private:
   virtual void DoAction1(); // =0
   virtual void DoAction2(); // =0
   virtual void DoAction3(); // =0
};
lien : en Virtuality, de Herb Sutter (gotw)
lien : faq Pourquoi le destructeur d'une classe de base doit être public et virtuel ou protégé et non virtuel ?

Comment varier le comportement au moment de l'exécution par le polymorphisme d'inclusion ?
Créé le 15/10/2009[haut]
auteurs : Alp Mestan, 3DArchi
En C++, on utilise souvent l'héritage pour ce faire. En effet, imaginez que nous soyons en présence d'une hiérarchie de composants graphiques, dont la classe de base serait Widget. On aurait ainsi Button et Textfield qui hériteraient de Widget par exemple. Enfin, chacun possèderait une méthode show() qui permet d'afficher le composant en question. Bien entendu, un Button et un Textfield étant de natures différentes, leur affichage le serait aussi. C'est grâce au polymorphisme d'héritage, mis en oeuvre en C++ grâce au mot clé virtual, que l'on peut réaliser cela dynamiquement : à l'exécution du programme, il sera choisi d'utiliser la méthode Button::show() ou la méthode Textfield::show() selon le type réel de l'objet sur lequel on appelle show(). Voici un exemple minimal illustrant cela.
class Widget
{
  public:
  virtual ~Widget() { /* ... */ }
  void show()
  {
    // ...
    do_show();
    // ...
  }
  // ...

  private : 
    virtual void do_show()=0; // fonction virtuelle pure
};

class Button : public Widget
{
  private : 
    virtual void do_show() { std::cout << "Button" << std::endl; }
  // ...
};

class Textfield : public Widget
{
  private : 
    virtual void do_show() { std::cout << "Textfield" << std::endl; }
  // ...
};

void show_widget(Widget& w)
{
  w.show();
}

// ...

Button b;
Textfield t;

show_widget(b); // affiche "Button"
show_widget(t); // affiche "Textfield"
Dans ce cas, rien à redire, vous avez fait un choix correct.


Puis-je appeler des fonctions virtuelles dans le constructeur (ou le destructeur) ?
Créé le 15/10/2009[haut]
auteurs : JolyLoic, Laurent Gomila
Oui, c'est possible, mais attention, ça ne fait pas toujours ce qu'on pense.
La première approche consiste à comprendre que lors de l'appel du constructeur d'une classe de base, la classe dérivée n'a pas encore été construite. Donc, c'est la méthode spécialisée à ce niveau qui est appelée.
Voyons en détail :
La règle est que le type dynamique d'une variable en cours de construction est celui du constructeur qui est en train d'être exécuté. Pour bien comprendre ce qui se passe, il faut donc revenir sur la différence entre le type statique d'une variable, et son type dynamique.

Prenons par exemple trois classes, C qui dérive de B qui dérive de A. Par exemple, dans :
A* a = new B();
La variable a possède comme type statique (son type déclaré dans le programme) A*. Par contre, son type dynamique est B*. Une fonction virtuelle est simplement une fonction dont on va chercher le code en utilisant le type dynamique de la variable, au lieu de son type statique, comme une fonction classique.

Maintenant, quand on crée un objet de type C, les choses se passent ainsi :

  • On alloue assez de mémoire pour un objet de la taille de C.
  • On initialise la sous partie correspondant à A de l'objet.
  • On appelle le corps du constructeur de A. Pendant cet appel, l'objet crée a pour type dynamique A.
  • On initialise la sous partie correspondant à B de l'objet.
  • On appelle le corps du constructeur de B. Pendant cet appel, l'objet crée a pour type dynamique B.
  • On initialise la sous partie correspondant à C de l'objet.
  • On appelle le corps du constructeur de C. Pendant cet appel, l'objet crée a pour type dynamique C.
Donc, dans le corps du constructeur de la classe B, un appel d'une fonction virtuelle appellera la version de la fonction définie dans la classe B (ou à défaut celle définie dans A si la fonction n'a pas été définie dans B), et non pas celle définie dans la classe C.

D'ailleurs, si la fonction est virtuelle pure dans B, ça causera quelques problèmes, puisqu'on tentera alors d'appeler une fonction qui n'existe pas. En général, le programme va planter, si on a de la chance, il affichera une message du style "Pure function called".

La problèmatique est exactement la même pour les destructeurs, mais dans l'ordre inverse.

Pourquoi cette règle ? Une fonction définie dans C a accès aux données membre de C. Or, on a vu que au moment où on exécute l'appel au corps du constructeur de B, ces dernières ne sont pas encore créées. On a donc préféré jouer la sécurité.

lien : faq Dans quel ordre sont construits les différents composants d'une classe ?

Que sont le typage statique et le typage dynamique ? Question subsidiaire : qu'est-ce que l'inférence de type ?
Créé le 15/10/2009[haut]
auteur : Davidbrcz
Le C++ est un langage typé, c'est-à-dire que toute variable possède au moment de sa définition un type connu par le compilateur : c'est le typage statique. Mais le type réel de l'objet peut être différent de son type statique. C'est ce qui se passe lors du polymorphisme dynamique.

class A{};
class B:public A {};
 
A* ptr= new B; // le type statique l'objet est A mais son type dynamique est B
Le type statique est l'interface par laquelle on manipule l'objet réellement derrière ce type. Pour déterminer ce type, vous pouvez regarder du coté de typeid et typeinfo, bien que souvent, avoir besoin de connaître le type réel de l'objet est signe d'une conception bancale.
Regardons la différence entre le typage statique/dynamique et le typage faible qui est présent dans bien des langages.

x = 10;
x = "Hello World";
La première instruction va assigner la valeur 10 à la variable x. La pensée de l'interpréteur est simpliste : "La variable contient un numérique, elle doit donc se comporter comme un nombre numérique". La seconde va lui assigner la chaîne "Hello World", x va donc se comporter comme une chaîne de caractères.
Dans les deux cas, x n'est ni de type numérique ni de type chaîne de caractères, c'est une juste variable qui contient une valeur se comportant d'une certaine façon. C'est le typage faible.
L'inférence de type est autre chose. Elle consiste à détecter automatiquement le type statique d'une variable, sans que celui-ci ne soit explicité dans le code source via le mot clé auto.
Néanmoins, cette détection possède ses propres limites, le type assigné va au plus simple possible :


auto s="blabla"; //ici s est de type const char* alors qu'on aurait voulu l'avoir de type std::string
lien : faq Qu'est-ce que le LSP ?


Consultez les autres F.A.Q.


Valid XHTML 1.0 TransitionalValid CSS!

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2008 Developpez LLC. Tous droits réservés Developpez LLC. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents et images sans l'autorisation expresse de Developpez LLC. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.