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

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

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


précédentsommairesuivant

XIII. Le retour covariant des fonctions virtuelles

Une fonction virtuelle dans la classe dérivée doit présenter la même signature (même nom, même nombre de paramètres, même type des paramètres, même constance de la fonction) pour spécialiser la fonction définie dans la classe de base. Or le type de retour de la fonction ne fait pas partie de la signature de la fonction. Il devient dès lors légitime de se poser la question suivante : une fonction virtuelle peut elle avoir un type de retour différent de la classe de base ? La réponse est en deux temps : non mais oui.
Non : si les deux types de retour n'ont rien en commun alors c'est une erreur :

La spécialisation d'une fonction virtuelle ne peut retourner un type vraiment différent :
Sélectionnez
struct base {
   virtual int function()
   {
      return 0;
   }
};
struct derived : public base
{
   virtual double function() // Erreur !
   {
      return 0.0;
   }
};

Mais oui à condition de respecter les contraintes suivantes :

  • le retour doit être par pointeur ou par référence ;
  • ET le type retourné dans la classe dérivée doit dériver directement ou indirectement et sans ambiguïté du type retourné dans la classe de base ;
  • ET les types retournés doivent avoir la même constance ou celui de la classe dérivée doit avoir une constance plus faible.

On parle alors d'un retour covariant car le retour de la fonction virtuelle varie en même temps que l'héritage :

Retour covariant :
Sélectionnez
struct base_return
{
};

struct base {
   virtual base_return& function();
};

struct derived_return : public base_return
{};

struct derived : public base
{
   virtual derived_return& function();
};

Le retour est dit covariant car une spécialisation dans une classe C ne peut retourner qu'un type égal ou dérivant de la fonction virtuelle la plus spécialisée de son arbre d'héritage :

Retour covariant : types retour et types de la classe suivent un même se spécialisent en même temps
Retour covariant : types retour et types de la classe suivent un même se spécialisent en même temps

Ce qui se traduit en code :

Retours covariants corrects :
Sélectionnez
struct base_return
{};
struct derived_1_return : public base_return
{};
struct derived_2_return : public derived_1_return
{};

struct base {
   virtual base_return& function();
};


struct derived_1 : public base
{
   virtual derived_1_return& function();
};

struct derived_2 : public derived_1
{
   virtual derived_2_return& function();
};
Erreur : le type retour n'est pas covariant
Erreur : le type retour n'est pas covariant

Ce qui se traduit en code :

Un type de retour non covariant est une erreur :
Sélectionnez
struct base_return
{};
struct derived_1_return : public base_return
{};
struct derived_2_return : public derived_1_return
{};

struct base {
   virtual base_return& function();
};


struct derived_1 : public base
{
   virtual derived_2_return& function();
};

struct derived_2 : public derived_1
{
   virtual derived_1_return& function(); // Erreur : le type retour n'est pas covariant !
   virtual base& function(); // Erreur : le type retour n'est pas covariant !
   virtual derived_2_return& function(); // OK
};
Erreur : le type retour n'est pas covariant
Erreur : le type retour n'est pas covariant

Ce qui se traduit en code :

Un type de retour non covariant est une erreur :
Sélectionnez
struct base_return
{};
struct derived_1_return : public base_return
{};
struct derived_3_return : public base_return
{};

struct base {
   virtual base_return& function();
};


struct derived_1 : public base
{
   virtual derived_1_return& function();
};

struct derived_2 : public derived_1
{
   virtual derived_3_return& function(); // Erreur : le type retour n'est pas covariant !
   virtual derived_1_return& function(); // OK
};

La constance du retour peut être perdue en chemin mais pas rajoutée :

La constance du retour peut être perdue :
Sélectionnez
struct base_return
{};

struct base {
   virtual base_return const & function_1();
   virtual base_return & function_2();
};


struct derived_1 : public base
{
   virtual base_return & function_1(); // OK
   virtual base_return const & function_2(); // Erreur
};

Pour comprendre ces règles de covariance, il faut se rappeler que la fonction de la classe dérivée peut être appelée avec un pointeur ou une référence d'un type statique de la classe de base. Pour que le compilateur puisse correctement vérifier les types à la compilation (donc pour les classes de base), les types retournés à l'exécution (donc par les classes dérivées) doivent présenter une certaine cohérence. C'est pour cela qu'une fonction spécialisée ne peut retourner qu'un type qui puisse se substituer à la version la plus spécialisée de ses classes de base.

Nécessité du retour covariant :
Sélectionnez
struct base {
   virtual base_return const & function_1();
   virtual base_return & function_2();
};

void a_function_handling_base(base&b_)
{
   base_return const & cr = b_function_1();// cr ne peut référencer que quelque 
    // chose qui se substitue à base_return const sans problème : une classe
    // dérivée et soit constante soit non constante.

   base_return & r2 = b_function_2();// r2 ne devrait pas pouvoir être liée
   // à une référence constante. Cette contrainte ne fait pas partie de 
   // l'interface présentée par base::function_2
}

XIV. Forcer un appel spécifique d'une fonction virtuelle

Il est possible d'utiliser le type statique pour appeler une fonction virtuelle d'un pointeur, d'une référence ou dans une classe (qui utilise d'habitude le type dynamique du pointeur this) à condition d'indiquer explicitement au compilateur la version à appeler. Cela se fait en ajoutant le nom de la classe de la spécialisation qui doit être appelée :

Appel statique d'une fonction virtuelle
Sélectionnez
#include <iostream>

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

   void call_base_function()
   {
      base::function(); // on force l'appel de base::function
   }

};
struct derived :public base
{
   virtual void function()
   {
      std::cout<<"derived::function\n";
   }
   void call_derived_function()
   {
      derived::function(); // on force l'appel de derived::function
   }
   void call_parent_function()
   {
      base::function(); // on force l'appel de base::function
   }
};

int main()
{
   derived d;
   base &rd = d;
   rd.function(); // appel virtuel
   rd.base::function(); // on force l'appel de base::function()
   rd.call_base_function();
   d.derived::function();// on force l'appel de derived::function()
   d.base::function();// on force l'appel de base::function()
   d.call_base_function();
   d.call_derived_function();
   d.call_parent_function();
   return 0;
}

Avec un appel explicite, la résolution se fait à la compilation avec le type statique qui préfixe l'appel. La fonction n'est plus vue comme virtuelle.
Si une classe dérivée veut appeler la définition de la classe parente dans son implémentation d'une fonction virtuelle, elle le fait donc en spécifiant explicitement cet appel :

Forcer un appel statique :
Sélectionnez
#include <iostream>

struct base
{
   virtual void who_am_i() const
   {
      std::cout<<"base\n";
   }
};
struct derived :public base
{
   virtual void who_am_i() const
   {
      std::cout<<"derived avec comme parent :\n";
      base::who_am_i();
   }
};

void who_are_you(base const &b_)
{
   b_.who_am_i();
}

int main()
{
   base b;
   std::cout<<"Qui est b ?\n";
   who_are_you(b);
   std::cout<<"Qui est d ?\n";
   derived d;
   who_are_you(d);
   return 0;
}

Si la classe de base doit effectuer certaines actions avant ou après l'exécution de la spécialisation, alors il est plus judicieux d'utiliser le pattern N.V.I. (Non Virtual Interface) présenté plus loin.

XV. Fonctions virtuelles et visibilité

Jusqu'à présent les exemples proposés montrent des classes dont les fonctions virtuelles sont publiques et leur spécialisation dans les classes dérivées aussi. En fait, les notions de visibilité et de redéfinition sont indépendantes les unes des autres comme nous allons le voir.

La visibilité d'une fonction virtuelle peut changer dans la classe dérivée sans que cela ait d'impact sur le mécanisme d'appel dynamique.

Une fonction virtuelle déclarée publique dans l'interface de base peut être spécialisée avec une portée privée ou protégée par la classe dérivée, l'appel avec une référence ou un pointeur sur la classe de base va chercher la bonne spécialisation :

Virtuel et visibilité sont indépendants :
Sélectionnez
#include <iostream>

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

   virtual ~base(){}
};

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

int main()
{
   derived d;
   base &rd = d;
   rd.function(); // ok : derived::function
   return 0;
}

Le C++ a choisi de différencier les deux notions. La visibilité d'une fonction est vérifiée par le compilateur avec le type statique d'une variable. La résolution de l'appel dynamiquement à l'exécution ne tient pas compte de la visibilité.
La conséquence est :

Une classe dérivée peut spécialiser une fonction virtuelle déclarée comme privée par la classe de base.

Le code suivant ne pose donc aucun problème :

Spécialisation d'une fonction virtuelle privée :
Sélectionnez
#include <iostream>

struct base
{
   public:
   void function()
   {
      do_function();// seul base peut appeler la fonction virtuelle privée.
   }

   virtual ~base(){}


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

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

struct derived_2 : public base
{
   void function_2()
   {
      do_function();// erreur : la fonction est privée
      function(); // OK
   }
};

int main()
{
   derived d;
   base &rd = d;
   rd.function(); // ok
   
   return 0;
}

Cela est vrai aussi si l'héritage mis en oeuvre est privé :

Spécialisation d'une fonction virtuelle héritée indirectement en privée :
Sélectionnez
#include <iostream>

struct base
{
   public:
   void function()
   {
      do_function();// seul base peut appeler la fonction virtuelle privée.
   }

   virtual ~base(){}


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

struct derived : private base
{
   public :
   void function_2() // on verra plus loin que le mot clé using permet
   		// de ramener le symbole sans avoir à redéfinir une nouvelle fonction
   {
      function();
   }
};

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

int main()
{
   derived_2 d;
   d.function_2();
   
   return 0;
}

Bien sûr, la fonction dans la classe de base étant privée, celle-ci ne peut être directement appelée depuis l'extérieur ou par les classes dérivées :

Les appels nécessitent les droits d'accès adéquats :
Sélectionnez
#include <iostream>

struct base
{
   public:
   virtual ~base(){}

   void function()
   {
      do_function(); // OK
   }

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

struct derived : public base
{
   void function_2()
   {
      base::do_function();//erreur
   }
   private:
   virtual void do_function()
   {
      std::cout<<"derived::function\n";
   }
};

int main()
{
   derived d;
   d.do_function(); // erreur
   base &rd = d;
   rd.do_function(); // erreur

   rd.function(); // OK
   d.function(); // OK

   return 0;
}

Nous verrons plus loin, lorsque nous évoquerons le pattern N.V.I. que cette différenciation entre la résolution dynamique et la visibilité statique est loin d'être anecdotique et apporte des avantages.

XVI. Fonction virtuelle et masquage de fonction

XVI-A. Masquage d'une fonction virtuelle par une fonction non virtuelle

Nous avons dit en début d'article qu'une fonction définie dans une classe dérivée avec une signature différente de celle de la classe de base masque la fonction de la classe de base. Et en particulier, son aspect virtuel peut être perdu :

Masquage des fonctions virtuelles :
Sélectionnez
#include <iostream>
struct base
{
   virtual void function()
   {
      std::cout<<"base::function\n";
   }
};

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

struct derived_2 : public derived
{
};

int main()
{
   base b;
   b.function();
   derived d;
   d.function();
   derived_2 d2;
   d2.function();

   return 0;
}

Ce code produit le résultat suivant :

 
Sélectionnez
base::function
derived::function
derived::function

Les choses peuvent sembler confuses dès lors que des fonctions d'une classe dérivée masquent les fonctions virtuelles des classes de bases. En effet, la résolution statique ou dynamique de l'appel dépend du type statique de l'expression contenant l'appel. Selon que ce type statique 'voit' la fonction virtuelle ou que celle-ci est masquée par la surcharge, l'appel sera traité comme un appel virtuel ou non. Et pour un même objet, selon l'expression utilisée pour appeler la fonction, le comportement peut être très différent. Voyons cela avec un peu de code :

Masquage de fonction :
Sélectionnez
#include <iostream>
struct base
{
   virtual void function()
   {
      std::cout<<"base::function\n";
   }
   void call_function()
   {
      function();
   }
};

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

struct derived_2 : public derived
{
   void call_function()
   {
      function();
   }
};

void call_with_base(base&b_)
{
   b_.function();
   b_.call_function();
}

void call_with_derived(derived&d_)
{
   d_.function();
   d_.call_function();
}

int main()
{
   base b;
   std::cout<<"base\n";
   b.function();
   b.call_function();
   call_with_base(b);

   derived d;
   std::cout<<"\nderived\n";
   d.function();
   d.call_function();
   call_with_base(d);
   call_with_derived(d);

   derived_2 d2;
   std::cout<<"\nderived_2\n";
   d2.function();
   d2.call_function();
   call_with_base(d2);
   call_with_derived(d2);


   return 0;
}

Nous voici avec le résultat suivant :

 
Sélectionnez
base
base::function
base::function
base::function
base::function

derived
derived::function
base::function
base::function
base::function
derived::function
base::function

derived_2
derived::function
derived::function
base::function
base::function
derived::function
base::function

Pour comprendre comment sont traités ces appels de fonction, il faut à chaque fois se poser les questions suivantes :

  • Quel est le type statique de l'expression sur laquelle s'applique la fonction ? La réponse permet de répondre à la question suivante :
  • Quelle est la fonction vue par ce type statique ? La réponse donne la colonne (fonction virtuelle ou non) de notre tableau montrant où se fait la résolution de l'appel.
  • Et s'il s'agit de la fonction virtuelle, alors quel est le type dynamique de l'expression ? La réponse est nécessaire si on se trouve sur la seconde colonne de notre tableau.

Reprenons les différents appels et procédons à leur analyse suivant cette grille :

Appels sur base :
Sélectionnez
   base b;
   std::cout<<"base\n";
   b.function();
   b.call_function();
   call_with_base(b);

Pour ces trois appels :

Fonctions appelées pour un objet de base
expression type statique fonction vue type dynamique fonction appelée
b.function base - - Appel d'une fonction : l'expression utilise une variable par valeur, la résolution est donc faite à la compilation avec le type statique : il s'agit de base::function.
b.call_function() aboutit à l'appel de base::call_function contenant l'expression qui nous intéresse function();. Nous devons regarder nous intéresser à this qui est l'objet sur lequel la fonction est appelée. base base::function (virtuelle) base L'expression utilise une variable par pointeur (this), la fonction est virtuelle, l'appel utilise donc le type dynamique (base) : base::function
call_with_base(base&b_) : b_.function(). Intéressons nous à b_ base base::function (virtuelle) base L'expression utilise une variable par référence, la fonction est virtuelle, l'appel utilise donc le type dynamique (base) : base::function
call_with_base(base&b_) : b_.call_function(). Cet appel est équivalent à la seconde ligne de ce tableau : b.call_function() base base::function (virtuelle) base L'expression utilise une variable par pointeur (this), la fonction est virtuelle, l'appel utilise donc le type dynamique (base) : base::function


Passons à derived d :

Appel sur derived
Sélectionnez
   derived d;
   d.function();
   d.call_function();
   call_with_base(d);
   call_with_derived(d);
Fonctions appelées pour un objet de base
expression type statique fonction vue type dynamique fonction appelée
d.function derived - - Appel d'une fonction : l'expression utilise une variable par valeur, la résolution est donc faite à la compilation avec le type statique : il s'agit de derived::function.
d.call_function() aboutit à l'appel de base::call_function contenant l'expression qui nous intéresse function();. Nous devons regarder nous intéresser à this qui est l'objet sur lequel la fonction est appelée. base base::function (virtuelle) derived L'expression utilise une variable par pointeur (this), la fonction est virtuelle, l'appel utilise donc le type dynamique (derived). Sauf que derived::function n'est pas une spécialisation de base::function. Donc, la fonction appelée est base::function
call_with_base(base&b_) : b_.function(). Intéressons nous à b_ base base::function (virtuelle) derived L'expression utilise une variable par référence, la fonction est virtuelle, l'appel utilise donc le type dynamique (derived) mais pour les mêmes raisons que ci-dessus l'appel est : base::function
call_with_base(base&b_) : b_.call_function(). Cet appel est équivalent à la seconde ligne de ce tableau : d.call_function() base base::function (virtuelle) derived L'expression utilise une variable par pointeur (this), la fonction est virtuelle, l'appel utilise donc le type dynamique (base) et toujours en l'absence de résolution, la fonction appelée est (12)base::function
call_with_derived(derived&d_) : d_.function(). Intéressons nous à d_ derived derived::function (non virtuelle) - L'expression utilise une fonction non virtuelle, l'appel utilise donc le type statique (derived), l'appel est donc résolu à la compilation vers derived::function
call_with_derived(derived&d_) : d_.call_function(); aboutit à l'appel de base::call_function contenant l'expression qui nous intéresse function();. Nous devons regarder nous intéresser à this qui est l'objet sur lequel la fonction est appelée. base base::function (virtuelle) derived L'expression utilise une variable par pointeur (this), la fonction est virtuelle, l'appel utilise donc le type dynamique (derived) mais la seule spécialisation disponible est base::function.


Enfin, pour les dernières expressions avec derived_2 d2 :

Appels sur derived_2
Sélectionnez
   derived_2 d2;
   d2.function();
   d2.call_function();
   call_with_base(d2);
   call_with_derived(d2);
Fonctions appelées pour un objet de base
expression type statique fonction vue type dynamique fonction appelée
d2.function derived_2 - - Appel d'une fonction : l'expression utilise une variable par valeur, la résolution est donc faite à la compilation avec le type statique : il s'agit de derived::function.
d2.call_function() aboutit à l'appel de derived_2::call_function contenant l'expression qui nous intéresse function();. Nous devons regarder nous intéresser à this qui est l'objet sur lequel la fonction est appelée. derived_2 derived::function (non virtuelle) derived_2 L'expression utilise une fonction non virtuelle, la résolution est donc à la compilation avec le type statique : derived::function.
call_with_base(base&b_) : b_.function(). Intéressons nous à b_ base base::function (virtuelle) derived_2 L'expression utilise une variable par référence, la fonction est virtuelle, l'appel utilise donc le type dynamique (derived_2) mais comme pour derived, la spécialisation disponible est : base::function
call_with_base(base&b_) : b_.call_function(). call_function() n'étant pas une fonction virtuelle, c'est la version de base qui est appelée. C'est donc l'expression function() dans base::call_function qui nous intéresse, donc le type de this dans cette expression. base base::function (virtuelle) derived_2 L'expression utilise une variable par pointeur (this), la fonction est virtuelle, l'appel utilise donc le type dynamique (base) et toujours en l'absence de résolution, la fonction appelée est base::function
call_with_derived(derived&d_) : d_.function(). Intéressons nous à d_ derived derived::function (non virtuelle) - L'expression utilise une fonction non virtuelle, l'appel utilise donc le type statique (derived), l'appel est donc résolu à la compilation vers derived::function
call_with_derived(derived&d_) : d_.call_function(); aboutit à l'appel de base::call_function contenant l'expression qui nous intéresse function();. Nous devons regarder nous intéresser à this qui est l'objet sur lequel la fonction est appelée. base base::function (virtuelle) derived_2 L'expression utilise une variable par pointeur (this), la fonction est virtuelle, l'appel utilise donc le type dynamique (derived_2) mais la seule spécialisation disponible est base::function.


XVI-B. Masquage d'une fonction non virtuelle par une fonction virtuelle

A la précédente section, nous avons vu qu'une fonction non virtuelle peut masquer une fonction virtuelle d'une classe de base, l'inverse est vrai :

Masquage d'une fonction non virtuelle par une fonction virtuelle
Sélectionnez
#include <iostream>
struct base
{
   void function()
   {
      std::cout<<"base::function\n";
   }
   void call_function()
   {
      function();
   }
};

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

struct derived_2 : public derived
{
   void call_function()
   {
      function();
   }
};

void call_with_base(base&b_)
{
   b_.function();
   b_.call_function();
}

void call_with_derived(derived&d_)
{
   d_.function();
   d_.call_function();
}

int main()
{
   base b;
   std::cout<<"base\n";
   b.function();
   b.call_function();
   call_with_base(b);

   derived d;
   std::cout<<"\nderived\n";
   d.function();
   d.call_function();
   call_with_base(d);
   call_with_derived(d);

   derived_2 d2;
   std::cout<<"\nderived_2\n";
   d2.function();
   d2.call_function();
   call_with_base(d2);
   call_with_derived(d2);

   return 0;
}

A titre d'exercice, je laisse le soin au lecteur d'expliquer les différents appels en reprenant nos grilles explicatives présentées en début de section.

XVI-C. Des fonctions pas totalement masquée.

En fait, la fonction n'était pas vraiment masquée mais nous nous sommes appuyés sur l'utilisation d'un argument par défaut (int=0) pour insérer une confusion dans les appels. En fait, si nous rajoutons une spécialisation de la fonction virtuelle de base, le résultat obtenu change encore de nature :

Spécialisation d'une fonction virtuelle masquée :
Sélectionnez
struct derived_2 : public derived
{
   void function()
   {
      std::cout<<"derived_2::function\n";
   }
   void call_function()
   {
      function();
   }
};

Avec cette définition, la sortie produite devient :

 
Sélectionnez
derived_2
derived_2::function
derived_2::function
derived_2::function
derived_2::function
derived::function
derived_2::function

Malgré l'absence du mot-clé virtual, la fonction derived_2::function est une spécialisation de la fonction de la classe de base virtual void base::function. Le mot-clé virtual comme nous l'avons vu n'étant plus nécessaire (mais fortement recommandé) dans les classe dérivée.

XVI-D. Ramener un symbole : using

Pour ceux qui arrivent encore à suivre, rajoutons un nouveau chemin dans ce dédale : le mot-clé using. Ce mot clé permet (entre autre) d'amener un symbole dans la portée courante. Dans notre exemple, nous pouvons dans derived_2 réintroduire base::function en 'sautant' la définition de derived::function aboutissant à un nouveau masquage de cette dernière :

Ramener un symbole avec using :
Sélectionnez
struct derived_2 : public derived
{
   using base::function;
   void call_function()
   {
      function();
   }
};

Avec ce petit rajout, tous les appels de function sur des variables de type statique derived_2 sont résolus en considérant base::function et non derived::function bien que derived_2 ne contienne aucune définition spécifique de cette fonction.
Ainsi, la sortie produite pour :

Résultat lorsque la fonction a été ramenée au niveau public :
Sélectionnez
   derived_2 d2;
   std::cout<<"\nderived_2\n";
   d2.function();
   d2.call_function();
   call_with_base(d2);
   call_with_derived(d2);

devient :

 
Sélectionnez
derived_2
base::function
base::function
base::function
base::function
derived::function
base::function

XVI-E. Que conclure ?

Ce petit chapitre n'avait pas comme objectif de semer la confusion sur la façon dont sont résolus les appels de fonction mais de montrer que quelques inattentions peuvent rendre un code obscur. La meilleure façon de s'en sortir n'est pas de connaître sur le bout des doigts les différents comportements mais de rechercher la simplicité. Ici, la simplicité veut qu'une classe dérivée n'introduise pas un symbole qui peut créer un conflit dans la classe parent :

Eviter d'introduire des masquages de fonctions !

XVII. Fonctions virtuelles et fonctions génériques (template)

XVII-A. Fonctions template

La question est rapidement traitée : il n'est pas possible de définir comme virtuelle une fonction générique dans une classe :

Les fonctions génériques ne peuvent être virtuelles :
Sélectionnez
struct base
{
   template<class T>
   virtual void function(); // Erreur : les modèles de fonction membre
                            // ne peuvent pas être virtuels
};

Réciproquement, une fonction générique d'une classe dérivée ne spécialise pas une fonction virtuelle de la classe de base :

Une fonction générique ne spécialise pas une fonction virtuelle :
Sélectionnez
#include <iostream>
struct base
{
   virtual void function(int)
   {
      std::cout<<"base::function\n";
   }

   virtual ~base(){}
};

struct derived : public base
{
   template<class T>
   void function(T)
   {
      std::cout<<"derived::function<T>\n";
   } // ne spécialise pas la fonction virtuelle de la classe de base

};
template<> void derived::function<int>(int) // même une spécialisation explicite 
         // de la fonction générique avec les bons paramètres 
         // ne spécialise pas la fonction virtuelle parent
{
   std::cout<<"derived::function<int>\n";
}


int main()
{
   derived d;
   base &rb = d;
   rb.function(1);
   return 0;
}

XVII-B. Fonctions virtuelles dans des classes génériques

Rien n'empêche de définir une fonction virtuelle dans une classe générique :

Une classe générique peut définir des fonctions virtuelles :
Sélectionnez
template<class T>
struct base
{
   virtual void function(T);
   virtual ~base(){}
};

La classe dérivée peut spécialiser la fonction virtuelle de la classe de base :

Spécialisations des fonctions virtuelles d'une classe générique dans les classes dérivées :
Sélectionnez
#include <iostream>
template<class T>
struct base
{
   virtual void function(T)
   {
      std::cout<<"base<T>::function\n";
   }
   virtual ~base(){}
};

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

template<class T2>
struct derived_t  : public base<T2>
{
   virtual void function(T2)
   {
      std::cout<<"derived_t::function\n";
   }
};

int main()
{
   derived d;
   base<int> &rd = d;
   rd.function(1);
   derived_t<double> dt;
   base<double> &rdt = dt;
   rdt.function(1.);
   return 0;
}

Bien sûr pour que la fonction soit une spécialisation de la classe de base, elle doit avoir la même signature :

La spécialisation d'une fonction virtuelle doit avoir la même signature :
Sélectionnez
#include <iostream>
template<class T>
struct base
{
   virtual void function(T)
   {
      std::cout<<"base<T>::function\n";
   }
   virtual ~base(){}
};

struct derived : public base<int>
{
   virtual void function(int const &)// ce n'est pas une spécialisation de 
          // la classe de base int et int const & sont des types différents
   {
      std::cout<<"derived::function\n";
   }
};

template<class T2>
struct derived_t  : public base<T2 const &>
{
   virtual void function(T2) // ce n'est pas une spécialisation de 
          // la classe de base T2 et T2 const & sont des types différents
   {
      std::cout<<"derived_t::function\n";
   }
};

int main()
{
   derived d;
   base<int> &rd = d;
   rd.function(1);
   derived_t<double> dt;
   base<double const&> &rdt = dt;
   rdt.function(1.);
   return 0;
}

Le retour covariant reste valide :

Les règles de covariance sont les mêmes :
Sélectionnez
#include <iostream>


template<class T>
struct base
{
   virtual T& function()
   {
      std::cout<<"base<T>::function\n";
      static T ret;
      return ret;
   }
   virtual ~base(){}
};

struct return_type_base
{
   virtual ~return_type_base(){}
};

struct return_type_derived : public return_type_base
{};

struct derived : public base<return_type_base>
{
   virtual return_type_derived& function()
   {
      std::cout<<"derived::function\n";
      static return_type_derived ret;
      return ret;
   }
};

template<class T2>
struct derived_t  : public base<T2>
{
   virtual return_type_derived& function()
   {
      std::cout<<"derived_t::function\n";
      static return_type_derived ret;
      return ret;
   }
};

int main()
{
   derived d;
   base<return_type_base> &rd = d;
   rd.function();
   derived_t<return_type_base> dt;
   base<return_type_base> &rdt = dt;
   rdt.function();
   return 0;
}

XVIII. Fonctions virtuelles et amitié (friend)

Les membres déclarées privés ou protégés sont visibles uniquement par la classe (private) ou que par la classe et ses classes dérivées (protected). L'amitié, introduite par le mot-clé friend permet de donner cet accès explicitement à une classe ou une fonction tierce :

L'amitié peut être donnée à une classe en entier ou une fonction seulement :
Sélectionnez
struct a_type;

struct a_dummy_type
{
   void a_trusted_function(a_type &);
   void a_non_trusted_function(a_type&);
};
struct a_trusted_type;

struct a_type
{
   friend struct a_trusted_type; // amitié donnée à toute la classe
   friend void a_dummy_type::a_trusted_function(a_type &); // amitié donnée uniquement à une fonction
   private :
   int membre;
};

void a_dummy_type::a_trusted_function(a_type &a_)
{
   a_.membre = 0; // OK la fonction est amie
}
void a_dummy_type::a_non_trusted_function(a_type&a_)
{
  a_.membre = 0; // ERREUR : membre est privé et la fonction n'est pas amie
}

struct a_trusted_type
{
   void a_function(a_type a_)
   {
      a_.membre = 0; // OK la classe est amie
   }
};

L'amitié ne s'hérite pas : si une classe base est déclarée amie de la classe a_type, alors la classe dérivée derived n'est pas ami de la classe a_type par défaut :

L'amitié ne s'hérite pas :
Sélectionnez
struct base;
struct a_type
{
   friend struct base;
   private :
   int membre;
};

struct base
{
   void function(a_type &a_)
   {
      a_.membre = 0;
   }
};

struct derived : public base
{
   void a_derived_function(a_type &a_)
   {
      // a_.membre = 0; ERREUR l'amitié ne s'hérite pas
   }
};

De la même façon, l'amitié sur une fonction virtuelle ne se propage pas sur ses spécialisations :

L'amitié ne se propage pas sur les spécialisations :
Sélectionnez
struct a_type;
struct base
{
   virtual void function(a_type &a_);
};

struct a_type
{
   friend void base::function(a_type &a_);
   private :
   int membre;
};

void base::function(a_type &a_)
{
   a_.membre = 0;
}

struct derived : public base
{
   virtual void function(a_type &a_)
   {
      a_.membre = 0; // ERREUR l'amitié ne s'hérite pas
   }
};

L'amitié sur une fonction virtuelle est déjà un indicateur d'un probable problème de conception. Car, cette amitié ne se transmettant pas aux spécialisations des classes dérivées, cela créé une dissymétrie forte entre la fonction virtuelle de base et ses spécialisations dans les classes dérivées. Il y a tout lieu de se demander si la fonction doit être amie ou si la fonction doit être virtuelle.
L'exemple devient vraiment singulier avec une fonction virtuelle pure sans définition. Cela revient à déclarer amie une fonction qui ne sera jamais appelée.

L'amitié peut être utile dans différent cas pour ne pas rompre l'encapsulation des données tout en évitant de créer des accesseurs dans les classes : Les amis brisent-ils l'encapsulation ?Les amis brisent-ils l'encapsulation ?. Parfois, la classe amie peut être une classe de base ayant des fonctions virtuelles amenées à être spécialisée pour les classes de base. La conception de la classe doit alors différencier les parties nécessitant d'accéder à la classe amie des fonctions virtuelles devant être spécialisées. Elles n'ont pas le même rôle :

Marier amitié et abstraction en séparant les rôles :
Sélectionnez
struct a_type
{
   friend struct abstract_builder;
   private :
   int membre;
};

struct abstract_builder
{
   void build_a_type(a_type &a_)
   {// les fonctions non virtuelles utilisent l'amitié pour construire
    // l'objet
      a_.membre = create_an_int(); 
         // le rôle des fonctions virtuelles ne dépend pas de l'accès aux
         // membre privé
   }
   virtual ~abstract_builder(){}
   private:
   virtual int create_an_int()=0;
};


struct concrete_builder : public abstract_builder
{
   private:
   virtual int create_an_int()
   {
      return 0;
   }
};

XIX. Fonctions virtuelles et spécification d'exceptions

XIX-A. Rappel sur les exceptions

La déclaration d'une fonction peut indiquer les exceptions qu'elle est susceptible de lever en rajoutant le mot-clé throw suivi des types d'exceptions éventuellement levées :

Indication des types d'exceptions levées :
Sélectionnez
void a_function() throw(int, std::exception, std::string);

a_function ne peut lever que des exceptions de type int, std::exception ou std::string. Si elle lève une exception d'un autre type, alors la fonction std::unexpected() est appelée qui aboutit (à gros traits) à la terminaison du programme.

Exemple d'exceptions pouvant être levée :
Sélectionnez
void a_function() throw(int, std::exception, std::string)
{
   /* (...) */
   throw 2;// OK
   /* (...) */
   throw std::exception(); // OK
   /* (...) */
   throw std::string("error"); // OK
   /* (...) */
   throw 3.14; // Erreur à l'exécution aboutissant à une fin anormale du programme
}

Si la liste suivant le mot clé throw est vide alors la fonction s'engage à ne lever aucune exception :

Fonction ne levant pas d'exception :
Sélectionnez
void a_function() throw();

A contrario, si aucune directive throw n'est présente, la fonction peut lever n'importe quel type d'exception :

Fonction pouvant lever tout type d'exception :
Sélectionnez
void a_function();

XIX-B. Exceptions et hiérarchie de classes

Une fonction peut lever une exception d'un type dérivant d'un des types indiqués dans sa liste d'exceptions sans que ce ne soit une erreur :

Une fonction peut déclencher une exception d'une classe dérivée de la classe parent :
Sélectionnez
struct my_base_excpetion
{
};

struct my_derived_exception : public my_base_excpetion
{
};

void a_function() throw(my_base_excpetion)
{
   throw my_derived_exception(); // OK my_derived_exception dérive de my_base_excpetion
}

Les fonctions virtuelles permettent ainsi de spécifier un comportement dans la classe de base des exceptions, comportement spécialisé dans les classes dérivées :

Spécialisation d'un comportement d'une exception par les fonctions virtuelles :
Sélectionnez
#include <iostream>
#include <exception>

struct my_base_excpetion
{
   virtual void error()const =0;
};

struct my_derived_exception : public my_base_excpetion
{
   virtual void error() const
   {
      std::cout<<"my_derived_exception::error\n";
   }
};

void a_function() throw(my_base_excpetion)
{
   throw my_derived_exception();
}

int main()
{
   try{
      a_function();
   }
   catch(my_base_excpetion const &e_)
   {
      e_.error();
   }
   return 0;
}

A noter que c'est sur ce principe que sont bâties les exceptions de la STL : elles dérivent toutes de std::exception qui propose la fonction virtuelle what retournant une chaîne de caractère décrivant l'erreur.

Pour bénéficier des fonctions virtuelles définies dans les classes d'exceptions, les blocs catch doivent récupérer l'exception par référence constante.

XIX-C. Les exceptions d'une fonction virtuelle

Les fonctions virtuelles peuvent spécifier les exceptions éventuellement levées comme toutes autres fonctions :

Les fonctions virtuelles peuvent spécifier les exceptions qu'elles souhaitent lever :
Sélectionnez
struct base
{
   virtual void function() throw(int,double);
};

Une spécialisation de fonction virtuelle ne peut pas spécifier des types d'exceptions non précisés par la déclaration dans la classe de base :

Une spécialisation ne peut lever des exceptions non présentes dans la définition de base :
Sélectionnez
struct base
{
   virtual void function() throw(int,double);
};

struct derived : public base
{
   virtual void function() throw(int,char);// Erreur, char ne fait pas parti
                                           // des types d'exceptions précisés
                                           // dans la classe de base
};

Ni en spécifier plus :

Une spécialisation ne peut lever plus d'exception que la définition de base :
Sélectionnez
struct base
{
   virtual void function() throw(int,double);
};

struct derived : public base
{
   virtual void function() throw(int, double, std::exception); // Erreur
                                              //    std::exception n'est pas 
                                              // présent dans la classe de base
};

Donc à fortiori, elle ne peut pas les spécifier toutes si la classe de base en précise au moins une :

Si la classe de base contient une exigence throw, alors les spécialisations aussi :
Sélectionnez
struct base
{
   virtual void function() throw(int,double);
};

struct derived : public base
{
   virtual void function(); // Erreur : la classe de base limite le type 
                            // d'exception pouvant être levées
};

Elle doit spécifier les mêmes types d'exceptions :

Une spécialisation doit indiquer les mêmes exceptions que la version de base :
Sélectionnez
struct base
{
   virtual void function() throw(int,double);
};

struct derived : public base
{
   virtual void function() throw(int, double); // OK
};

ou moins :

Une spécialisation peut indiquer moins exceptions que la version de base :
Sélectionnez
struct base
{
   virtual void function() throw(int,double);
};

struct derived : public base
{
   virtual void function() throw(int); // OK
};

ou s'engager à n'en lever aucune :

Une spécialisation peut s'engager à ne lever aucune exception :
Sélectionnez
struct base
{
   virtual void function() throw(int,double);
};

struct derived : public base
{
   virtual void function() throw(); // OK
};

précédentsommairesuivant
Il est important de noter que dans ces trois cas l'appel est bien dynamique. C'est à dire que c'est à l'exécution que le lien vers base::function est effectué et non à la compilation. En anticipant un peu, nous pouvons dire que ces appels s'appuient sur la vtable dont l'entrée pour (virtual) function pointe vers base::function.

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.