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 retours n'ont rien en commun alors c'est une erreur :
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 :
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 :
Ce qui se traduit en code :
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();
}
;
Ce qui se traduit en code :
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
}
;
Ce qui se traduit en code :
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 :
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.
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 :
#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 :
#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 :
#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 :
#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 œuvre est privé :
#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 :
#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 :
#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 :
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 base. 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 :
#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<<
"
\n
derived
\n
"
;
d.function();
d.call_function();
call_with_base(d);
call_with_derived(d);
derived_2 d2;
std::
cout<<
"
\n
derived_2
\n
"
;
d2.function();
d2.call_function();
call_with_base(d2);
call_with_derived(d2);
return
0
;
}
Nous voici avec le résultat suivant :
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 :
base b;
std::
cout<<
"base
\n
"
;
b.function();
b.call_function();
call_with_base(b);
Pour ces trois appels :
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 :
derived d;
d.function();
d.call_function();
call_with_base(d);
call_with_derived(d);
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 :
derived_2 d2;
d2.function();
d2.call_function();
call_with_base(d2);
call_with_derived(d2);
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▲
À 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 :
#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<<
"
\n
derived
\n
"
;
d.function();
d.call_function();
call_with_base(d);
call_with_derived(d);
derived_2 d2;
std::
cout<<
"
\n
derived_2
\n
"
;
d2.function();
d2.call_function();
call_with_base(d2);
call_with_derived(d2);
return
0
;
}
À 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ées.▲
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 :
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 :
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 autres) 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 :
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 :
derived_2 d2;
std::
cout<<
"
\n
derived_2
\n
"
;
d2.function();
d2.call_function();
call_with_base(d2);
call_with_derived(d2);
devient :
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 :
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 :
#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 :
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 :
#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 :
#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 :
#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 :
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 :
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 :
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ée 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érents 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'encapsulationLes amis brisent-ils l'encapsulation ? ? Parfois, la classe amie peut être une classe de base ayant des fonctions virtuelles amenées à être spécialisées 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 :
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 :
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.
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 :
void
a_function() throw
();
A contrario, si aucune directive throw n'est présente, la fonction peut lever n'importe quel type d'exception :
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 :
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 :
#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
;
}
À 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 :
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 :
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 :
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 a fortiori, elle ne peut pas les spécifier toutes si la classe de base en précise au moins une :
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 :
struct
base
{
virtual
void
function() throw
(int
,double
);
}
;
struct
derived : public
base
{
virtual
void
function() throw
(int
, double
); // OK
}
;
ou moins :
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 :
struct
base
{
virtual
void
function() throw
(int
,double
);
}
;
struct
derived : public
base
{
virtual
void
function() throw
(); // OK
}
;