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 :
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 :
#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.
#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 :
#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) :
#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 :
#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 :
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.
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é :
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 : 
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 :
#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 :
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) :
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 :
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 :
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 :
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 : 
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 :
| 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é) :
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 :
| 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 :
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;
}| 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é :
struct base {};
struct derived : public base {};
int main()
{
   base b;
   derived d;
   base *pd = &d;
   
   return 0;
}| 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 :
| 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 : 
| 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 :
#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 :
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_2Les types statiques et dynamiques sont :
| 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 :
| 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é :
#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 :
#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 :
function_1 :
base::function_1
derived::function_1
derived::function_1
function_2 :
base::function_2
base::function_2
base::function_2La 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 :
#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.



