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

Les fonctions virtuelles en C++ : Types statiques et types dynamiques


précédentsommairesuivant

I. Les fonctions membres en C++

Trois types de fonctions peuvent être définis dans une classe (1) en C++ :

  • les fonctions membres statiques ;
  • les fonctions membres normales ;
  • les fonctions membres virtuelles.

I-A. Les fonctions membres statiques

Le mot-clé static utilisé en début du prototype de la fonction permet de déclarer une fonction membre statique ou encore fonction de classe :

Exemple de fonction membre statique :
Sélectionnez
struct my_type
{
  static void s_function(); // fonction statique
};

Une fonction membre statique n'est pas liée à un objet. Elle n'a donc pas de paramètre implicite this et ne peut donc accéder à d'autres membres d'instances de la classe sinon les membres statiques :

Appel d'une fonction membre statique :
Sélectionnez
#include <iostream>
struct my_type
{
  static void s_function(); // fonction membre statique

  static int mi_class_variable;
  int mi_member_variable;
};
int my_type::mi_class_variable=0;
void my_type::s_function()
{
   std::cout<<"je suis une fonction membre statique !\n";
   mi_class_variable = 5; // On peut référencer un membre statique de la classe
   /*
   mi_member_variable = 2;// Erreur, une fonction membre statique ne peut utiliser 
                          // un membre non statique de la classe dépendant
                          // d'une instance
   this->mi_member_variable = 2; // Erreur, une fonction membre statique 
                                 // ne peut utiliser this ; elle n'est pas
                                 // liée à une instance de la classe
   */
}

int main()
{
   my_type::s_function(); // appel d'une fonction membre statique
   my_type var;
   var.s_function(); // bien qu'un objet soit utilisé, cet appel est équivalent 
                     // au précédent. var n'est pas pris en compte dans l'appel
   return 0;
}

L'appel d'une fonction membre statique d'une classe ne dépend pas d'une instance de ce type.

Par conséquent, les fonctions membres statiques ne nous intéresseront pas par la suite, car elles ne dépendent nullement d'un objet.

I-B. Les fonctions normales

Les fonctions normales n'ont pas de mot-clé spécifique pour leur déclaration : c'est le comportement par défaut d'une fonction membre d'une classe.

Exemple de fonction membre normale :
Sélectionnez
#include <iostream>
struct my_type
{
  void a_function(); // fonction membre normale
};
void my_type::a_function()
{
   std::cout<<"je suis une fonction normale !\n";
}

int main()
{
   /*
   my_type::a_function(); // Erreur : nécessite un objet pour être appelée
   */
   my_type var;
   var.a_function(); // nécessite une instance du type pour être invoquée.
   return 0;
}

Ces fonctions membres ont un pointeur this désignant l'objet au départ duquel elles ont été invoquées et peuvent accéder aux membres de cet objet :

Appel d'une fonction normale :
Sélectionnez
#include <iostream>
struct my_type
{
  void a_function(); // fonction membre normale
  int mi_member;
};
void my_type::a_function() 
{
   std::cout<<"je suis une fonction normale !\n";
   this->mi_member =41; // OK
   mi_member++; // OK : équivalent à (this->mi_member)++;
}

int main()
{
   my_type var;
   var.a_function();
   std::cout<<"valeur de mi_member de var : "<<var.mi_member<<"\n"; 
               // C'est bien l'objet var qui a été associé à l'appel de la fonction
   return 0;
}

I-C. Les fonctions virtuelles

Les fonctions virtuelles précèdent leur déclaration du mot-clé virtual(2) :

Exemple d'une fonction virtuelle :
Sélectionnez
#include <iostream>
struct my_type
{
  virtual void a_function(); // fonction virtuelle
};
void my_type::a_function() // dans la définition de la fonction, il ne faut 
                           // pas répéter le mot-clé 'virtual'
{
   std::cout<<"je suis une fonction virtuelle !\n";
}

int main()
{
/*
   my_type::a_function(); // Erreur : nécessite un objet pour être appelée
*/
   my_type var;
   var.a_function(); // nécessite une instance du type pour être invoquée.
   return 0;
}

Comme les fonctions membres normales, les fonctions virtuelles ont un pointeur this et peuvent accéder aux membres de cet objet :

Appel d'une fonction virtuelle :
Sélectionnez
#include <iostream>
struct my_type
{
  virtual void a_function(); // fonction virtuelle
  int mi_member;
};
void my_type::a_function()
{
   std::cout<<"je suis une fonction normale !\n";
   this->mi_member =41; // OK
   mi_member++; // OK : équivalent à (this->mi_member)++;
}

int main()
{
   my_type var;
   var.a_function();
   std::cout<<"valeur de mi_member de var : "<<var.mi_member<<"\n"; 
               // C'est bien l'objet var qui a été associé à l'appel de la fonction
   return 0;
}

Soit en résumé :

L'appel d'une fonction membre non statique d'une classe nécessite une instance du type.

Par défaut, en C++, les fonctions membres ne sont pas virtuelles. Le mot-clé virtual est nécessaire pour définir une fonction virtuelle.

En C++, les fonctions virtuelles doivent être membres d'une classe.

Anticipons en signalant l'existence d'une catégorie particulière de fonctions virtuelles en C++ : les fonctions virtuelles pures. Une fonction virtuelle pure est une fonction virtuelle à laquelle est rajoutée =0 à la fin de sa déclaration :

Exemple d'une fonction virtuelle pure :
Sélectionnez
struct my_type
{
  virtual void a_function()=0; // fonction virtuelle pure
};

Nous reviendrons un peu plus loin sur les fonctions virtuelles pures, pour l'instant il suffit de savoir que ça existe et qu'avant d'être « pures », ce sont avant tout des fonctions virtuelles.

Toutes les fonctions d'une classe peuvent-elles être virtuelles ? Oui, feinte presque : seuls les constructeurs (et les fonctions statiques) ne peuvent pas être virtuels. Toutes les autres fonctions le peuvent : que ce soit le destructeur, les opérateurs, ou des fonctions quelconques. Nous verrons par la suite ce que cela signifie et l'intérêt dans chaque cas.

Toutes (ou presque) les fonctions peuvent être virtuelles
Sélectionnez
struct my_type
{
//   virtual my_type(); // erreur : un constructeur ne peut être virtuel
//   virtual static void s_function(); // erreur : une fonction ne peut être ET statique ET virtuelle
   virtual ~my_type();// OK
   virtual void function(); // OK
   virtual my_type& operator+(my_type const&); // OK
};

La virtualité s'hérite : une fonction virtuelle dans la classe de base reste virtuelle dans la classe dérivée même si le mot-clé virtual n'est pas accolé :

La virtualité s'hérite :
Sélectionnez
struct base
{
   void function_1();
   virtual void function_2();
   void function_3();
};

struct derived : public base
{
   void function_1();
   void function_2();
   virtual void function_3();
};

Nous avons avec cet exemple :

 

base

derived

function_1

non virtuelle

non virtuelle

function_2

virtuelle

virtuelle

function_3

non virtuelle

virtuelle

Autant comme le montre la troisième ligne, il est possible de masquer une fonction non virtuelle d'une classe de base par une fonction virtuelle dans une classe dérivée, autant il est impossible de s'en débarrasser. Une fonction est et sera virtuelle pour toutes les classes dérivant de la classe l'ayant définie comme telle. Cependant, afin d'éviter toute confusion, il est fortement recommandé d'utiliser le mot-clé virtual dans les classes dérivées :

Les classes dérivées devraient utiliser le mot-clé virtual pour les fonctions définies comme virtuelles dans la classe de base.

I-D. La surcharge de fonction

Introduisons un dernier point pour poser notre problématique : la surcharge de fonction. Il est aussi possible de surcharger une fonction dans une classe, c'est-à-dire définir plusieurs fonctions avec le même nom, à condition qu'elles diffèrent par :

  • leur nombre d'arguments ;
  • et/ou le type d'au moins un des arguments ;
  • et/ou leur constance.

La signature d'une fonction en C++ désigne son nom, le nombre de ses arguments, leur type et la constance de la fonction. Surcharger une fonction F1 revient alors à proposer une nouvelle fonction F2, telle que la signature de F1 est différente de celle de F2 autrement que par le nom qu'elles partagent.
Par exemple, le code suivant présente différentes surcharges d'une fonction membre :

Surcharges d'une fonction membre :
Sélectionnez
struct my_type
{
   void function(double,double){}
   void function(double){} // le nombre d'arguments est différent 
                        // de la précédente définition
   void function(int){} // le type de l'argument est différent 
                         // de la précédente définition
   void function(int) const {} // la constance est différente 
                                // de la précédente définition
   void function(char&){} 
   void function(char const &){} // char& et char const & sont deux types différents
};

La surcharge est un cas de polymorphisme en C++ (3). Cela permet d'adapter la fonction à appeler selon les arguments en paramètre :

Appels des différentes surcharges d'une fonction membre :
Sélectionnez
#include <iostream>
struct my_type
{
   void function(double,double) // (1)
   {
      std::cout<<"(1)\n";
   }
   void function(double) // (2)
   {
      std::cout<<"(2)\n";
   }
   void function(int) // (3)
   {
      std::cout<<"(3)\n";
   }
   void function(int) const // (4)
   {
      std::cout<<"(4)\n";
   }
   void function(char&) // (5)
   {
      std::cout<<"(5)\n";
   }
   void function(char const &) // (6)
   {
      std::cout<<"(6)\n";
   }
};

int main()
{
   my_type var;
   var.function(1.,1.); // (1)
   var.function(1.); // (2)
   var.function(1); // (3)
   my_type const c_var=my_type();
   c_var.function(1); // (4)
   char c('a');
   var.function(c);  // (5)
   char const &rc = c;
   var.function(rc); // (6)

   return 0;
}

La surcharge a ses limites. En particulier, une même classe ne peut pas redéfinir une fonction avec le même nom d'une fonction existante si :

  • elles ne diffèrent que par leur type retour ;
  • elles ont les mêmes arguments, le même type retour, mais l'une d'elles est statique ;
  • elles ont les mêmes arguments, le même type retour, la même constance, mais l'une d'elles est virtuelle (ou virtuelle pure) et l'autre normale ;
  • elles ont les mêmes arguments, le même type retour, la même constance, mais l'une d'elles est virtuelle et l'autre virtuelle pure ;
  • un argument ne diffère qu'à cause d'un typedef ;
  • elles ne diffèrent que par un const non significatif sur le type d'un argument.
  • elles ne diffèrent que par les valeurs par défaut de leur(s) argument(s).

Ainsi les surcharges suivantes sont interdites dans une même classe :

Surcharges interdites :
Sélectionnez
struct my_type
{

   void function_1();
   int function_1(); // Erreur : seul le type retour est différent

   static void function_2();
   void function_2(); // Erreur : function_2 est déjà définie et statique
   void function_2()const; // const ne suffit pas pour différencier une
                            // fonction non statique et une fonction statique

   virtual void function_3();
   void function_3(); // Erreur : function_3 est déjà définie et virtuelle

   virtual void function_3_bis();
   void function_3_bis()const; // OK : const suffit pour différencier des
                           // fonctions membres non statiques


   virtual void function_4();
   virtual void function_4()=0; // Erreur : la fonction a déjà été déclarée
                                // virtuelle, mais non pure.
                                // L'erreur aurait été la même si on avait
                                // d'abord déclaré la fonction virtuelle pure
                                // puis la fonction virtuelle (non pure)

   void function_5(int);
   typedef int t_int;
   void function_5(t_int); // Erreur : le typedef n'est qu'un synonyme

   void function_6(char);
   void function_6(char const); // Erreur : le const n'est pas significatif
                                 // pour différencier les deux fonctions

   void function_6_bis(char *);
   void function_6_bis(char * const); // Erreur : le const n'est pas significatif
                                   // pour différencier les deux fonctions
   void function_6_ter(char *);
   void function_6_ter(char const *); // OK : char const * et char * sont bien
                                   // deux types différents
                                   
   void function_7(int );
   void function_7(int =42); // Erreur : les définitions sont équivalentes
};

I-E. Quand l'héritage chamboule tout !

L'héritage importe dans la classe dérivée toutes les déclarations des classes de base (4) :

Héritage des membres de la classe parent :
Sélectionnez
struct base
{
   void function(){}
};

struct derived : base
{
};

int main()
{
   derived d;
   d.function(); // on récupère l'interface de base

   return 0;
}

Cependant, une fonction déclarée dans une classe dérivée ayant le même nom qu'une fonction de la classe de base, mais avec une signature différente masque la fonction de la classe de base dans la classe dérivée :

Masquage des fonctions de base dans la classe dérivée :
Sélectionnez
struct base
{
   void function(){}
};

struct derived : base
{
   void function(int){}
   void call_a_function()
   {
      function(); // Erreur base::function est masquée par derived::function
      base::function(); // OK : on indique explicitement la fonction à appeler
      function(1); // appel de derived::function(int)
   }
};

int main()
{
   derived d;
   d.function(); // Erreur base::function est masquée par derived::function
   d.base::function(); // OK : on indique explicitement la fonction à appeler
   d.function(1); // appel de derived::function(int)

   return 0;
}

La surcharge dans une classe dérivée d'une fonction définie dans une classe de base avec une signature différente masque la fonction de la classe de base dans et pour la classe dérivée.

Nous avons vu à la section précédente qu'il n'était pas possible dans une même classe de redéfinir une nouvelle fonction avec la même signature qu'une fonction existante (même nom, mêmes paramètres, même constance). Et, nous en arrivons au point qui va nous intéresser, à savoir :

Une classe dérivée peut redéfinir une fonction d'une classe de base ayant la même signature !

Ainsi, en reprenant dans la section précédente l'exemple des surcharges interdites et en recopiant les surcharges interdites vers la classe dérivée, nous obtenons le code valide suivant :

Tout redevient possible avec l'héritage :
Sélectionnez
struct base
{

   void function_1();

   static void function_2();

   virtual void function_3();

   virtual void function_4();

   void function_5(int);

   void function_6(char);
   void function_7(int );
};

struct derived : public base
{
   int function_1(); // OK
   void function_2(); // OK
   void function_2()const; // OK
   void function_3(); // OK
   virtual void function_4()=0; // OK
   typedef int t_int; // OK
   void function_5(t_int); // OK
   void function_6(char const); // OK
   void function_7(int =42); // OK
};

L'objectif est maintenant de savoir quelles fonctions sont appelées lorsqu'une même signature est disponible dans une classe dérivée et une classe de base selon l'expression utilisée pour l'appel :

Quelle est la fonction appelée ?
Sélectionnez
struct base
{
   void function_1()
   {
   }
   virtual void function_2()
   {
   }
   void call_function_1()
   {
      function_1(); // Quelle est la fonction appelée ?
   }
   void call_function_2()
   {
      function_2(); // Quelle est la fonction appelée ?
   }
};

struct derived : public base
{
   void function_1()
   {
   }
   virtual void function_2()
   {
   }
   void call_function_1()
   {
      function_1(); // Quelle est la fonction appelée ?
   }
   void call_function_2()
   {
      function_2(); // Quelle est la fonction appelée ?
   }
};

int main()
{
   base b;
   b.function_1(); // Quelle est la fonction appelée ?
   b.function_2(); // Quelle est la fonction appelée ?

   derived d;
   d.function_1(); // Quelle est la fonction appelée ?
   d.function_2(); // Quelle est la fonction appelée ?

   base &rb = b;
   rb.function_1(); // Quelle est la fonction appelée ?
   rb.function_2(); // Quelle est la fonction appelée ?

   base &rd = d;
   rd.function_1(); // Quelle est la fonction appelée ?
   rd.function_2(); // Quelle est la fonction appelée ?

   return 0;
}

Pour arriver à répondre à toutes ces questions, il nous faut introduire une nouvelle notion : le type statique et le type dynamique d'une variable.

II. Type statique et type dynamique

Une variable possède deux types : un type statique et un type dynamique.
Le type statique d'une variable est celui déterminé à la compilation. Le type statique est le plus évident : c'est celui avec lequel vous avez déclaré votre variable. Il est sous votre nez lorsque vous regardez le code.
Le type dynamique d'une variable est celui déterminé à l'exécution. Le type dynamique quant à lui n'est pas immédiat en regardant le code. En effet, il va pouvoir varier à l'exécution selon ce que la variable va effectivement désigner pendant le déroulement du programme.
Le type dynamique et le type statique coïncident pour les variables utilisées par valeur :

Type statique et type dynamique de variables par valeur :
Sélectionnez
int a;
char c;
std::string s;
class a_class{/*[...]*/};
a_class an_object;
enum E_enumeration{/*[...]*/};
E_enumeration e;

Avec cet exemple, on a :

Type statique et dynamique d'une variable par valeur

variable

type statique

type dynamique

a

int

int

c

char

char

s

std::string

std::string

an_object

a_class

a_class

e

E_enumeration

E_enumeration

Encore une fois, l'héritage introduit une différence entre un type dynamique et un type statique. Cette différence apparaît avec les pointeurs et les références quand le type déclaré du pointeur ou de la référence n'est pas le type de l'objet effectivement pointé (resp. référencé) à l'exécution. Le type statique est celui défini dans le code, le type dynamique d'une référence ou d'un pointeur est celui de l'objet référencé (resp. pointé) :

Type statique, type dynamique, héritage, pointeurs et références :
Sélectionnez
struct base {};
struct derived : public base {};

int main()
{

   base b;
   derived d;
   base &rb = b;
   base &rd = d;
   base *pb = &b;
   base *pd = &d;
   
   return 0;
}

Avec l'exemple, ci-dessus, les types des variables (5) sont :

Type statique et dynamique d'une variable par référence

variable

type statique

type dynamique

base b

base

base

derived d

derived

derived

base &rb = b

base

base

base &rd = d

base

derived

base *pb = &b

base

base

base *pd = &d

base

derived

Seul les pointeurs et les références vers des instances de classe ou de structure ont des types dynamiques et des types statiques pouvant diverger.

Le type statique d'un objet dans une fonction membre est celui où se déroule la fonction. Le type dynamique, this étant un pointeur, dépend de l'objet effectif sur lequel s'applique la fonction. Type statique et type dynamique peuvent alors être différents :

Type de this :
Sélectionnez
class base
{
public:
   void function()
   {
           // Quel est le type static de this ?
           // Quel est le type dynamique de this ?
   }
};
class derived : public base
{
public:
   void function_2()
   {
           // Quel est le type static de this ?
           // Quel est le type dynamique de this ?
   }
};
int main()
{
   base b;
   b.function();
   derived d;
   d.function();
   d.function_2();

   return 0;
}
Type statique et dynamique dans une fonction membre

fonction

type statique de this

type dynamique de this

Pour l'appel b.fonction, dans la fonction base::fonction

base

base

Pour l'appel de d.fonction, dans la fonction base::fonction

base

derived

Pour l'appel de d.fonction_2 dans la fonction derived::fonction_2

derived

derived

Le type statique d'une variable est soit le type dynamique de cette variable soit une classe de base directe ou indirecte du type dynamique.

Toute autre combinaison est une erreur pouvant aboutir à un plantage ou un comportement indéterminé.

Attention, si nous avons dit que le pointeur a un type statique différent de son type dynamique, cela s'applique aussi bien au pointeur non déréférencé qu'au pointeur déréférencé :

Types statiques et dynamiques d'un pointeur :
Sélectionnez
struct base {};
struct derived : public base {};

int main()
{

   base b;
   derived d;
   base *pd = &d;
   
   return 0;
}
Type statique et dynamique d'une variable par référence

variable

type statique

type dynamique

pd

base*

derived*

*pd

base

derived

III. Types et appel de fonction

Lorsqu'une expression contient un appel d'une fonction sur un objet donné, la fonction effectivement appelée dépend de plusieurs paramètres :

  • la fonction est-elle virtuelle ou non ?
  • la fonction est-elle appelée sur un objet par valeur ou sur une référence/pointeur ?

Selon la réponse, la résolution de l'appel est faite à la compilation ou à l'exécution :

Quand est résolu l'appel ?
 

fonction non virtuelle

fonction virtuelle

appel sur un objet

COMPILATION

COMPILATION

appel sur une référence ou un pointeur

COMPILATION

EXÉCUTION

La résolution à la compilation utilise le type statique, car c'est le seul connu à ce moment.
La résolution à l'exécution se base sur le type dynamique.
Ce qui donne :

Type utilisé pour la résolution de l'appel
 

fonction non virtuelle

fonction virtuelle

appel sur un objet

type statique

type statique

appel sur une référence ou un pointeur

type statique

type dynamique

Un peu de code pour illustrer tout cela :

Résolution à la compilation ou à l'exécution des appels :
Sélectionnez
#include <iostream>

struct base
{
   void function_1()
   {
      std::cout<<"base::function_1\n";
   }

   virtual void function_2()
   {
      std::cout<<"base::function_2\n";
   }

};
struct derived :public base
{
   void function_1()
   {
      std::cout<<"derived::function_1\n";
   }

   virtual void function_2()
   {
      std::cout<<"derived::function_2\n";
   }
};

int main()
{
   base b;
   std::cout<<"\nappels pour base b; : \n";
   b.function_1();
   b.function_2();

   derived d;
   std::cout<<"\nappels pour derived d; : \n";
   d.function_1();
   d.function_2();

   base &rb = b;
   std::cout<<"\nappels pour base &rb = b; : \n";
   rb.function_1();
   rb.function_2();

   base &rd = d;
   std::cout<<"\nappels pour base &rd = d; : \n";
   rd.function_1();
   rd.function_2();

   return 0;
}

Ce code produit comme sortie :

Sortie :
Sélectionnez
appels pour base b; :
base::function_1
base::function_2

appels pour derived d; :
derived::function_1
derived::function_2

appels pour base &rb = b; :
base::function_1
base::function_2

appels pour base &rd = d; :
base::function_1
derived::function_2

Les types statiques et dynamiques sont :

Type statique et dynamique des différentes variables

variable

type statique

type dynamique

base b

base

base

derived d

derived

derived

base &rb = b

base

base

base &rd = d

base

derived

fonction_1 est une fonction non virtuelle et fonction_2 est une fonction virtuelle. Soit en reprenant le tableau précédent précisant la fonction appelée :

Fonction appelée selon le moment de résolution de l'appel

Appels sur b

fonction non virtuelle ( fonction_1 )

fonction virtuelle ( fonction_2 )

appel sur un objet

type statique : base::fonction_1

type statique : base::fonction_2

Appels sur d

   

appel sur un objet

type statique : derived::fonction_1

type statique : derived::fonction_2

Appels sur rb

   

appel sur une référence

type statique : base::fonction_1

type dynamique : base::fonction_2

Appels sur rd

   

appel sur une référence

type statique : base::fonction_1

type dynamique : derived::fonction_2

Cette résolution dynamique présentée avec des références fonctionne de la même façon avec un pointeur qu'il soit utilisé en tant que tel ou déréférencé :

Références et pointeurs : même combat !
Sélectionnez
#include <iostream>

struct base
{
   virtual void function()
   {
      std::cout<<"base::function\n";
   }

};
struct derived :public base
{
   virtual void function()
   {
      std::cout<<"derived::function\n";
   }
};

int main()
{
   derived d;
   base &rd = d;
   base *pd = &d;

   // 3 appels équivalents :
   pd->function();
   (*pd).function();
   rd.function();

   return 0;
}

Le comportement est identique si l'expression utilise un pointeur de fonction :

Appel de fonction avec un pointeur de fonction membre :
Sélectionnez
#include <iostream>

struct base
{
   virtual ~base(){}
   virtual void function_1()
   {
      std::cout<<"base::function_1\n";
   }

   void function_2()
   {
      std::cout<<"base::function_2\n";
   }

};
struct derived : public base
{
   virtual void function_1()
   {
      std::cout<<"derived::function_1\n";
   }

   void function_2()
   {
      std::cout<<"base::function_2\n";
   }
};

void call_a_function(void (base::*pf_)())
{
   base b;
   (b.*pf_)();
   derived d;
   (d.*pf_)();
   base &rd = d;
   (rd.*pf_)();
}

int main()
{
   std::cout<<"function_1 :\n";
   call_a_function(&base::function_1);
   std::cout<<"function_2 :\n";
   call_a_function(&base::function_2);

   return 0;
}

La sortie produite est bien :

 
Sélectionnez
function_1 :
base::function_1
derived::function_1
derived::function_1
function_2 :
base::function_2
base::function_2
base::function_2

La liaison tardive prenant appui sur le type dynamique telle que nous la décrivons ici est valable presque tout le temps. Comme nous allons le voir par la suite, ce mécanisme présente quelque subtilité en particulier lors des phases sensibles que sont la construction ou la destruction d'un objet.

IV. À quoi servent les fonctions virtuelles ?

L'utilisation du type dynamique pour résoudre l'appel d'une fonction virtuelle est une des grandes forces de la programmation orientée objet (POO). Elle permet d'adapter et de faire évoluer un comportement défini dans une classe de base en spécialisant les fonctions virtuelles dans les classes dérivées. La substitution d'un objet de type dynamique dérivant du type statique présent dans l'expression contenant l'appel vers une fonction virtuelle s'inscrit dans le cadre du polymorphisme d'inclusionQu'est-ce que le polymorphisme d'inclusion ?.

Ce polymorphisme d'inclusion permet l'abstraction dans un logiciel orienté objet. Ainsi, un objet peut être manipulé à partir d'un pointeur ou d'une référence vers la classe de base. Les membres publics de la classe de base déterminent les services proposés et la mise en œuvre est déléguée aux classes dérivées apportant des points de variations ou spécialisant des comportements dans les fonctions virtuelles :

Le polymorphisme d'inclusion : un principe fondamental de l'objet !
Sélectionnez
#include <iostream>
struct shape
{
   virtual void draw() const
   {
      std::cout<<"une forme amorphe\n";
   }
};

void draw_a_shape(shape const &rs)
{ // ne connaît que shape. Pas ses classes dérivées
   rs.draw();
}

struct square : public shape
{
   virtual void draw() const
   {
      std::cout<<"un carre\n";
   }
};
struct circle : public shape
{
   virtual void draw() const
   {
      std::cout<<"un cercle\n";
   }
};

int main()
{
   shape sh;
   draw_a_shape(sh);
   square sq;
   draw_a_shape(sq);
   circle c;
   draw_a_shape(c);
   
   return 0;
}

L'abstraction permet de mieux isoler les différents composants les uns des autres en ne les rendant dépendants que des services dont ils ont vraiment besoin. Elle favorise la modularité et l'évolutivité des architectures logicielles. Notre fonction draw_a_shape peut dessiner tout type d'objet non prévu lors de son écriture du moment que ces objets sont d'un type dynamique héritant de shape et spécialisant ses fonctions virtuelles. Il devient possible de rajouter de nouveaux types et de pouvoir les dessiner sans avoir à réécrire la fonction draw_a_shape.

Les fonctions virtuelles réduisent le couplage entre une classe ou une fonction cliente et une classe fournisseur en déléguant aux classes dérivées la réalisation d'une partie des services proposés par la classe de base.

Les fonctions virtuelles sont un mécanisme pour la mise en œuvre du principe ouvert/fermé (6) en permettant de faire évoluer une application par l'ajout de nouvelle classe dérivée (ouvert) sans avoir à toucher le code existant utilisant l'interface de la classe de base (fermé).

Les fonctions virtuelles favorisent la réutilisation. Toutes les fonctions ou les classes s'appuyant sur des références ou des pointeurs de la classe de base peuvent être directement utilisées avec des objets d'un nouveau type dérivé.

Si le terme complet est polymorphisme d'inclusion, beaucoup de documents en C++ (articles - et celui-ci n'échappe pas à la règle - , cours, livres, etc.) omettent de préciser d'inclusion. Polymorphe, polymorphique, polymorphisme, polymorphiquement s'emploient souvent seuls dès qu'il s'agit de parler d'héritage et donc de la manipulation d'un objet d'une classe dérivée à partir d'une référence ou d'un pointeur d'une de ses classes de base avec en arrière plan le mécanisme des fonctions virtuelles. C'est un raccourci qui peut faire oublier les autres formes de polymorphisme qui ne s'appuient pas sur les fonctions virtuelles et le mécanisme d'héritage. Il faut juste se souvenir que le polymorphisme ne se réduit pas au polymorphisme d'inclusion.


précédentsommairesuivant
Dans cet article, les termes classes et structures sont employés indifféremment. En effet, la seule différence en ce qui nous concerne entre class et struct est la visibilité par défaut : publique pour les membres et l'héritage pour les types définis avec struct et privée pour les membres et l'héritage pour les types définis avec class. Pour tout ce que nous allons dire par la suite, la chose est strictement identique que le type soit struct ou class. Cf. F.A.Q. : Quelle est la différence entre class et structQuelle est la différence entre class et struct ? ?
Attention, le mot-clé virtual peut être utilisé pour l'héritage virtuel afin de résoudre le problème des classes de base communes (héritage en losange) : Qu'est-ce que l'héritage virtuel et quelle est son utilitéQu'est-ce que l'héritage virtuel et quelle est son utilité ? ? Cela n'a strictement aucune influence sur les fonctions virtuelles.
Bien sûr, la visibilité des fonctions (publique, protégée ou privée) combinée à celle de l'héritage limite les cas où l'appel est valide. On lira avec profit l'entrée de la F.A.Q. suivante : Que signifient public, private et protectedQue signifient public, private et protected ? ?
Par facilité d'écriture, dans ce document, pour une variable Type *p_var, on dira que son type est Type alors que formellement p_var est de type Type* (pointeur de Type) et c'est *p_var qui est de type Type. De même, pour les références Type & r_var, on dira que le type de r_var est Type et non Type&. Enfin, de la même façon on fera pudiquement l'impasse sur les qualifications const et/ou volatile qui peuvent préciser le type réel d'une variable.
Le principe ouvert/fermé ou OCP (open/close principle pour les amis de Lord Byron) énonce qu'une classe doit être ouverte à l'extension et fermée à la modification. Apparemment contradictoire, ce principe s'appuie justement sur le mécanisme des fonctions virtuelles pour permettre l'extension par les classes dérivées (ouverture), mais cela doit se faire sans modifier le code existant (fermeture). Pour plus d'information, le billet Le principe « ouvert/fermé »Le principe 'ouvert/fermé' d'Emmanuel Deloget propose une présentation de ce principe. The Open-Closed PrincipleO.C.P. par objectmentor est une présentation de ce même principe en anglais.

Copyright © 2009 3DArchi. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.