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

3DArchi

Les fonctions virtuelles sont un des piliers de la programmation orientée objet. En favorisant l'abstraction, elles permettent la construction d'architectures logicielles stables et évolutives. Cet article se propose d'explorer les fonctions virtuelles dans le langage C++ en abordant aussi bien les problèmes syntaxiques que les conséquences sémantiques de leur utilisation.
Vous pouvez lire cet article sur une seule page.
Vous pouvez lire cet article sur une version multipage.
Commentaires, conseils et réactions dans cette discussion : Commentez

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

- Auteur : 3DArchi

- Parution : 19 novembre 2009

- Mise à jour : 26 juin 2020

- Licence : Copyright ® 2009 3DArchi. Aucune reproduction, même partielle, ne peut être faite de ce site et 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.

Sommaire

- I. Les fonctions membres en C++

- I-A. Les fonctions membres statiques

- I-B. Les fonctions normales

- I-C. Les fonctions virtuelles

- I-D. La surcharge de fonction

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

- II. Type statique et type dynamique

- III. Types et appel de fonction

- IV. À quoi servent les fonctions virtuelles ?

- V. Première conséquence : comment bien déclarer son destructeur

- VI. Seconde conséquence : inlining de fonctions et fonctions virtuelles

- VII. Quand est construit le type dynamique ou quel est l'impact des appels de fonctions virtuelles dans un constructeur ?

- VIII. Que devient le type dynamique lors de la destruction de l'objet ou peut-on appeler des fonctions virtuelles dans un destructeur ?

- IX. Construction, destruction, fonctions virtuelles et multithreading

- X. Une vision plus avancée des appels de fonctions virtuelles dans les constructeurs et destructeurs

- XI. Et pour les fonctions virtuelles pures ?

- XI-A. Fonctions virtuelles pures, classes abstraites, classes concrètes

- XI-B. Appel d'une fonction virtuelle pure

- XI-C. Un destructeur virtuel pur

- XI-D. Appel d'une fonction virtuelle pure dans le constructeur/destructeur d'une classe abstraite

- XI-E. Fonctions virtuelles pures et constructeur/destructeur des classes concrètes

- XII. L'opérateur d'affectation : operator=

- XIII. Le retour covariant des fonctions virtuelles

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

- XV. Fonctions virtuelles et visibilité

- XVI. Fonction virtuelle et masquage de fonction

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

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

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

- XVI-D. Ramener un symbole : using

- XVI-E. Que conclure ?

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

- XVII-A. Fonctions template

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

- XVIII. Fonctions virtuelles et amitié (friend)

- XIX. Fonctions virtuelles et spécification d'exceptions

- XIX-A. Rappel sur les exceptions

- XIX-B. Exceptions et hiérarchie de classes

- XIX-C. Les exceptions d'une fonction virtuelle

- XX. Fonctions virtuelles et programmation par contrat

- XX-A. Un rapide rappel

- XX-B. Le principe de substitution de Liskov

- XX-C. Impact sur les invariants pour une fonction virtuelle

- XX-D. Impact sur les préconditions pour une fonction virtuelle

- XX-E. Impact sur les postconditions pour une fonction virtuelle

- XXI. Le pattern N.V.I.

- XXII. Informations sur les types (dynamiques et statiques) et conversions

- XXII-A. Types polymorphes

- XXII-B. Comment connaître le type dynamique d'une variable ?

- XXII-B-1. L'opérateur typeid

- XXII-B-2. Évaluation de l'expression

- XXII-B-3. La classe type_info

- XXII-B-4. Pourquoi récupérer le type dynamique ?

- XXII-C. Comment connaître le type statique d'une variable ?

- XXII-D. Conversions entre type de base et type dérivé

- XXII-D-1. Conversion du type dérivé vers le type de base

- XXII-D-2. Conversion du type de base vers le type dérivé

- XXII-D-3. Pourquoi faire une conversion d'un type de base vers un type dérivé

- XXIII. Comment ça marche ?

- XXIII-A. Qui définit la mise en œuvre ?

- XXIII-B. Résoudre l'appel dynamiquement : les tables virtuelles

- XXIII-C. Quelle entrée pour les fonctions virtuelles pures ?

- XXIII-D. Comment sont construites les tables virtuelles ?

- XXIII-E. Comment sont détruites les tables virtuelles ?

- XXIII-F. Qu'est-ce qu'un pointeur de fonction virtuelle ?

- XXIV. À retenir

- XXV. Un peu de lecture

- XXVI. Remerciements

I - Les fonctions membres en C++

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

- les fonctions membres statiques ;

- les fonctions membres normales ;

- les fonctions membres virtuelles.

I-A - Les fonctions membres statiques

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

Exemple de fonction membre statique :

struct my_type
{
static void s_function(); // fonction statique
};

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

Appel d'une fonction membre statique :

#include <iostream>
struct my_type
{
static void s_function(); // fonction membre statique

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

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

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

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

I-B - Les fonctions normales

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

Exemple de fonction membre normale :

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

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

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

Appel d'une fonction normale :

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

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

I-C - Les fonctions virtuelles

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

Exemple d'une fonction virtuelle :

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

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

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

Appel d'une fonction virtuelle :

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

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

Soit en résumé :

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

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

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

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

Exemple d'une fonction virtuelle pure :

struct my_type
{
virtual void a_function()=0; // fonction virtuelle pure
};

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

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

Toutes (ou presque) les fonctions peuvent être virtuelles

struct my_type
{
//   virtual my_type(); // erreur : un constructeur ne peut être virtuel
//   virtual static void s_function(); // erreur : une fonction ne peut être ET statique ET virtuelle
virtual ~my_type();// OK
virtual void function(); // OK
virtual my_type& operator+(my_type const&); // OK
};

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

La virtualité s'hérite :

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

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

Nous avons avec cet exemple :

base

derived

function_1

non virtuelle

non virtuelle

function_2

virtuelle

virtuelle

function_3

non virtuelle

virtuelle

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

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

I-D - La surcharge de fonction

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

- leur nombre d'arguments ;

- et/ou le type d'au moins un des arguments ;

- et/ou leur constance.

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

Surcharges d'une fonction membre :

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

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

Appels des différentes surcharges d'une fonction membre :

#include <iostream>
struct my_type
{
void function(double,double) // (1)
{
std::cout<<"(1)\n";
}
void function(double) // (2)
{
std::cout<<"(2)\n";
}
void function(int) // (3)
{
std::cout<<"(3)\n";
}
void function(int) const // (4)
{
std::cout<<"(4)\n";
}
void function(char&) // (5)
{
std::cout<<"(5)\n";
}
void function(char const &) // (6)
{
std::cout<<"(6)\n";
}
};

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

return 0;
}

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

- elles ne diffèrent que par leur type retour ;

- elles ont les mêmes arguments, le même type retour, mais l'une d'elles est statique ;

- elles ont les mêmes arguments, le même type retour, la même constance, mais l'une d'elles est virtuelle (ou virtuelle pure) et l'autre normale ;

- elles ont les mêmes arguments, le même type retour, la même constance, mais l'une d'elles est virtuelle et l'autre virtuelle pure ;

- un argument ne diffère qu'à cause d'un typedef ;

- elles ne diffèrent que par un const non significatif sur le type d'un argument.

- elles ne diffèrent que par les valeurs par défaut de leur(s) argument(s).

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

Surcharges interdites :

struct my_type
{

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

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

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

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

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

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

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

void function_6_bis(char *);
void function_6_bis(char * const); // Erreur : le const n'est pas significatif
// pour différencier les deux fonctions
void function_6_ter(char *);
void function_6_ter(char const *); // OK : char const * et char * sont bien
// deux types différents

void function_7(int );
void function_7(int =42); // Erreur : les définitions sont équivalentes
};

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

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

Héritage des membres de la classe parent :

struct base
{
void function(){}
};

struct derived : base
{
};

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

return 0;
}

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

Masquage des fonctions de base dans la classe dérivée :

struct base
{
void function(){}
};

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

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

return 0;
}

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

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

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

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

Tout redevient possible avec l'héritage :

struct base
{

void function_1();

static void function_2();

virtual void function_3();

virtual void function_4();

void function_5(int);

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

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

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

Quelle est la fonction appelée ?

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

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

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

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

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

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

return 0;
}

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

II - Type statique et type dynamique

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

Type statique et type dynamique de variables par valeur :

int a;
char c;
std::string s;
class a_class{/*[...]*/};
a_class an_object;
enum E_enumeration{/*[...]*/};
E_enumeration e;

Avec cet exemple, on a :

Type statique et dynamique d'une variable par valeur

variable

type statique

type dynamique

a

int

int

c

char

char

s

std::string

std::string

an_object

a_class

a_class

e

E_enumeration

E_enumeration

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

Type statique, type dynamique, héritage, pointeurs et références :

struct base {};
struct derived : public base {};

int main()
{

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

return 0;
}

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

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

variable

type statique

type dynamique

base b

base

base

derived d

derived

derived

base &rb = b

base

base

base &rd = d

base

derived

base *pb = &b

base

base

base *pd = &d

base

derived

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

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

Type de this :

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

return 0;
}

Type statique et dynamique dans une fonction membre

fonction

type statique de this

type dynamique de this

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

base

base

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

base

derived

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

derived

derived

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

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

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

Types statiques et dynamiques d'un pointeur :

struct base {};
struct derived : public base {};

int main()
{

base b;
derived d;
base *pd = &d;

return 0;
}

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

variable

type statique

type dynamique

pd

base*

derived*

*pd

base

derived

III - Types et appel de fonction

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

- la fonction est-elle virtuelle ou non ?

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

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

Quand est résolu l'appel ?

fonction non virtuelle

fonction virtuelle

appel sur un objet

COMPILATION

COMPILATION

appel sur une référence ou un pointeur

COMPILATION

EXÉCUTION

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

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

fonction non virtuelle

fonction virtuelle

appel sur un objet

type statique

type statique

appel sur une référence ou un pointeur

type statique

type dynamique

Un peu de code pour illustrer tout cela :

Résolution à la compilation ou à l'exécution des appels :

#include <iostream>

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

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

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

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

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

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

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

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

return 0;
}

Ce code produit comme sortie :

Sortie :

appels pour base b; :
base::function_1
base::function_2

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

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

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

Les types statiques et dynamiques sont :

Type statique et dynamique des différentes variables

variable

type statique

type dynamique

base b

base

base

derived d

derived

derived

base &rb = b

base

base

base &rd = d

base

derived

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

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

Appels sur b

fonction non virtuelle (fonction_1)

fonction virtuelle (fonction_2)

appel sur un objet

type statique : base::fonction_1

type statique : base::fonction_2

Appels sur d

appel sur un objet

type statique : derived::fonction_1

type statique : derived::fonction_2

Appels sur rb

appel sur une référence

type statique : base::fonction_1

type dynamique : base::fonction_2

Appels sur rd

appel sur une référence

type statique : base::fonction_1

type dynamique : derived::fonction_2

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

Références et pointeurs : même combat !

#include <iostream>

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

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

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

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

return 0;
}

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

Appel de fonction avec un pointeur de fonction membre :

#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_2

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

IV - À quoi servent les fonctions virtuelles ?

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

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

Le polymorphisme d'inclusion : un principe fondamental de l'objet !

#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.

V - Première conséquence : comment bien déclarer son destructeur

Prenons maintenant l'exemple suivant :

Erreur : un héritage public sans destructeur virtuel !

#include <iostream>

struct base
{
~base()
{
std::cout<<"destructeur de base\n";
}
};
struct derived :public base
{
~derived()
{
std::cout<<"destructeur de derived\n";
}
};

int main()
{
base *p_b = new derived;
std::cout<<"Destruction de l'objet :\n";
delete p_b;

return 0;
}

Que va-t-il se passer ? Le type statique de p_b est base. Son type dynamique est derived. Cependant, le destructeur n'étant pas virtuel, l'appel est résolu à la compilation en utilisant le type statique. Le destructeur appelé est donc base::~base. Ainsi, le code précédent produit dans les compilateurs les plus courants le résultat suivant :

Un résultat parmi d'autres :

Destruction de l'objet :
destructeur de base

Le destructeur de la classe derived n'est pas appelé ! Conclusion : le destructeur d'une classe de base pouvant être détruit de façon polymorphique DOIT être virtuel.

En fait, la norme est beaucoup plus sévère : l'appel d'un destructeur non virtuel sur un pointeur dont le type dynamique est différent du type statique provoque un comportement indéterminé(7) . Si les compilateurs se contentent de résoudre l'appel statiquement à la compilation, il est fort possible qu'un jour ou l'autre le même code fasse tout autre chose, car vous aurez changé de compilateur (portage vers une autre cible) ou parce que la nouvelle version de votre compilateur favori aura décidé de gérer cet aspect d'une façon toute différente. Le code présenté ci-dessus a un comportement indéterminé.

La règle précédente s'étend de façon plus globale :

Un destructeur d'une classe utilisée pour l'héritage public doit être virtuel s'il est public ou protégé s'il est non virtuel.

L'entrée de la F.A.Q. reprend cette problématique : Pourquoi le destructeur d'une classe de base doit-il être public et virtuel ou protégé et non virtuel ?

Le code précédent peut alors soit se décliner :

Interdire la destruction polymorphe :

#include <iostream>

struct base
{
protected:
~base()
{
std::cout<<"destructeur de base\n";
}
};
struct derived :public base
{
~derived()
{
std::cout<<"destructeur de derived\n";
}
};

int main()
{
base *p_b = new derived;
std::cout<<"Destruction de l'objet :\n";
delete p_b; // -> Erreur de compilation

return 0;
}

Et une erreur de compilation signale alors le problème.
S'il faut pouvoir détruire des objets de la classe derived depuis un pointeur de base, le destructeur doit être virtuel :

Autoriser la destruction polymorphe :

#include <iostream>

struct base
{
virtual ~base()
{
std::cout<<"destructeur de base\n";
}
};
struct derived :public base
{
virtual ~derived()
{
std::cout<<"destructeur de derived\n";
}
};

int main()
{
base *p_b = new derived;
std::cout<<"Destruction de l'objet :\n";
delete p_b;

return 0;
}

Le résultat est bien celui attendu :

Un résultat correct :

Destruction de l'objet :
destructeur de derived
destructeur de base

Suivre cette règle permet de garantir qu'une erreur de compilation signalera tout bug potentiel.
Corollaire : si une bibliothèque définit des classes ou des structures dont le destructeur est public et non virtuel alors cette classe ne doit pas être utilisée comme classe de base pour un héritage public.
Si on prend l'exemple de la STL, le destructeur de std::vector n'étant pas virtuel, cette classe ne doit pas servir de base à un héritage :

Erreur potentiellement dramatique :

class mon_vecteur : public std::vecteur<int>
{/*[...]*/
};
// code DANGEREUX et à éviter !

À l'inverse, std::iostream possède un destructeur virtuel. Cette classe peut servir comme classe de base pour un héritage public.

Dans le cadre d'un héritage privé, la problématique ne se pose pas. Il n'est pas possible d'avoir une référence (ou un pointeur) d'un type de base hérité en privé sur un objet dérivé (8) . On ne peut donc pas vouloir détruire polymorphiquement un type dérivé par une de ses interfaces privées :

Héritage privée :

struct base
{
public:
~base(){}
};
struct derived : private base
{
};
int main()
{
base *pb = new derived; // Erreur à la compilation !

return 0;
}

Si une classe ne définit pas explicitement de destructeur, alors le compilateur crée un destructeur implicite qui se contente d'appeler le destructeur de chaque membre de la classe ainsi que le destructeur de chaque classe de base. Le destructeur implicitement créé est public et non virtuel. Conclusion, il est potentiellement dangereux si la classe doit servir de classe de base pour un héritage. L'héritage est donc un des rares cas où il faut définir un constructeur explicite même si celui-ci est trivial (c'est-à-dire vide).

Le destructeur implicite créé par le compilateur étant public et non virtuel, une classe destinée à l'héritage doit définir un destructeur explicite public et virtuel ou protégé et non virtuel.

Un destructeur public DOIT être virtuel :

struct base
{
virtual ~base(){} // définition obligatoire si base va servir pour un héritage public !
};

C++0x : le mot-clé =default est introduit dans la future norme C++0x. Cela permet de définir une fonction dans la classe, mais sans lui donner d'implémentation explicite. Le compilateur génère une implémentation implicite telle que définie par la norme. Pour les destructeurs triviaux (c'est-à-dire, ne faisant explicitement rien), cette nouveauté est l'idéal :

Destructeur virtuel par défaut en C++0x :

struct base
{
virtual ~base()=default;// utilise la définition par défaut
// pour le destructeur virtuel de base
};

struct derived : public base
{
virtual ~derived()=default; // utilise la définition par défaut
// pour le destructeur virtuel de derived
};

Destructeur et constructeur sont les deux seules fonctions en C++ dont l'appel vers la classe de base est automatique. Pour prendre l'exemple du destructeur, lorsque le destructeur d'une classe dérivée est terminé, le destructeur de la classe de base est automatiquement appelé sans avoir à l'expliciter dans l'implémentation du destructeur :

Appel en cascade des destructeurs virtuels ou pas :

struct base
{
virtual ~base()
{
std::cout<<"~base\n";
}
};
struct derived: public base
{
virtual ~derived()
{
std::cout<<"~derived\n";
} // A la sortie, base::~base est appelé
};

int main()
{
derived d;

return 0;
} // A la sortie, le destructeur derived::~derived est appelé

Le fait que le destructeur soit virtuel ne change pas ce comportement. Le destructeur de la classe de base continue d'être appelé. La seule chose qui change est le premier appel dans le cadre d'une destruction polymorphe : celui du type dynamique de l'objet si le destructeur est virtuel (OK), indéterminé sinon.

VI - Seconde conséquence : inlining de fonctions et fonctions virtuelles

Le compilateur peut choisir d'inliner une fonction, c'est-à-dire de remplacer l'appel de celle-ci en recopiant directement le code correspondant à la fonction appelée au niveau de la fonction appelante (9) . C'est une optimisation qui permet d'accélérer le code puisqu'un appel de fonction (qui est toujours coûteux) est évité (10) . En C++, deux façons permettent de définir une fonction inline : en ajoutant le mot-clé inline devant la fonction ou en la définissant dans la déclaration de la classe :

Fonctions inline :

struct my_struct
{
inline void function_1();
void function_2()
{
// ...
}
protected : // ne plus oublier maintenant de définir son destructeur
// selon la politique souhaitée !
~my_struct()
{}
};

void my_struct::function_1()
{
// ...
}

int main()
{
my_struct var;
var.function_1(); // l'appel est remplacé par le corps de la fonction
var.function_2(); // l'appel est remplacé par le corps de la fonction

return 0;
}

Quel rapport avec le type statique, dynamique et les fonctions virtuelles ? Et bien, c'est le compilateur qui inline une fonction donc seuls les appels résolus à la compilation peuvent faire l'objet de cette optimisation. Si nous reprenons le tableau indiquant le moment de la résolution d'un appel, les cas où le compilateur peut inliner apparaissent immédiatement :

inlining possible ?

fonction non virtuelle

fonction virtuelle

appel sur un objet

COMPILATION : inline possible

COMPILATION : inline possible

appel sur une référence ou un pointeur

COMPILATION : inline possible

EXÉCUTION : inline impossible

Deux conséquences apparemment contradictoires :

Il est inutile de déclarer inline une fonction virtuelle.

En effet, cela ne sert à rien si la fonction est appelée avec une référence ou un pointeur puisque la résolution de l'appel se fait à l'exécution. Outre que c'est inutile, il est recommandé de ne pas le faire pour ne pas laisser croire à un relecteur du code que cela pourrait l'être et accessoirement pour montrer à ce relecteur que vous avez compris ce mécanisme.

Une fonction virtuelle peut être inlinée par le compilateur si elle est appelée dans un contexte non polymorphe.

Cela correspond à la première ligne et la seconde colonne de notre tableau : l'appel d'une fonction virtuelle sur un objet par valeur peut très bien être inliné puisque l'appel est résolu à la compilation :

Fonction virtuelle inlinée :

struct base
{
virtual ~base(){}
virtual void function()
{
// ...
}
};
struct derived : public base
{
virtual void function()
{// spécialisation
// ...
}
};

int main()
{
base b;
b.function(); // peut être inliné

derived d;
d.function(); // peut être inliné

base &rd = d;
rd.function(); // NE peut PAS être inliné

derived &rd2 = d;
rd2.function(); // NE peut PAS être inliné

return 0;
}

VII - Quand est construit le type dynamique ou quel est l'impact des appels de fonctions virtuelles dans un constructeur ?

Le type dynamique d'un objet en cours de construction est celui du constructeur en cours d'exécution et non de l'objet réellement construit.
Reprenons l'ordre de construction d'un objet tel que nous le trouvons présenté dans la F.A.Q. : Dans quel ordre sont construits les différents composants d'une classe ?

- le constructeur des classes de base héritées virtuellement en profondeur croissante et de gauche à droite ;

- le constructeur des classes de base héritées non virtuellement en profondeur croissante et de gauche à droite ;

- le constructeur des membres dans l'ordre de leur déclaration ;

- le constructeur de la classe.

Déroulons l'exemple suivant :

Type dynamique et construction d'un objet :

#include <iostream>

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

struct derived_1 : public base
{
derived_1()
{
std::cout<<"constructeur de derived_1\n";
}
};

struct derived_2 : public derived_1
{
derived_2()
{
std::cout<<"constructeur de derived_2\n";
}
};

int main()
{
derived_2 d2;
return 0;
}

L'ordre de construction de d2 donne comme appels :

Types statiques et dynamiques pendant la construction

Constructeur en cours d'exécution

type statique

type dynamique

base::base

base

base

derived_1::derived_1

derived_1

derived_1

derived_2::derived_2

derived_2

derived_2

Ce tableau fait tout de suite apparaître le problème des appels de fonctions virtuelles dans le constructeur : la fonction effectivement appelée sera celle définie dans la spécialisation la plus basse en cours de construction.
Illustrons avec un peu de code :

Appel de fonctions virtuelles dans un constructeur :

#include <iostream>

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

protected:
~base()
{
}
};

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

struct derived_2 : public derived_1
{
derived_2()
{
init();
}
virtual void init()
{
std::cout<<"derived_2::init\n";
}
};

int main()
{
derived_2 d2;
return 0;
}

Ce code produit comme sortie :

base::init
derived_1::init
derived_2::init

En effet, en reprenant le tableau sur le type dynamique en cours de construction, le déroulement s'explique aisément :

Types statiques et dynamiques pendant la construction

Constructeur en cours d'exécution

type dynamique

Spécialisation appelée

base::base

base

base::init

derived_1::derived_1

derived_1

derived_1::init

derived_2::derived_2

derived_2

derived_2::init

Appeler une fonction virtuelle dans un constructeur n'appelle pas la fonction la plus spécialisée de l'objet en cours de construction, mais celle disponible pour le constructeur en cours d'exécution !

VIII - Que devient le type dynamique lors de la destruction de l'objet ou peut-on appeler des fonctions virtuelles dans un destructeur ?

Le problème est symétrique lors de l'appel du destructeur, car les destructeurs sont appelés dans l'ordre inverse de l'appel des constructeurs : le type dynamique dans le destructeur coïncide, comme dans le constructeur, avec le type statique du destructeur en train d'être déroulé. Et donc la fonction virtuelle appelée est celle disponible pour le destructeur en cours d'exécution.
Soit en partant du code suivant :

Type dynamique et destruction d'un objet :

#include <iostream>

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

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

struct derived_2 : public derived_1
{
virtual ~derived_2()
{
exit();
}
virtual void exit()
{
std::cout<<"derived_2::exit\n";
}
};

int main()
{
derived_2 d2;
return 0;
}

Le code produit la sortie suivante :

derived_2::exit
derived_1::exit
base::exit

Cette sortie s'explique grâce à notre tableau indiquant le type dynamique en cours d'exécution :

Types statiques et dynamiques pendant la destruction

Destructeur en cours d'exécution

type dynamique

Spécialisation appelée

derived_2::~derived_2

derived_2

derived_2::exit

derived_1::~derived_1

derived_1

derived_1::exit

base::~base

base

base::exit

Nous pouvons donc tirer une conclusion similaire pour la destruction d'un objet à celle de la construction :

Appeler une fonction virtuelle dans un destructeur n'appelle pas la fonction la plus spécialisée de l'objet en cours de destruction, mais celle disponible pour le destructeur en cours d'exécution !

IX - Construction, destruction, fonctions virtuelles et multithreading

Les constructions et les destructions ne sont pas des opérations atomiques. Par conséquent, commencer à utiliser un objet dans un fil d'exécution T1 pendant qu'il est construit dans un fil d'exécution T2 peut révéler des surprises. En particulier, l'appel d'une fonction virtuelle peut ne pas correspondre à l'appel attendu si le type dynamique n'est pas correctement initialisé. Il en va de même pendant la phase de destruction. Un objet en train d'être détruit voit son type dynamique modifié lors de la remontée des appels des destructeurs. Ces situations de compétitions doivent être anticipées lors de l'utilisation de l'objet sous peine de comportement indéterminé.

X - Une vision plus avancée des appels de fonctions virtuelles dans les constructeurs et destructeurs

En fait, si l'approche type dynamique/type statique permet de facilement comprendre que l'appel d'une fonction virtuelle dans un constructeur ou un destructeur ne provoque pas l'appel de la spécialisation de la classe la plus dérivée de l'objet en cours de construction (resp. de destruction), la réalité est plus subtile.
En effet, la norme dit juste que l'appel d'une fonction virtuelle dans un constructeur (resp. destructeur) doit être résolu par l'appel de la spécialisation de la classe en train d'être construite (et non de l'objet en train d'être construit) ou d'une de ses classes de base.
Cette petite nuance permet à certains compilateurs d'optimiser les appels directs des fonctions virtuelles dans les constructeurs et les destructeurs. En effet, au moment de la compilation du constructeur (resp. destructeur), le compilateur connaît la classe en train d'être construite (resp. détruite) et toutes ses classes de base. Cet appel peut dès lors être traité par le compilateur comme il le ferait avec une fonction non virtuelle en utilisant une résolution statique. Et c'est bien le comportement observé avec un compilateur comme Visual C++ Express 2008 ou GCC 4.4.1 (MinGW) (11) . Les appels directs des fonctions virtuelles dans le constructeur et le destructeur sont identiques avec ces compilateurs à ceux de fonctions non virtuelles. Il s'agit d'une optimisation de certains compilateurs et non de la règle générale !

Certains compilateurs optimisent les appels directs des fonctions virtuelles dans le constructeur ou le destructeur en utilisant une résolution statique de cet appel.

Optimisation de l'appel direct d'une fonction virtuelle dans le constructeur/destructeur :

struct base
{
base()
{
do_init(); // certains compilateurs traitent cet appel comme une fonction non virtuelle
}
virtual void do_init()
{
std::cout<<"base::do_init\n";
}

virtual ~base(){}
};

Bien sûr ceci n'est pas possible lorsque la fonction virtuelle est appelée dans le constructeur (ou le destructeur) de façon indirecte :

Optimisation non réalisée pour un appel indirect :

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

virtual ~base(){}
};

Les appels indirects ne sont pas traités comme des fonctions non virtuelles. En effet, la fonction init peut être appelée dans un autre contexte que le constructeur (ou le destructeur). Cependant, le type utilisé dans le constructeur (ou le destructeur) pour la résolution de l'appel indirect d'une fonction virtuelle reste toujours le type du constructeur (resp. destructeur) en cours d'exécution et non le type de l'objet en cours de construction (resp. destruction). Nous verrons plus loin dans cet article (Comment ça marche ?), comment le compilateur arrive à respecter la norme dans ce cas.
Un peu de code pour montrer que l'appel indirect a bien le comportement attendu :

Différences dans la résolution dynamique entre la construction/destruction et pendant la vie 'normale' de l'objet :

#include <iostream>
struct base
{
base()
{
init();
}
void init()
{
do_init();
}
virtual void do_init()
{
std::cout<<"base::do_init\n";
}

virtual ~base(){}
};

struct derived : public base
{
derived()
{
init();
}
virtual void do_init()
{
std::cout<<"derived::do_init\n";
}
};
int main()
{
std::cout<<"construction de derived :\n";
derived d;
std::cout<<"appel de init apres la construction de l'objet :\n";
d.init();

return 0;
}

Ce code produit toujours la sortie :

construction de derived :
base::do_init
derived::do_init
appel de init apres la construction de l'objet :
derived::do_init

On revient à notre tableau sur le type dynamique courant lors d'une construction (resp. destruction) :

Types statiques et dynamiques pendant la construction

Constructeur en cours d'exécution

type dynamique

Spécialisation appelée dans base::init

base::base

base

base::do_init

derived::derived

derived

derived::do_init

Comme nous le verrons dans la section 'Comment ça marche ?', la résolution dynamique de l'appel des fonctions virtuelles a un cout. Pouvoir dans des cas précis comme ceux-ci dessus remplacer la résolution dynamique par une résolution statique permet de ne pas payer ce cout.

XI - Et pour les fonctions virtuelles pures ?

XI-A - Fonctions virtuelles pures, classes abstraites, classes concrètes

Revenons sur les fonctions virtuelles pures brièvement évoquées en début d'article. Syntaxiquement, une fonction virtuelle pure est une fonction virtuelle à laquelle est accolée =0 à la fin de la déclaration :

Fonction virtuelle pure :

struct base
{
void pure_function()=0;
virtual ~base(){}
};

Une fonction virtuelle pure définit une classe abstraite. Une classe abstraite ne peut pas être instanciée :

Une classe abstraite n'est pas instanciable :

struct abstract
{
virtual void pure_function()=0;
virtual ~abstract(){}
};
int main()
{
abstract b; // erreur
return 0;
}

Une classe définit une fonction virtuelle pure quand elle souhaite indiquer que les classes dérivées DOIVENT spécialiser cette fonction virtuelle pour pouvoir être instanciée. Une classe dérivant d'une classe abstraite et spécialisant les fonctions virtuelles pures est appelée classe concrète. Une classe concrète peut alors être instanciée :

Seule une classe concrète peut être instanciée :

struct abstract
{
virtual void pure_function()=0;
virtual ~abstract(){}
};
struct concrete : public abstract
{
virtual void pure_function() {}
};
int main()
{
concrete b; // OK
return 0;
}

Comme les fonctions virtuelles pures doivent être spécialisées par les classes concrètes, il est possible de ne pas définir d'implémentation pour une fonction virtuelle pure dans la classe abstraite :

Les fonctions virtuelles pures peuvent ne pas être définies dans la classe abstraite :

struct abstract
{
virtual void pure_function()=0; // pas de définition
virtual void function(); // doit être définie

virtual ~abstract(){}
};
struct concrete : public abstract
{
virtual void pure_function() {} // la définition dans la classe concrète peut suffire
// A noter que la spécialisation de la fonction
// dans la classe concrète DOIT être définie
virtual void function(){} // la définition dans la classe ne suffit pas pour
// une classe virtuelle non pure. Elle doit aussi être définie dans la
// classe de base
};

int main()
{
concrete c;
return 0;
}//Erreur à l'édition de lien : abstract::fonction non définie

Enfin, si une classe abstraite veut définir une fonction virtuelle pure alors elle doit le faire en dehors de la déclaration de la fonction :

Définition d'une fonction virtuelle pure à l'extérieur de la déclaration de la classe :

struct abstract
{
virtual void pure_function()=0;

virtual ~abstract(){}
};
void abstract::pure_function()
{// définition de la fonction
}

Visual C++ compile une fonction virtuelle pure définie dans la classe abstraite, mais c'est un écart à la norme. Le code suivant compile avec Visual C++, mais pas GCC :

Ecart de Visual C++ :

struct abstract
{
virtual void pure_function()=0
{// Compile avec Visual C++, mais pas avec GCC
}

virtual ~abstract(){}
};

S'il est impossible d'instancier une classe abstraite, rien en revanche n'interdit d'utiliser un pointeur ou une référence vers une classe abstraite :

Pointeur et référence vers une classe abstraite :

struct abstract
{
virtual void function()=0;

virtual ~abstract(){}
};
struct concrete : public abstract
{
virtual void function(){}
};

int main()
{
concrete c;
abstract &ra = c;
abstract *pa = new concrete; // On n'oublie pas notre destructeur virtuel...
delete pa;
return 0;
}

Enfin, les plus attentifs auront déjà déduit cette conséquence de l'impossibilité d'instancier une classe abstraite :

Le type dynamique d'une variable ne peut jamais être une classe abstraite en dehors du constructeur ou du destructeur.

En effet, le type dynamique est le type de l'objet réellement utilisé à l'exécution du programme. Or ne pouvant instancier de classe abstraite, l'objet réellement utilisé sera toujours une classe concrète dérivée de la classe abstraite. Nous avons vu que les deux seuls cas échappant à cette règle sont le constructeur et le destructeur dans lesquels types dynamique et statique coïncident.
En revanche, le type statique d'une variable peut être celui d'une classe abstraite. this à l'intérieur d'une fonction de la classe abstraite est un cas trivial où le type statique est celui de la classe abstraite. Mais il en va de même pour les références et les pointeurs de classe abstraite :

Types statiques et dynamiques dans les classes abstraites et concrètes :

struct abstract
{
virtual void pure_function()=0;
void a_function()
{// type statique == abstract
// type dynamique == type de la classe concrète dérivée
}

virtual ~abstract() {}
};
struct concrete : public abstract
{
virtual void pure_function() {}
};
int main()
{
concrete b;
abstract &rb = b; // le type statique de rb est abstract, le type dynamique est concrete
abstract *pb = &b; // le type statique de pb est abstract, le type dynamique est concrete
return 0;
}

XI-B - Appel d'une fonction virtuelle pure

L'appel d'une fonction virtuelle pure suit toujours les mêmes règles que celle d'une fonction virtuelle classique. Il est possible de l'appeler depuis n'importe quelle autre fonction :

Résolution dynamique de l'appel d'une fonction virtuelle pure :

#include <iostream>

struct base
{
void call_function()
{
function();
}
virtual void function()=0;

virtual ~base(){}

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

int main()
{
derived d;
std::cout<<"\nappels pour derived d; : \n";
d.function();
d.call_function();

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

return 0;
}

La résolution de l'appel utilise le type dynamique pour choisir la fonction à dérouler. Il s'agit donc de la version la plus spécialisée dans la classe la plus dérivée.

Comme nous avons vu que le type dynamique d'un objet ne peut jamais être celui d'une classe abstraite, il est aisé de comprendre la conclusion suivante :

La définition d'une fonction virtuelle pure dans une classe abstraite ne sera jamais exécutée par une résolution dynamique d'un appel.

Le seul moyen pour que cette définition soit appelée est de forcer l'appel en indiquant explicitement la classe abstraite dans l'appel :

Forcer l'appel à la définition de la classe de base :

#include <iostream>

struct base
{
virtual void function() const =0;
virtual ~base(){}
};
void base::function() const
{
std::cout<<"Appel de base::function\n";
}

struct derived :public base
{
virtual void function() const
{
std::cout<<"Appel de derived::function\n";
base::function(); // appel explicite de la classe parent
}
};

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

int main()
{
derived d;
call_function(d);
return 0;
}

Si la fonction virtuelle pure de la classe abstraite de base n'a pas de définition, alors la compilation échoue sur une erreur de lien :

Echec à l'édition de lien si la classe abstraite n'a pas de définition :

#include <iostream>

struct base
{
virtual void function() const =0;

virtual ~base(){}
};

struct derived :public base
{
virtual void function() const
{
std::cout<<"Appel de derived::function\n";
base::function(); // appel explicite de la classe parent : erreur de lien
}
};

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

int main()
{
derived d;
call_function(d);
return 0;
}

Le code précédent produit une erreur de lien.

On peut se demander pourquoi devrait-on avoir besoin de définir une fonction virtuelle pure dans la classe abstraite si elle n'est jamais implicitement appelée par la résolution dynamique. Mon opinion est qu'une telle définition est souvent une erreur de conception. Voyons les différents cas qui semblent nécessiter une définition dans la classe abstraite et regardons comment les contourner :

- La classe de base propose un comportement par défaut : si tel est le cas, alors c'est que la classe dérivée peut très bien n'utiliser que ce comportement par défaut et ne pas avoir de comportement spécifique. Dans ce cas, la fonction peut être uniquement virtuelle (pas pure), les classes dérivées n'ayant pas de comportement spécifique ne spécialisent pas la fonction virtuelle.

Avec une fonction virtuelle pure :

Une autre approche :

Une fonction virtuelle pure avec un comportement par défaut : pour quoi faire ?

#include <iostream>

struct base
{
virtual void function()=0;

virtual ~base(){}
};
void base::function()
{
std::cout<<"Comportement par defaut\n";
}

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

struct derived_2 :public base
{
virtual void function()
{
base::function();
}
};

Une fonction virtuelle suffit !

#include <iostream>

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

virtual ~base(){}
};

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

struct derived_2 :public base
{
// pas de spécialisation de function : utilise la version par défaut
};

- La classe de base factorise une partie du comportement : je préfère alors sortir le comportement factorisé dans une autre fonction spécifique protégée et que seules les classes dérivées peuvent alors appeler. Les responsabilités sont séparées et le code est plus clair.

Avec une fonction virtuelle pure :

Une autre approche :

Une fonction virtuelle pure avec un comportement factorisé : pour quoi faire ?

#include <iostream>

struct base
{
virtual void function()=0;

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

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

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

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

Une fonction virtuelle suffit !

#include <iostream>

struct base
{
virtual void function()=0;
protected :
void specific_recurrent_point()
{
std::cout<<"Comportement frequent\n";
}

virtual ~base(){}
};

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

struct derived_2 :public base
{
virtual void function()
{
std::cout<<"Comportement specialise ET \n";
specific_recurrent_point();
}
};

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

- Une partie doit toujours être déroulée lors de l'exécution de la fonction : si une partie doit toujours être déroulée, et que cette partie est dans le code d'une fonction virtuelle, alors on prend le risque qu'un jour une classe dérivée 'oublie' de faire l'appel vers la classe de base. Ce genre de bug arrive vite dès qu'un projet est conséquent : on oublie ce petit détail lorsqu'on doit intervenir dans l'urgence dans le code plusieurs mois après son développement, un nouveau venu n'aura pas forcément l'information (surtout si l'héritage est complexe, même une documentation dans le code pourra échapper au développeur). Je préfère pour plus de tranquillité définir une fonction non virtuelle qui déroule le code qui doit être appelée et utilise une fonction virtuelle pure et sans définition pour les parties qui doivent être spécialisée (voir le pattern N.V.I.) :

Avec une fonction virtuelle pure :

Une autre approche :

Une fonction virtuelle pure avec un comportement obligatoire : le pattern N.V.I. ?

struct base
{
virtual void function()=0;

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

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

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

Une fonction virtuelle suffit !

#include <iostream>

struct base
{
void function()
{
pre_function();
do_function();
post_function();
}

virtual ~base(){}

private:
void pre_function()
{
std::cout<<"Comportement necessaire avant\n";
}
void post_function()
{
std::cout<<"Comportement necessaire apres\n";
}
virtual void do_function()=0;

};

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

struct derived_2 :public base
{
virtual void do_function()
{
std::cout<<"Comportement specialise 2\n";
}
};

Il existe une seule bonne raison de déclarer une fonction virtuelle pure avec une définition : un destructeur virtuel pur.

XI-C - Un destructeur virtuel pur

Le destructeur comme toute fonction virtuelle peut être une fonction virtuelle pure. La différence est qu'un destructeur virtuel pur DOIT avoir une définition. En effet, le destructeur étant toujours appelé lors de la destruction de l'objet, il doit avoir un corps même vide. Ou dit autrement, les destructeurs doivent toujours avoir une définition qu'ils soient virtuels ou non, cette définition étant soit donnée par le code soit implicitement construite par le compilateur dans le cas des destructeurs implicites.
Définir un destructeur virtuel pur est parfois la seule façon de définir une classe abstraite si les autres fonctions n'ont pas lieu d'être virtuelle pure. Comme la définition d'un corps est obligatoire et que son appel est implicite, cela ne pose pas de problème.

Un destructeur virtuel pur pour une classe abstraite :

struct base
{
virtual ~base()=0; // la classe est abstraite, car le destructeur est virtuel pur
};
base::~base()
{ // la définition du destructeur est obligatoire même s'il est pur.
}

En C++0x, le mot-clé =default est une définition de fonction. Comme une fonction virtuelle pure ne peut être définie dans la classe, cela doit se faire à l'extérieur :

Destructeur virtuel par défaut en C++0x :

struct base
{
virtual ~base()=0;
};
base::~base()=default; // définition à l'extérieur de la classe

XI-D - Appel d'une fonction virtuelle pure dans le constructeur/destructeur d'une classe abstraite

La norme est catégorique pour les appels des fonctions virtuelles pures dans le constructeur ou le destructeur :

L'appel d'une fonction virtuelle pure dans le constructeur ou le destructeur d'une classe abstraite produit un comportement indéterminé !

Cette règle tombe sous le bon sens. Rappelez-vous une fonction virtuelle pure peut ne pas avoir de définition dans une classe abstraite. Dès lors, la règle pour les fonctions virtuelles stipulant que c'est la fonction définie dans le constructeur en cours de construction qui est appelée ne peut être étendue puisque cette définition peut ne pas exister. Afin de ne pas créer d'ambiguïté, la règle ci-dessus qui a l'avantage de la simplicité a été introduite dans la norme : l'appel d'une fonction virtuelle pure dans une classe abstraite provoque un comportement indéterminé.
Si cette règle a l'avantage de la simplicité, elle a l'inconvénient qu'un comportement indéterminé peut révéler des surprises. Regardons ainsi comment cette règle est mise en œuvre dans 2 compilateurs différents : Visual C++ et GCC.

Prenons un premier exemple simple : une fonction virtuelle pure sans définition dans la classe abstraite appelée directement dans le constructeur (le comportement est le même si l'appel a lieu dans le destructeur)

Appel direct d'une fonction virtuelle pure sans définition dans un constructeur

#include <iostream>
struct base
{
base()
{
do_init();
}
virtual void do_init()=0;
virtual ~base(){}
};

struct derived : public base
{
derived()
{
}
virtual void do_init()
{
std::cout<<"derived::do_init\n";
}
};
int main()
{
derived d;
return 0;
}

À la compilation, Visual C++ est muet comme une carpe, mais GCC nous met la puce à l'oreille :

warning : abstract virtual 'virtual void base::do_init()' called from constructor

Mais le point le plus intéressant et sur lesquels les deux compilateurs se rejoignent est l'édition de lien : Visual C++ et GCC provoquent une erreur de lien :

Visual C++ : symbole externe non résolu « public: virtual void __thiscall base::do_init(void) »
GCC : undefined reference to `base::do_init()'

Il est donc impossible de produire un code avec un tel comportement indéterminé !
Cette erreur à l'édition de lien se comprend au regard de l'optimisation des appels des fonctions virtuelles dans le constructeur (resp. destructeur). Cette optimisation permet une résolution statique de l'appel par le compilateur. Il insère donc tout simplement un appel vers base::do_init dans le constructeur. Or comme cette fonction n'est définie nulle part, l'édition de lien échoue. C'est bien un appel vers base::do_init qui est tenté et non vers derived::do_init,car la règle stipule bien que le compilateur doit considérer le constructeur en cours d'exécution.

Un second exemple plus embêtant. Considérons maintenant l'appel indirect d'une fonction virtuelle pure dans le constructeur (comme toujours, le comportement est le même dans le destructeur) :

Appel indirect d'une fonction virtuelle pure sans définition dans un constructeur

#include <iostream>
struct base
{
base()
{
init();
}
void init()
{
do_init();
}
virtual void do_init()=0;
virtual ~base(){}
};

struct derived : public base
{
derived()
{
}
virtual void do_init()
{
std::cout<<"derived::do_init\n";
}
};
int main()
{
derived d;
return 0;
}

Effet démo garanti : ce code provoque une erreur à l'exécution avec les deux compilateurs. Ils ont l'amabilité d'indiquer l'erreur de façon très explicite :

GCC : pure virtual method called
Visual C++ : R6025
- pure virtual function call

L'appel indirect d'une fonction virtuelle pure dans un constructeur ou un destructeur d'une classe abstraite provoque une erreur à l'exécution avec Visual C++ et GCC.

Un troisième exemple : appelons directement une fonction virtuelle pure dans une classe abstraite, mais en ayant défini cette fonction :

Appel direct d'une fonction virtuelle pure avec définition dans un constructeur

#include <iostream>
struct base
{
base()
{
do_init();
}
virtual void do_init()=0;
virtual ~base(){}
};
void base::do_init()
{
std::cout<<"base::do_init\n";
}

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

int main()
{
derived d;
return 0;
}

Surprise ! Ce code compile avec Visual C++ et GCC (modulo l'avertissement de l'appel d'une fonction virtuelle pure dans le constructeur) sans problème et s'exécute sans planter.
L'explication est analogue à notre premier exemple : les compilateurs ont résolu l'appel statiquement et donc inséré un appel directement vers base::do_init. Cette fonction étant cette fois-ci bien définie, l'édition de lien a pu aboutir. À l'exécution, la fonction a tout simplement été déroulée.

Enfin, un dernier exemple : considérons l'appel indirect d'une fonction virtuelle pure avec une définition dans la classe abstraite.

Appel indirect d'une fonction virtuelle pure avec définition dans un constructeur

#include <iostream>
struct base
{
base()
{
init();
}
void init()
{
do_init();
}
virtual void do_init()=0;
virtual ~base(){}
};
void base::do_init()
{
std::cout<<"base::do_init\n";
}

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

int main()
{
derived d;
return 0;
}

Ici, nous retrouvons le comportement observé dans le second exemple à savoir un plantage à l'exécution indiquant l'appel d'une fonction virtuelle pure dans le constructeur :

GCC : pure virtual method called>
Visual C++ : R6025
- pure virtual function call

Soit pour résumer, nous avons avec Visual C++ et GCC les comportements suivants pour un appel de fonction virtuelle pure dans un constructeur ou un destructeur :

Comportement des compilateurs GCC et Visual C++ lors de l'appel d'une fonction virtuelle pure dans un constructeur ou un destructeur de la classe abstraite

Fonction virtuelle pure sans définition

Fonction virtuelle pure avec définition

appel direct

erreur de lien

OK

appel indirect

erreur à l'exécution

erreur à l'exécution

Ceci avait pour seul but de montrer qu'un comportement déclaré comme indéterminé par la norme se révèle effectivement varié et subtil. Vos codes ne doivent pas transiger avec cette règle élémentaire :

Une classe abstraite ne doit pas appeler de fonction virtuelle pure directement ou indirectement dans son constructeur ou son destructeur.

XI-E - Fonctions virtuelles pures et constructeur/destructeur des classes concrètes

À partir du moment où une fonction virtuelle pure reçoit une spécialisation dans une classe concrète, alors elle se comporte comme une fonction virtuelle classique. La fonction appelée est celle du constructeur en train de s'exécuter et non de l'objet en train d'être construit :

Appel d'une fonction virtuelle pure dans une classe concrète l'ayant spécialisée

#include <iostream>
struct base
{
base()
{}
void init()
{
do_init();
}
virtual void do_init()=0;
virtual ~base(){}
};

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

struct derived_2 : public derived
{
derived_2()
{
init();
}
virtual void do_init()
{
std::cout<<"derived_2::do_init\n";
}
};

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

Si init est bien une fonction de la classe abstraite base, l'appel effectif à init se fait bien dans le constructeur des classes concrètes derived et derived_2. Il ne faut pas tenir compte de la classe qui fait l'appel, mais bien du constructeur (resp. destructeur) qui est en train de se dérouler.

Il est possible d'appeler une fonction virtuelle pure dans le constructeur ou le destructeur d'une classe concrète l'ayant spécialisée. Le comportement est le même que pour une fonction virtuelle classique : c'est la version du constructeur/destructeur en train d'être exécuté qui est appelée et non celle de l'objet en train d'être construit.

XII - L'opérateur d'affectation : operator=

L'opérateur d'affectation peut être déclaré virtuel comme toutes les autres fonctions. On distingue deux opérateurs d'affectation : l'opérateur de 'conversion' et l'opérateur de 'copie'.
Le premier prend en paramètre un type différent de la classe pour laquelle il est défini :

Opérateur = avec sémantique de conversion :

struct a_type
{
a_type& operator=(int i_);
};

Il permet d'initialiser tout ou partie d'un objet d'un type donné avec la valeur d'un autre type. Définir comme virtuel et le spécialiser au besoin dans les classes dérivées ne posent pas de problèmes particuliers pour ce type d'affectation.
L'opérateur d'affectation avec une sémantique de copie prend en argument le même type que la classe pour laquelle il est défini :

Opérateur = avec sémantique de copie :

struct a_type
{
a_type& operator=(a_type rhs_);
};

D'ailleurs, pour ne laisser aucune confusion sur le fait qu'il s'agit bien d'un opérateur de copie, on utilise souvent l'idiome copy and swap :

Copy and swap :

struct a_type
{
a_type& operator=(a_type rhs_) // la copie est fait lors du passage de l'argument
{
std::swap(membre_1, rhs_.membre_1); // swap des membres
std::swap(membre_2, rhs_.membre_2);
/*[...] */
std::swap(membre_n, rhs_.membre_n);

return *this;
}
};

L'opérateur d'affectation de copie d'une classe virtuelle ne spécialise pas celui de la classe de base :

Ceci n'est pas une spécialisation :

#include <iostream>

struct base
{
virtual base& operator=(base)
{
std::cout<<"base::operator=\n";
return *this;
}
virtual ~base(){}
};

struct derived : public base
{
virtual derived& operator=(derived)
{
std::cout<<"derived::operator=\n";
return *this;
}
};

int main()
{
base b1;
base b2;
b1 = b2; // base::operator=
derived d1;
derived d2;
d1 = d2; // derived::operator=

base &rd1 = d1;
base &rd2 = d2;
rd1 = rd2; // base::operator=
rd1 = d1;  // base::operator=
return 0;
}

Pourquoi ? Tout simplement, car les signatures sont différentes dans la classe dérivée et la classe de base. La première prend en argument un type base. La seconde prend en argument un type derived. La résolution dynamique ne se fait que sur l'objet appelant l'opérateur, pas sur l'argument.
Pour avoir une spécialisation, il faut que la classe dérivée prenne en argument un base :

Spécialiser l'opérateur de base ?

#include <iostream>

struct base
{
virtual base& operator=(base)
{
std::cout<<"base::operator=\n";
return *this;
}
virtual ~base(){}
};

struct derived : public base
{
virtual derived& operator=(derived)
{
std::cout<<"derived::operator=(derived)\n";
return *this;
}
virtual derived& operator=(base) // spécialisation la version de la classe de base
{
std::cout<<"derived::operator=(base)\n";
return *this;
}
};

int main()
{
base b1;
base b2;
b1 = b2; // base::operator=
derived d1;
derived d2;
d1 = d2; // derived::operator=  (1)

base &rd1 = d1;
base &rd2 = d2;
rd1 = rd2; // base::operator= (2)
rd1 = d1;  // base::operator= (3)
return 0;
}

Certes, maintenant la classe spécialise bien l'opérateur défini dans la classe de base. Mais cette solution ne peut être satisfaisante. En effet, dans le cas (1) et dans les cas (2) et (3), on souhaite affecter à un objet derived un autre objet derived. Mais passer par une référence de la classe de base nous fait perdre l'information sur le type effectif de l'argument. On a donc deux appels différents selon (1) ou selon (1) et (2).
Quelle conclusion tirer ? En fait l'opérateur d'affectation de copie met en évidence un problème plus fondamental en conception objet concernant les classes servant à l'héritage et utilisant les fonctions virtuelles. Ces classes ont souvent une sémantique d'entité. Et une sémantique d'entité, comme on le voit, se marie mal avec une sémantique de copie.

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 :

La spécialisation d'une fonction virtuelle ne peut retourner un type vraiment différent :

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 :

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 chemin et se spécialisent en même temps

Ce qui se traduit en code :

Retours covariants corrects :

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

Ce qui se traduit en code :

Un type de retour non covariant est une erreur :

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

Ce qui se traduit en code :

Un type de retour non covariant est une erreur :

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 :

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 :

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

#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 :

#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 :

#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 :

#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é :

Spécialisation d'une fonction virtuelle héritée indirectement en privée :

#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 :

#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 :

#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 :

Masquage de fonction :

#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 :

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 :

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

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

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

À 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

#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;
}

À 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 :

Spécialisation d'une fonction virtuelle masquée :

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 :

Ramener un symbole avec using :

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 :

derived_2 d2;
std::cout<<"\nderived_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 :

Les fonctions génériques ne peuvent être virtuelles :

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 :

#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 :

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 :

#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 :

#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 :

#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 :

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 :

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 :

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'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 :

Marier amitié et abstraction en séparant les rôles :

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 :

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 :

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 :

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 :

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 :

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 :

#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 :

Les fonctions virtuelles peuvent spécifier les exceptions qu'elles souhaitent lever :

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 :

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 :

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 :

Si la classe de base contient une exigence throw, alors les spécialisations aussi :

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 :

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 :

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 :

struct base
{
virtual void function() throw(int,double);
};

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

XX - Fonctions virtuelles et programmation par contrat

XX-A - Un rapide rappel

Le but n'est pas de présenter la programmation par contrat (13). Si celle-ci vous est totalement inconnue, alors il est temps de vous y mettre. La programmation par contrat est un puissant outil de conception pour réaliser des classes sûres et simples clarifiant les responsabilités de chacun des acteurs d'un échange. Contentons-nous de rappeler les éléments dont nous allons avoir besoin.
Un invariant est une propriété d'un objet qui doit rester valide pendant toute la durée de vie de l'objet ou pour être plus précis dès que cet objet est utilisable par un tiers. Par exemple, un invariant de la classe std::vector est : size()<=capacity(). Cela signifie qu'un vecteur doit toujours avoir un nombre d'éléments au plus égal au nombre total d'éléments qu'il peut contenir.
Une précondition d'une fonction est une propriété qui doit être vérifiée pour que l'appel de la fonction garantisse le contrat. Par exemple, toujours pour notre std::vector, une précondition pour appeler std::vector::pop_back est que size()>=1. On ne peut supprimer le dernier élément que si le vecteur en contient au moins un. C'est à l'appelant d'une fonction de s'assurer que les préconditions sont remplies avant de solliciter cette fonction sur un objet. Les préconditions peuvent porter sur les paramètres de la fonction ou sur l'état de l'objet.
Une postcondition est une propriété que garantit la fonction à la fin de son exécution. Par exemple, std::vector<T>::push_back garantit que size()>=1. La postcondition peut même être formulée plus précisément : size() = old size() + 1. La postcondition garantit que si on demande à rajouter un élément dans le vecteur, alors la taille du vecteur augmente de un.
Si une fonction est appelée sur un objet respectant ses invariants avec des préconditions invalides, alors le bug est à chercher du côté de l'appelant. Si au retour d'une fonction appelée avec un objet respectant ses invariants et en remplissant les préconditions, les postconditions ne sont pas remplies, alors le bug est à chercher dans l'implémentation de la fonction.
Si une fonction est appelée sur un objet en ne respectant pas les préconditions, alors la fonction n'a pas à respecter le contrat : elle peut légitimement retourner un objet qui ne respecte plus les invariants ou ne pas respecter les postconditions en sortie. Ce n'est pas un bug de la fonction, mais une erreur lors de l'appel. À noter que cette situation a toutes les chances de se traduire à un moment ou un autre par au mieux un plantage au pire des résultats erronés.
Si une fonction est appelée sur un objet respectant les invariants et en respectant les préconditions mais que, pour une autre raison, la fonction ne peut garantir les postconditions alors elle doit lever une exception.
Le client désigne celui qui fait l'appel à une fonction d'un objet. Le fournisseur désigne la classe qui implémente la fonction.

XX-B - Le principe de substitution de Liskov

Le principe de substitution de Liskov énonce que partout où dans une expression valide on trouve un objet d'un type B, l'expression doit rester valide si on remplace l'objet par un autre objet de type D avec D dérivant publiquement de B. Avec les fonctions non virtuelles alors le problème est moindre, car leur résolution statique implique l'appel de la fonction définie dans la classe de base donc en respectant ses préconditions, celle-ci assure alors les postconditions. Avec une fonction virtuelle, lorsqu'un objet de la classe de base est substitué par un objet de la classe dérivée, la fonction appelée est celle de la classe dérivée. Se posent alors les questions suivantes :

- Que se passe-t-il pour les invariants de la classe dérivée et de la classe de base ?

- Quelles sont les préconditions que doit vérifier l'appelant ?

- Quelles sont les postconditions que doit garantir la classe dérivée ?

- Qu'en est-il des exceptions pouvant être levées ?

Nous allons voir comment répondre à ces questions. (14)

XX-C - Impact sur les invariants pour une fonction virtuelle

Le problème des invariants ne concerne pas l'appel d'une fonction virtuelle à l'extérieur de la classe de base ou de la classe dérivée, mais au sein d'une fonction membre. Une fonction peut pendant son déroulement momentanément rompre un ou des invariants à condition qu'elle les rétablisse avant de sortir vers l'appelant. Il faut donc considérer les invariants dans une fonction F1 appelée pour un objet O quand elle sollicite une fonction virtuelle F2 sur ce même objet (sur this) :

Impact sur les invariants :

struct base{
void F1()
{
/* [...] */
// Quel doit être l'état des invariants avant l'appel ?
F2();
// Comment F2 doit laisser les invariants après l'appel ?
/* [...] */
}
virtual void F2();
};

La réponse à la seconde question est la plus facile : qu'une fonction soit virtuelle ou non, qu'elle soit appelée depuis l'extérieur ou non, elle doit de toute façon rétablir les invariants qu'elle a elle-même rompus.
En revanche, savoir s'il faut rétablir les invariants avant l'appel d'une fonction virtuelle est une question qui suscite des positions variées. Cette question a fait l'objet d'une discussion : Ruptures des invariants en C++. La question s'articule alors entre savoir si le contrat doit être abordé de façon stricte au niveau de la classe. Ou si au contraire, il est préférable d'avoir une approche à un niveau juste au-dessus - le module (bien cela n'est pas de sens en C++) - qui est le seul pertinent d'un point de vue de conception (définition du service, réutilisabilité, etc.).

XX-D - Impact sur les préconditions pour une fonction virtuelle

Supposons que la classe de base B pose (P1 && P2 && P3 … && Pn) (15) comme préconditions pour l'appel de la fonction virtuelle F. Le client a la charge de vérifier les préconditions avant d'appeler F. Le code peut ressembler alors à :

Exemple de vérification des préconditions avant l'appel d'une fonction :

void client::some_function()
{
/* on suppose que l'on a une référence de type statique B &rb valide
sur un objet de type dynamique B */
if(P1(rb)&&P2(rb)&&...&&Pn(rb)){
rb.F();
}
/*else : gestion de ce cas */
}

Maintenant, supposons qu'une classe dérivée D demande une précondition supplémentaire Pk. Alors pour rester dans la programmation par contrat, le code devrait être changé en :

Vérifier les conditions supplémentaires ?

void client::some_function()
{
/* on suppose que l'on a une référence B &rb valide*/
if(is_dynamic_type_a_B(rb)){
if(P1(rb)&&P2(rb)&&...&&Pn(rb)){
rb.F();
}
}
else
if(is_dynamic_type_a_D(rb)){
if(P1(rb)&&P2(rb)&&...&&Pn(rb)&&Pk(rb)){
rb.F();
}
}
}

On viole un des principes fondamentaux de la programmation objet (le principe ouvert/fermé) : à chaque fois qu'une nouvelle classe dérivée est ajoutée, il faut venir changer cet appel. Qui plus est, avoir à se soucier du type dynamique avant d'appeler une fonction virtuelle enlève tout l'intérêt de ce mécanisme. Ce n'est donc pas envisageable. D'où la conclusion :

Une classe dérivée ne peut pas imposer des préconditions plus restrictives pour une fonction virtuelle que celles demandées par la classe de base.

Les préconditions ne peuvent être renforcées dans les classes dérivées.
Peut-on supprimer une précondition dans la classe dérivée ? Supposons que la classe dérivée supprime la précondition Pi. Cela veut dire qu'un appel à la fonction dérivée doit vérifier (P0 && P1 && P2 && … && (Pi || !Pi) && … && Pn). Cette expression est vraie dès que Pi est vrai. Dit autrement, (P0 && P1 && P2 && … && Pi && … && Pn) implique (P0 && P1 && P2 && … && (Pi || !Pi) && … && Pn). Donc notre code ne tenant pas compte du type dynamique reste valide pour une classe dérivée :

Les préconditions peuvent être relâchées :

void client::some_function()
{
/* on suppose que l'on a une référence de type statique B &rb valide */
if(P1(rb)&&P2(rb)&&...&&Pn(rb)){
rb.F();
}
}

Que Pi soit vérifiée est une contrainte supplémentaire qui n'est pas demandée par la classe dérivée, mais qui, si elle l'est, permet tout autant à la classe dérivée de remplir son contrat :

Une classe dérivée peut imposer des préconditions plus larges pour une fonction virtuelle que celles demandées par la classe de base.

Reformuler autrement, une classe dérivée doit savoir faire la même chose que sa classe de base (les préconditions ne peuvent pas être plus fortes), mais peut en savoir faire plus (les préconditions peuvent être plus faibles).

XX-E - Impact sur les postconditions pour une fonction virtuelle

Examinons les postconditions en suivant un raisonnement similaire au précédent. Soit une classe B proposant une fonction virtuelle F garantissant les postconditions (P1 && P2 && P3 … && Pn) :

Vérifier les postconditions :

void client::some_function()
{
/* on suppose que l'on a une référence de type statique B &rb valide
sur un objet de type dynamique B */
if(check_preconditions_for_F(rb)){
rb.F();
assert( P1(rb) && P2(rb) && .. && Pn(rb));
// F doit avoir correctement fait son boulot !
}
}

Si la classe dérivée est plus souple que la classe de base et décide de ne plus assurer la postcondition Pi. Alors la garantie précédente peut ne plus être vérifiée chez le client en sortie de F, le seul moyen qu'aurait le client serait à nouveau de différencier le traitement selon le type dynamique. Nous avons vu que cela était une impasse. Une classe dérivée ne peut donc supprimer une postcondition.

Une classe dérivée doit garantir toutes les postconditions d'une fonction virtuelle indiquées dans la classe de base.

À l'inverse, supposons que la classe dérivée garantisse une nouvelle postcondition Pk. Cela n'empêche pas les autres postconditions d'être valides. Le client peut donc continuer de s'appuyer sur les autres postconditions sans se soucier de ce 'bonus'.

Une classe dérivée peut ajouter de nouvelles postconditions au contrat d'une fonction virtuelle de la classe de base.

En d'autres termes, une classe dérivée doit faire au minimum la même chose que la classe de base (pas de postconditions en moins), mais peut en faire plus (on peut rajouter des postconditions).

On peut résumer les contraintes sur les préconditions et les postconditions par cette simple image : un sous-traitant (classe dérivée) DOIT faire autant (postconditions) et au même prix (préconditions) qu'un fournisseur (classe de base), mais il peut faire plus (renforcement des postconditions) pour moins cher (allégement des préconditions). En aucun cas il ne peut faire moins (allégement des postconditions) pour plus cher (renforcement de préconditions) :

Un sous-traitant doit faire autant au même prix

XXI - Le pattern N.V.I.

Comme beaucoup de bonnes idées, le pattern N.V.I. a probablement été introduit par plusieurs auteurs simultanément. L'article certainement le plus cité pour décrire ce pattern et écrit par Herb Sutter le présente sous le nom de Template Method dans le C/C++ User Journal. Mais ce nom a deux inconvénients majeurs : introduire le mot template alors qu'il ne s'agit pas de programmation générique, mais surtout de créer la confusion avec le patron de conception de même nom (D.P. Template Method) dont les objectifs sont différents. On parle donc de pattern N.V.I. pour Non Virtual Interface.
Le principe est simple : déclarer les fonctions publiques comme non virtuelles et les fonctions virtuelles comme privées. Les premières s'appuyant sur les secondes :

Pattern NVI :

struct non_virtual_interface
{
public:
void function()
{
do_function();
}
private:
virtual void do_function()
{
}
};

struct derived_non_virtual_interface : public non_virtual_interface
{
private :
virtual void do_function()
{
}
};

Au regard de ce que nous avons déjà vu, les avantages sont évidents :

- Différenciation du contrat : les utilisateurs de la classe doivent respecter le contrat défini par la fonction publique. Les classes dérivées doivent respecter le contrat défini par la fonction virtuelle. Il est dès lors plus facile de faire évoluer l'un indépendamment de l'autre. Les rôles sont bien distribués.

- Cela permet de regrouper les vérifications du contrat dans la fonction non virtuelle pour garantir l'appel des spécialisations dans les fonctions dérivées.

- Cela permet de regrouper des fonctions d'instrumentation (trace par exemple) dans la fonction non virtuelle sans avoir à les dupliquer.

- Comme évoqué dans la section concernant les fonctions virtuelles pures, le pattern N.V.I. permet d'entourer l'appel de la fonction virtuelle de prétraitements ou de posttraitements.

Pourquoi rendre la fonction virtuelle privée ?
Tout d'abord, nous avons vu que la visibilité de la fonction n'influe pas sur l'appel dynamique et n'empêche donc pas la fonction non virtuelle de l'invoquer. L'intérêt est que même les classes dérivées ne seront pas tentées d'appeler directement la fonction virtuelle et doivent passer par l'interface publique de la classe de base.

Quelles différences entre le pattern N.V.I. et le patron de conception Template Method ?
Le DP Template Method a pour objectif d'introduire des points de variation dans un algorithme. La fonction de base enchaîne différentes opérations dont certaines peuvent changer ou dépendre d'un contexte particulier : elles doivent être spécialisées par les classes dérivées.
La pattern N.V.I. a un tout autre objectif : séparer l'interface d'une classe en fonction de ses clients. L'interface publique et non virtuelle s'adresse aux utilisateurs de la classe. L'interface privée et virtuelle s'adresse aux classes dérivées.

XXII - Informations sur les types (dynamiques et statiques) et conversions

XXII-A - Types polymorphes

Le C++ définit un type polymorphe comme un type contenant au moins une fonction virtuelle ou dont une des classes de base contient une fonction virtuelle.

Types polymorphes :

struct base
{
};

struct derived_1 : private base
{
virtual void do_it();
};

struct derived_2 : private base
{
void do_it();
};

struct base_2
{
virtual ~base_2();
};
struct derived_3 : public base_2
{
};

Dans l'exemple précédent, cela donne :

- base : non polymorphe

- derived_1 : polymorphe

- derived_2 : non polymorphe

- base_2 : polymorphe

- derived_3 : polymorphe

L'héritage ne suffit pas pour qu'un type soit polymorphe. Une hiérarchie de classe ne contenant aucune fonction virtuelle n'est pas considérée comme polymorphe. L'explication est facile à comprendre au regard de ce que nous avons vu jusqu'à présent : en l'absence de fonction virtuelle, cela signifie que, quelle que soit l'expression utilisée, la résolution des appels se fait toujours avec le type statique de l'objet (variable, référence ou pointeur). Il n'est donc pas possible d'invoquer le comportement d'une classe dérivée à partir de la classe mère. En ce sens, il n'y a pas vraiment de polymorphisme, car l'objet est toujours vu comme le type statique qui le manipule.

XXII-B - Comment connaître le type dynamique d'une variable ?

XXII-B-1 - L'opérateur typeid

Pour être utilisé, l'opérateur typeid nécessite le fichier d'en-tête <typeinfo>. L'opérateur typeid retourne un objet de type statique const std::type_info et dont le type dynamique est défini par l'implémentation du compilateur (mais qui a de toute façon const std::type_info comme une de ses classes de base). L'opérateur s'applique aux variables, aux types ainsi qu'aux expressions. Elle permet d'avoir des informations de typage. Les qualificateurs const sont ignorés. Les références sont identiques au type qu'elles désignent et les pointeurs déréférencés aussi. Les valeurs retournées par typeid peuvent être comparées (avec l'opérateur == ou l'opérateur !=), cette comparaison permet de savoir si deux variables/types/expressions sont du même type :

Comparer des résultats de typeid pour comparer des types :

#include<typeinfo>
#include <iostream>

struct my_type
{
};

int main()
{
my_type t1;
my_type t2;
my_type &rt = t1;
my_type *pt = &t1;
my_type const ct=my_type();

std::cout<<std::boolalpha;
std::cout<<"typeid(t1)==typeid(t2) : "<<(typeid(t1)==typeid(t2))<<"\n";
std::cout<<"typeid(t1)==typeid(my_type) : "<<(typeid(t1)==typeid(my_type))<<"\n";
std::cout<<"typeid(t1)==typeid(rt) : "<<(typeid(t1)==typeid(rt))<<"\n";
std::cout<<"typeid(t1)==typeid(my_type&) : "<<(typeid(t1)==typeid(my_type&))<<"\n";
std::cout<<"typeid(t1)==typeid(pt) : "<<(typeid(t1)==typeid(pt))<<"\n";
std::cout<<"typeid(t1)==typeid(ct) : "<<(typeid(t1)==typeid(ct))<<"\n";
std::cout<<"typeid(t1)==typeid(my_type const) : "<<(typeid(t1)==typeid(my_type const))<<"\n";
std::cout<<"typeid(t1)==typeid(*pt) : "<<(typeid(t1)==typeid(*pt))<<"\n";
std::cout<<"typeid(t1)==typeid(my_type*) : "<<(typeid(t1)==typeid(my_type*))<<"\n";

std::cout<<"typeid(t1)==typeid(int) : "<<(typeid(t1)==typeid(int))<<"\n";

return 0;
}

Appliquer à un type polymorphe, typeid retourne un const std::type_info relatif au type dynamique de l'objet :

typeid retrouve le type dynamique d'objets polymorphes :

#include<typeinfo>
#include <iostream>

struct polymorphic_base
{
virtual ~polymorphic_base(){}
};
struct polymorphic_derived : public polymorphic_base
{
};

int main()
{
polymorphic_base b;
polymorphic_derived d;

std::cout<<std::boolalpha;
std::cout<<"typeid(b)==typeid(d) : "<<(typeid(b)==typeid(d))<<"\n";

polymorphic_base &rb=b;
std::cout<<"typeid(b)==typeid(rb) : "<<(typeid(b)==typeid(rb))<<"\n";

polymorphic_base &rd=d;
std::cout<<"typeid(b)==typeid(rd) : "<<(typeid(b)==typeid(rd))<<"\n";
std::cout<<"typeid(d)==typeid(rd) : "<<(typeid(d)==typeid(rd))<<"\n";

return 0;
}

Ce code produit comme sortie :

typeid(b)==typeid(d) : false
typeid(b)==typeid(rb) : true
typeid(b)==typeid(rd) : false
typeid(d)==typeid(rd) : true

Le type dynamique sur la référence rd est bien retrouvé et correspond au type polymorphic_derived de d.

Appliquer à un type non polymorphe, l'information de type retournée est relative au type statique :

typeid retrouve le type statique des objets non polymorphes :

#include<typeinfo>
#include <iostream>

struct non_polymorphic_base
{
~non_polymorphic_base(){}
};
struct non_polymorphic_derived : public non_polymorphic_base
{
};

int main()
{
non_polymorphic_base b;
non_polymorphic_derived d;

std::cout<<std::boolalpha;
std::cout<<"typeid(b)==typeid(d) : "<<(typeid(b)==typeid(d))<<"\n";

non_polymorphic_base &rb=b;
std::cout<<"typeid(b)==typeid(rb) : "<<(typeid(b)==typeid(rb))<<"\n";

non_polymorphic_base &rd=d;
std::cout<<"typeid(b)==typeid(rd) : "<<(typeid(b)==typeid(rd))<<"\n";
std::cout<<"typeid(d)==typeid(rd) : "<<(typeid(d)==typeid(rd))<<"\n";

return 0;
}

Ce code donne comme résultat :

typeid(b)==typeid(d) : false
typeid(b)==typeid(rb) : true
typeid(b)==typeid(rd) : true
typeid(d)==typeid(rd) : false

Les deux dernières lignes mettent en évidence que typeid(rd) prend le type statique de la référence non_polymorphic_base et non son type dynamique, car non_polymorphic_base n'est pas une classe polymorphe.

XXII-B-2 - Évaluation de l'expression

Appliqué à un type non polymorphe et utilisant par conséquent le type statique de l'expression, l'opérateur typeid n'évalue pas l'expression à laquelle il est appliqué. En particulier, si un pointeur est nul, et qu'il est déréférencé dans l'expression, cela ne provoque pas d'erreur :

L'expression pour un type non polymorphe n'est pas évaluée :

#include<typeinfo>
#include <iostream>

struct non_polymorphic_base
{
~non_polymorphic_base(){}
};
struct non_polymorphic_derived : public non_polymorphic_base
{
};

int main()
{
non_polymorphic_base *pb=0;
std::cout<<std::boolalpha;
std::cout<<"typeid(*pb)==typeid(non_polymorphic_base) : "
<<(typeid(*pb)==typeid(non_polymorphic_base))<<"\n";
return 0;
}

Ce code est correct et ne pose pas de problème à l'exécution.

En revanche, sur un type polymorphe, l'expression est évaluée, car c'est le type dynamique obtenu à l'exécution qui est nécessaire à l'opérateur typeid. En particulier, si un pointeur est nul et que celui-ci est déréférencé dans l'expression, une exception de type std::bad_typeid est générée :

L'expression pour un type non polymorphe est évaluée :

#include<typeinfo>
#include <iostream>

struct polymorphic_base
{
virtual ~polymorphic_base(){}
};
struct polymorphic_derived : public polymorphic_base
{
};

int main()
{
polymorphic_base *pb=0;
std::cout<<std::boolalpha;
try{
std::cout<<"typeid(*pb)==typeid(polymorphic_base) : "
<<(typeid(*pb)==typeid(polymorphic_base))<<"\n";
}
catch(std::bad_typeid const &e_)
{
std::cout<<"Erreur : "<<e_.what()<<"\n";
}
return 0;
}

Lorsqu'une expression est évaluée, cela signifie aussi que les fonctions qu'elle contient sont effectivement déroulées. Alors que lorsque l'expression n'est pas évaluée, les fonctions ne sont pas exécutées :

Évaluation implique appel de fonctions :

#include<typeinfo>
#include <iostream>

struct polymorphic_base
{
virtual ~polymorphic_base(){}
};
struct polymorphic_derived : public polymorphic_base
{
};

polymorphic_base&identity_1(polymorphic_base&r_)
{
std::cout<<"Fonction identity_1";
return r_;
}

struct non_polymorphic_base
{
~non_polymorphic_base(){}
};
struct non_polymorphic_derived : public non_polymorphic_base
{
};

non_polymorphic_base&identity_2(non_polymorphic_base&r_)
{
std::cout<<"Fonction identity_2";
return r_;
}

int main()
{
polymorphic_base b;
std::cout<<"typeid(identity_1(b)) : ";
typeid(identity_1(b));
std::cout<<" --\n";

non_polymorphic_base b2;
std::cout<<"typeid(identity_2(b2)) : ";
typeid(identity_2(b2));
std::cout<<" --\n";

return 0;
}

Ce code donne le résultat :

typeid(identity_1(b)) : Fonction identity_1 --
typeid(identity_2(b2)) :  --

Notons que GCC donne un avertissement lorsqu'un l'expression contient un appel non exécuté :

Avertissement GCC :

warning : statement has no effect

XXII-B-3 - La classe type_info

Le synopsis de cette classe est assez simple :

Synopsis de la classe type_info

namespace std {
class type_info {
public:
virtual ~type_info();
bool operator==(const type_info& rhs) const;
bool operator!=(const type_info& rhs) const;
bool before(const type_info& rhs) const;
const char* name() const;
private:
type_info(const type_info& rhs);
type_info& operator=(const type_info& rhs);
};
}

type_info est dans l'espace de nom std. Le constructeur par copie et l'opérateur d'affectation étant privés, les objets retournés par typeid ne sont pas copiables. Il n'est donc pas possible de garder par valeur le retour de typeid dans un autre objet ou dans un conteneur. En revanche, on peut garder une référence ou un pointeur sur l'objet type_info retourné, la norme garantit que la durée de vie de cet objet doit dépasser celle du programme (pas du processus !).
type_info::name() retourne une chaîne de caractère terminée par un '\0' contenant le nom du type. Ce nom est spécifique au compilateur :

Récupérer le nom d'un type :

#include<typeinfo>
#include <iostream>

class A{};

int main()
{
std::cout<<typeid(int).name()<<"\n";
std::cout<<typeid(A).name()<<"\n";
return 0;
}

Produit comme sortie avec GCC :

Noms des types avec GCC :

i
1A

Et avec Visual C++ :

Noms des types avec Visual C++ :

int
class A

type_info::before() permet de définir un ordre sur les objets retournés. Cet ordre est spécifique au compilateur.

XXII-B-4 - Pourquoi récupérer le type dynamique ?

Il est fort à parier que si vous ressentez le besoin de récupérer le type dynamique d'une variable ou d'une expression, c'est que vous avez probablement une erreur de conception de votre logiciel. En revanche, typeid peut-être utile en mode debug et pour le log afin d'avoir des traces sur les objets effectivement manipulés. En dehors de ces cas de debug et de tests-U, rares sont les bonnes raisons d'avoir à le faire.

XXII-C - Comment connaître le type statique d'une variable ?

Cette question peut sembler incongrue puisque nous avons toujours dit et vu que le type statique d'une variable est celui que nous avons sous le nez en lisant le code. Écartons le typedef qui ne définit qu'un synonyme d'un type et dont on peut retrouver facilement le type effectivement désigné. Avec les fonctions et les classes génériques (template) le type est un paramètre et ne peut donc être connu immédiatement lors de son utilisation :

Avoir des informations sur le type statique :

template<class T> void function(T const&t_)
{
// quelles informations sur le type T ?
}

Si l'utilisation de template suppose que justement le type n'a pas d'importance, en pratique, on souhaite parfois avoir des informations sur le type effectivement utilisé pour instancier la fonction ou la classe générique. Pour cela, on s'appuie sur des classes traits. Les classes traits utilisent le mécanisme des classes génériques et des spécialisations pour permettre d'obtenir des informations sur les types :

Les classes traits :

#include <iostream>

template<class T> struct is_int
{
static const bool value = false;
};
template<> struct is_int<int>
{
static const bool value = true;
};

template<class T> void is_an_int(T const &)
{
std::cout<<is_int<T>::value<<"\n";
}

class A{};
int main()
{
int i;
À a;
double d;

std::cout<<std::boolalpha;
std::cout<<"is_an_int(i) ? ";
is_an_int(i);
std::cout<<"is_an_int(a) ? ";
is_an_int(a);
std::cout<<"is_an_int(d) ? ";
is_an_int(d);

return 0;
}

Les classes traits sont beaucoup utilisées dans la programmation générique. Vous pouvez trouver une introduction aux classes traits et aux classes de politiques dans : Présentation des classes de Traits et de Politiques en C++, par Alp Mestan.
Actuellement, le C++ possède quelques classes traits dans la STL. Par exemple, la bibliothèque string contient des classes traits sur les types de caractères pour définir des propriétés sur ceux-ci : template<class charT> char_traits<charT> est spécialisée avec char et wchar_t. Cela permet de n'avoir qu'une seule classe std::basic_string et de s'appuyer sur ces classes traits pour définir les chaînes ANSI (std::string) ou UNICODE (std::wstring) :

Exemple de classe trait de la STL :

namespace std {
template<typename _CharT, typename _Traits, typename _Alloc>
class basic_string;

typedef basic_string<char,char_traits<char> >    string;
typedef basic_string<wchar_t,char_traits<<wchar_t> > wstring;
}

La bibliothèque Boost.Type Traits propose beaucoup de classes traits permettant d'avoir un nombre appréciable d'informations.

C++0x : la nouvelle norme prévoir d'intégrer une collection plus riche de classes traits. Beaucoup des ces classes qui nécessitent aujourd'hui une bibliothèque comme Boost.Type Traits seront avec les compilateurs conformes à cette nouvelle norme directement disponibles avec le fichier d'en-tête #include <type_traits>. Voici quelques exemples tirés du draft N3000 de septembre 2009 (donc encore susceptible d'évoluer) :

Exemples de classes traits en C++0x :

namespace std {
template <class T> struct is_void;
template <class T> struct is_integral;
template <class T> struct is_floating_point;
template <class T> struct is_array;
template <class T> struct is_pointer;
template <class T> struct is_class;
template <class T> struct is_const;
template <class T> struct is_polymorphic;
template <class T> struct is_abstract;
// etc.
}

XXII-D - Conversions entre type de base et type dérivé

XXII-D-1 - Conversion du type dérivé vers le type de base

La conversion depuis le type dérivé vers le type de base (upcast) est automatique en C++ dès lors que l'héritage est public. Nous l'avons employé dans chacun des exemples précédents sans que cela pose le moindre problème :

L'upcast est automatique :

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

int main()
{
derived d;
base b = d;
base b2;
b2 = d;
base &rb = d;
base *pb = &d;
return 0;
}

base b = d; suppose que base est constructible par copie. En effet cette ligne appel le constructeur de copie de base sur la nouvelle variable b avec en paramètre d.
b2 = d; suppose que base est assignable. En effet cette ligne appelle l'opérateur d'affectation base sur la variable b2 avec en paramètre d.
Les deux autres lignes base &rb = d; et base *pb = &d; ne supposent rien sur le type base et définissent une référence (respectivement un pointeur) dont le type statique est base et le type dynamique derived.
La conversion depuis un type dérivé vers un type de base ne pose strictement aucun problème. S'il s'agit de pointeurs ou de références alors la nouvelle variable pointe/référence un objet du type dérivé au travers d'un pointeur/référence de type statique de base. Si la conversion va vers une valeur, bien sûr l'information du type dérivé est perdue puisqu'un nouvel objet du type de base est construit avec uniquement les informations du type de base contenu dans l'objet dérivé.

XXII-D-2 - Conversion du type de base vers le type dérivé

Si vous venez du monde du C ou que vous avez eu un cours mal appelé C/C++, vous aurez peut être le réflexe de faire ça :

Les cast à la C : une erreur !

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

int main()
{
derived d;
base &rb = d;
derived&rd = (derived&)d;
return 0;
}

Oubliez tout de suite ce genre de conversion, car un jour vous aurez ça :

Les casts à la C brisent la vérification de type :

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

int main()
{
derived d;
int i;
derived&rd = (derived&)i;
return 0;
}

Code qui compile parfaitement, mais qui emmènera votre application directement dans le mur.
Le C++ propose un premier opérateur de conversion entre types compatibles : static_cast. Cet opérateur vous préserve de l'erreur précédente :

Eviter les erreurs avec les opérateurs de transtypage du langage :

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

int main()
{
derived d;
int i;
derived&rd = static_cast<derived&>(i);
// 'static_cast' : impossible de convertir de 'int' en 'derived &'
return 0;
}

Utilisé avec une valeur, l'opérateur static_cast<T>(e) est valide si la déclaration T t_(e) est valide. Autrement dit, la conversion réussie s'il existe un constructeur prenant un argument du type de e ou d'un type pouvant être automatiquement converti depuis e. Donc, en général, vouloir affecter à une classe dérivée un objet d'une classe de base provoque une erreur ce qui est bien normal :

Le downcast static doit être possible :

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

int main()
{
base b;
derived d = static_cast<derived>(b);
// 'static_cast' : impossible de convertir de 'base' en 'derived'
return 0;
}

La conversion d'un type de base vers un type dérivé (downcast) peut être intéressante si le type dynamique du type de base correspond au type dérivé cible. Cela peut donc se faire avec les références et les pointeurs :

downcast statique pour les références et les pointeurs :

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

int main()
{
derived d;
base &rb = d;
derived &rd = static_cast<derived&>(rb); // rd désigne maintenant d

return 0;
}

L'opérateur static_cast ne peut pas s'appliquer si derived hérite virtuellement de base. Le compilateur signale une erreur.

À noter que si le type dynamique de rb n'est pas derived (ou une classe dérivant de derived), la conversion est indéterminée, en d'autres termes c'est une erreur d'exécution. Cette erreur est silencieuse et le plantage peut avoir lieu à n'importe quel moment, en particulier très loin après le cast malheureux :

Le downcast statique peut poser problème :

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

struct derived2 : public base
{
};

int main()
{
derived d;
base b;
base &rb = b;
derived &rd = static_cast<derived&>(rb);
// OK à la compilation, mais
// erreur à l'exécution, car rb désigne une base (b) et non un derived

derived2 d2;
base &rb2 = d2;
derived &rd2 = static_cast<derived&>(rb2);
// OK à la compilation, mais
// erreur à l'exécution, car rb2 désigne un derived_2 (d2) et non un derived

return 0;
}

Pour pallier ces défauts, un autre opérateur existe en C++ : dynamic_cast. Il fonctionne de façon semblable à static_cast si ce n'est qu'il vérifie à l'exécution que le type dynamique de l'expression peut être converti vers le type demandé. Comme le type dynamique est sollicité, dynamic_cast ne s'applique qu'aux pointeurs et aux références. Si la conversion échoue avec une référence, alors l'opérateur soulève une exception std::bad_cast définie dans le fichier typeinfo. Cette exception peut ainsi être récupérée et traitée :

Le downcast dynamique permet de gérer les problèmes du downcast statique :

#include <typeinfo>
#include <iostream>
struct base
{
virtual ~base(){}
};
struct derived : public base
{
};

struct derived2 : public base
{
};

int main()
{
derived d;
base &rb_1 = d;
try{
std::cout<<"dynamic_cast<derived&>(base &rb_1 = derived d) : ";
derived &rd = dynamic_cast<derived&>(rb_1);
std::cout<<"Conversion valide\n";
}
catch(std::bad_cast const &e_){
std::cout<<"Erreur de dynamic_cast : "<<e_.what()<<std::endl;
}

base b;
base &rb_2 = b;
try{
std::cout<<"dynamic_cast<derived&>(base &rb_2 = base b) : ";
derived &rd = dynamic_cast<derived&>(rb_2);
std::cout<<"Conversion valide\n";
}
catch(std::bad_cast const &e_){
std::cout<<"Erreur de dynamic_cast : "<<e_.what()<<std::endl;
}

derived2 d2;
base &rb_3 = d2;
try{
std::cout<<"dynamic_cast<derived&>(base &rb_3 = derived2 d2) : ";
derived &rd = dynamic_cast<derived&>(rb_3);
std::cout<<"Conversion valide\n";
}
catch(std::bad_cast const &e_){
std::cout<<"Erreur de dynamic_cast : "<<e_.what()<<std::endl;
}

return 0;
}

Ce code produit la sortie suivante :

dynamic_cast<derived&>(base &rb_1 = derived d) : Conversion valide
dynamic_cast<derived&>(base &rb_2 = base b) : Erreur de dynamic_cast : std::bad_cast
dynamic_cast<derived&>(base &rb_3 = derived2 d2) : Erreur de dynamic_cast : std::bad_cast

Avec les pointeurs, l'erreur n'est pas remontée avec une exception, mais en retournant un pointeur nul :

Le downcast dynamique d'un pointeur retourne nul plutôt qu'une exception en cas d'erreur :

#include <iostream>
struct base
{
virtual ~base(){}
};
struct derived : public base
{
};

struct derived2 : public base
{
};

int main()
{
derived d;
base *pb_1 = &d;
std::cout<<"dynamic_cast<derived*>(base *pb_1 = derived d) : ";
derived *pd_1 = dynamic_cast<derived*>(pb_1);
if(pd_1){
std::cout<<"Conversion valide\n";
}
else{
std::cout<<"Echec de la conversion\n";
}

base b;
base *pb_2 = &b;
std::cout<<"dynamic_cast<derived*>(base *pb_2 = base b) : ";
derived *pd_2 = dynamic_cast<derived*>(pb_2);
if(pd_2){
std::cout<<"Conversion valide\n";
}
else{
std::cout<<"Echec de la conversion\n";
}

derived2 d2;
base *pb_3 = &d2;
std::cout<<"dynamic_cast<derived*>(base *pb_3 = derived2 d2) : ";
derived *pd_3 = dynamic_cast<derived*>(pb_3);
if(pd_3){
std::cout<<"Conversion valide\n";
}
else{
std::cout<<"Echec de la conversion\n";
}

return 0;
}

Le résultat obtenu :

dynamic_cast<derived*>(base *pb_1 = derived d) : Conversion valide
dynamic_cast<derived*>(base *pb_2 = base b) : Echec de la conversion
dynamic_cast<derived*>(base *pb_3 = derived2 d2) : Echec de la conversion

Appliqué à un pointeur nul, dynamic_cast retourne un pointeur nul :

dynamic_cast fonctionne sur un pointeur nul :

#include <iostream>
struct base
{
virtual ~base(){}
};
struct derived : public base
{
};

int main()
{
base *pb_1 = 0;
std::cout<<"dynamic_cast<derived*>(base *pb_1 = 0) : ";
derived *pd_1 = dynamic_cast<derived*>(pb_1);
if(pd_1){
std::cout<<"Conversion valide\n";
}
else{
std::cout<<"Echec de la conversion\n";
}

return 0;
}

La sortie est :

dynamic_cast<derived*>(base *pb_1 = 0) : Echec de la conversion

dynamic_cast ne peut s'appliquer que sur une expression dont le type est polymorphique :

dynamic_cast ne s'utilise qu'avec des types polymorphes :

struct polymorphic_base
{
virtual ~polymorphic_base(){}
};
struct polymorphic_derived : public polymorphic_base
{
};

struct non_polymorphic_base
{
~non_polymorphic_base(){}
};
struct non_polymorphic_derived : public non_polymorphic_base
{
};

int main()
{
polymorphic_derived pd;
polymorphic_base &rpd = pd;
polymorphic_derived &rpdd = dynamic_cast<polymorphic_derived &>(rpd);// OK

non_polymorphic_derived npd;
non_polymorphic_base &rnpd = npd;
non_polymorphic_derived &rnpdd = dynamic_cast<non_polymorphic_derived &>(rnpd);
// Erreur de compilation : 'non_polymorphic_base' n'est pas un type polymorphe

return 0;
}

Enfin, dynamic_cast échoue si l'héritage n'est pas public (héritage privée ou protégé) ou s'il est ambigu (héritage multiple).

XXII-D-3 - Pourquoi faire une conversion d'un type de base vers un type dérivé

95% des cas où vous avez besoin de convertir un pointeur ou une référence depuis une classe de base vers un type dérivé traduisent une erreur de conception. Je ne parlerai pas des 5% restant pour ne pas semer le trouble. Si le downcast vous titille, visiter le forum C++ pour confirmer votre besoin ou refroidir vos ardeurs.

XXIII - Comment ça marche ?

XXIII-A - Qui définit la mise en œuvre ?

La norme C++ et donc le langage ne précisent absolument pas comment doit être mise en œuvre la résolution dynamique des appels de fonctions virtuelles. Le langage se contente de définir le comportement attendu. À charge des compilateurs de trouver des mécanismes appropriés pour atteindre cet objectif. Ce qui suit précise les solutions habituellement utilisées par les compilateurs. Il existe peut-être des compilateurs ayant opté pour d'autres solutions (je n'en connais pas) ou à l'avenir d'autres techniques émergeront. En ce sens, tout ce qui suit concerne plus des détails d'implémentation que des comportements imposés par la norme.

XXIII-B - Résoudre l'appel dynamiquement : les tables virtuelles

La compilation traduit le code source en code exécutable, c'est-à-dire en une suite d'instructions-machine directement exécutées par le processeur. Une fonction est donc compilée en une suite continue d'instructions à une adresse donnée, fixe et connue à la compilation. Cette adresse est l'adresse de la fonction. Lorsqu'un appel vers une fonction est résolue à la compilation, le compilateur génère une instruction spéciale (un saut) qui demande au processeur de continuer son exécution à l'adresse indiquée. Cette adresse est connue à la compilation, et c'est l'éditeur de lien qui a la charge de mettre en dur l'adresse du saut :

pseudo-code assembleur d'appel d'une fonction non virtuelle :

aller à 0x12345678  (0x12345678 est l'adresse de la fonction)

Les nouvelles instructions, celle de la fonction, sont alors lues à partir de cette adresse et exécutées par la machine.
Pour une résolution dynamique d'une fonction virtuelle, le compilateur ne sait pas vers quelle adresse le saut doit être fait, car cela dépend du type dynamique connu uniquement à l'exécution. Pour résoudre ce problème, les compilateurs - sans que ce soit une obligation, mais je ne connais pas d'autres méthodes - utilisent une indirection : la table des fonctions virtuelles. Les mots vtable, v-table, dispatch table sont aussi souvent employés pour désigner la table des fonctions virtuelles. Cette table contient autant d'entrées que de fonctions virtuelles, chaque entrée contenant l'adresse d'une fonction. À chaque classe est associée une telle table avec les adresses des fonctions virtuelles lui correspondant : celle de la classe de base si la fonction n'est pas spécialisée, sinon celle de sa spécialisation.
Pour le code suivant :

Tables des fonctions virtuelles :

struct base
{
virtual void function_1();
virtual void function_2();
virtual ~base();
};

struct derived : public base
{
virtual void function_1();
virtual ~derived();

};

Les tables virtuelles sont :

Entrées de la table des fonctions virtuelles pour une classe de base et sa classe dérivée.

Entrée de la table

base

derived

0

base::function_1

derived::function_1

1

base::function_2

base::function_2

2

base::~base

derived::~derived

Le compilateur est chargé de construire ces tables. Une fonction virtuelle donnée est toujours au même emplacement dans la table des fonctions virtuelles dans tout l'arbre d'héritage. Dans notre exemple, function_1 est à l'emplacement 0 pour la classe de base et pour la classe dérivée. Si une classe dérivée ajoute de nouvelles fonctions virtuelles alors celles-ci sont mises à la suite des fonctions précédentes :
Pour le code suivant :

Tables des fonctions virtuelles :

struct base
{
virtual void function_1();
virtual void function_2();
virtual ~base();
};

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

};

Les tables virtuelles sont :

Entrées de la table des fonctions virtuelles pour une classe de base et sa classe dérivée.

Entrée de la table

base

derived

0

base::function_1

derived::function_1

1

base::function_2

base::function_2

2

base::~base

derived::~derived

3

-

derived::~function_3

L'appel d'une fonction virtuelle consiste alors à aller chercher dans cette table l'adresse de la fonction vers laquelle faire le saut. En pseudo-code, les appels ressemblent à ça :

Fonction non virtuelle

Fonction virtuelle

1. Empiler l'adresse de retour ;

2. empiler les paramètres ;

3. aller à l'adresse de la fonction 0x12345678 (saut).

1. Empiler l'adresse de retour ;

2. empiler les paramètres ;

3. récupérer la table virtuelle ;

4. lire l'adresse de la fonction dans l'entrée i de la table virtuelle ;

5. aller à l'adresse lue (saut).

L'appel d'une fonction virtuelle a par conséquent un cout à l'exécution puisqu'il faut d'abord récupérer l'adresse de la fonction vers laquelle faire le saut. L'appel d'une fonction virtuelle est donc légèrement plus lent que l'appel d'une fonction non virtuelle. Ce cout n'est pas prohibitif en regard des avantages du mécanisme de résolution dynamique des fonctions virtuelles. Généralement le cout est relativement faible face à la durée d'exécution de la fonction. Enfin, avec ces tables virtuelles, ce cout est constant, quel que soit la profondeur de l'héritage ou le nombre de fonctions virtuelles, car l'index dans le tableau est connu à la compilation.

Les tables des fonctions virtuelles sont associées aux classes. Dès lors, comment retrouver la table virtuelle d'un objet ? Chaque objet instance d'une classe polymorphe contient un pointeur caché, vpointer, vers la table des fonctions virtuelles de son type dynamique. Pour s'en convaincre, le test suivant montre que la taille d'une classe change selon qu'elle est polymorphe ou non. Cette différence est due au pointeur vers la table des fonctions virtuelles :

Un pointeur caché vers une table virtuelle est ajoutée pour les classes polymorphes :

#include <iostream>

struct non_polymorphic_type
{
};

struct polymorphic_type
{
virtual ~polymorphic_type();
};

int main()
{
std::cout<<"sizeof(non_polymorphic_type) = "<<sizeof(non_polymorphic_type)<<"\n";
std::cout<<"sizeof(polymorphic_type) = "<<sizeof(polymorphic_type)<<"\n";

return 0;
}

Ce code produit comme sortie :

sizeof(non_polymorphic_type) = 1
sizeof(polymorphic_type) = 4

La classe sans membre virtuel n'a pas une taille de zéro, car le C++ impose que tous les types aient une taille non nulle.
Les 4 octets de la classe polymorphe correspondent avec mon Windows 32 bits à la taille d'un pointeur : 4 octets (et non pas 5, car de par la table virtuelle la classe n'a plus une taille de 0 nécessitant un octet fictif).

Lorsqu'il y a héritage multiple, pour maintenir la contrainte d'utilisation du même index dans la table des fonctions virtuelles, plusieurs tables sont associées à la classe dérivée :

En cas d'héritage multiple, plusieurs tables virtuelles peuvent être ajoutées :

#include <iostream>

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

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

struct derived : public base_1, public base_2
{
virtual ~derived(){}

};

int main()
{
std::cout<<"sizeof(base_1) = "<<sizeof(base_1)<<"\n";
std::cout<<"sizeof(base_2) = "<<sizeof(base_2)<<"\n";
std::cout<<"sizeof(derived) = "<<sizeof(derived)<<"\n";

return 0;
}

La sortie produite est :

sizeof(base_1) = 4
sizeof(base_2) = 4
sizeof(derived) = 8

- sizeof(base_1) = 4 : 4 octets pour l'adresse de la table des fonctions virtuelles de base_1.

- sizeof(base_2) = 4 : 4 octets pour l'adresse de la table des fonctions virtuelles de base_2.

- sizeof(derived) = 8 : 4 octets pour l'adresse de la table des fonctions virtuelles base_1, et 4 octets pour l'adresse de la table des fonctions virtuelles base_2.

XXIII-C - Quelle entrée pour les fonctions virtuelles pures ?

Nous avons vu que le type dynamique d'une variable ne peut jamais correspondre à une classe abstraite. Autrement dit, l'entrée de la vtable de la classe abstraite pour une fonction virtuelle pure n'est jamais lue pour trouver l'appel correspondant… Les compilateurs utilisent généralement 3 stratégies :

- laisser l'entrée initialisée : tenter de la déréférencer provoque une erreur ;

- positionner l'entrée à un pointeur nul : tenter de la déréférencer provoque une erreur ;

- positionner l'entrée vers une fonction spéciale proposée par le compilateur : en général, cette fonction affiche un message d'erreur et provoque la terminaison du programme.

XXIII-D - Comment sont construites les tables virtuelles ?

Les tables de fonctions virtuelles sont de simples tableaux statiques générés par le compilateur pour chaque classe contenant des fonctions virtuelles. Il existe donc un tableau par classe indépendamment des objets instanciés. Les tables de fonctions virtuelles peuvent être vues comme des membres statiques d'une classe : elles sont partagées par toutes les instances de la classe. Le compilateur possède toutes les informations nécessaires à l'initialisation de ces tables. Le compilateur ajoute donc ces tables correctement initialisées dans le code du programme. En revanche, pour un objet instance d'une classe polymorphe, le compilateur doit ajouter le code nécessaire pour initialiser correctement le pointeur caché (vpointer) vers la bonne table. Pour cela, le compilateur ajoute dans le constructeur d'une classe un morceau de code après l'appel des constructeurs des classes de base et avant d'entrer dans le constructeur en cours pour faire pointer le vpointer vers le tableau des fonctions virtuelles de la classe dont le constructeur va être déroulé. Ainsi, dans le constructeur, le vpointer pointe sur la table de la classe en cours. Si un appel vers une fonction virtuelle est demandé, la résolution prend l'entrée dans la table de la classe en cours et c'est bien l'appel de la fonction dont le constructeur est en train d'être exécuté qui est appelée. Cela est conforme au comportement décrit par la norme tel que nous l'avons présenté plus tôt.
Avec l'exemple suivant :

Construction des tables virtuelles :

#include <iostream>
struct base_1
{
base_1()
{
function_1();
function_2();
}
virtual void function_1()
{
std::cout<<"base_1::function_1\n";
}
virtual void function_2()
{
std::cout<<"base_1::function_2\n";
}
virtual void function_3()=0;

virtual ~base_1(){}
};

struct base_2 : public base_1
{
base_2()   {
function_1();
function_2();
}
virtual void function_1()
{
std::cout<<"base_2::function_1\n";
}

virtual ~base_2(){}
};
struct base_3 : public base_2
{
base_3()   {
function_1();
function_2();
function_3();
}
virtual void function_1()
{
std::cout<<"base_3::function_1\n";
}

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

virtual void function_3()
{
std::cout<<"base_3::function_3\n";
}

virtual ~base_3(){}
};

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

};

int main()
{
derived d;
return 0;
}

4 tables de fonctions virtuelles sont générées. Appelons-les :

Entrée de la table :

base_1 : vtable_1

base_2 : vtable_2

base_3 : vtable_3

base_derived : vtable_derived

[0]

base_1::function_1

base_2::function_1

base_3::function_1

derived::function_1

[1]

base_1::function_2

base_1::function_2

base_3::function_2

base_3::function_2

[2]

-

-

base_3::function_3

base_3::function_3

Regardons maintenant la valeur du vpointeur lors de la construction :

1. Juste avant d'entrée dans base_1::base_1 : vpointer = vtable_1

2. Juste avant d'entrée dans base_2::base_2 : vpointer = vtable_2

3. Juste avant d'entrée dans base_3::base_3 : vpointer = vtable_3

4. Juste avant d'entrée dans derived::derived : vpointer = vtable_derived

Avec cette liste et le tableau précédent, vous avez tous les éléments pour prévoir la séquence de trace provoquée par l'instanciation de la variable d de la classe derived dans le main.

Si in-fine l'appel d'une fonction virtuelle pure dans le constructeur est indéterminée, au regarde de ce que nous venons de voir, on comprend mieux les différents comportements :

- Appel direct d'une fonction virtuelle pure sans définition : nous avons vu que les compilateurs choisissent une résolution statique de l'appel. Donc, la fonction n'existant pas, une erreur de lien est obtenue.

- Appel indirect d'une fonction virtuelle pure sans définition : avec un appel indirect, le compilateur doit utiliser la vtable pour résoudre l'appel. Or, l'entrée pour la fonction virtuelle est renseignée avec l'adresse d'une fonction spéciale. À l'exécution, le message d'erreur est affiché et le programme est terminé.

- Appel direct d'une fonction virtuelle pure avec définition : comme pour le premier cas, l'appel direct est optimisé par le compilateur et un saut vers la définition de la fonction est ajouté. Cela se passe donc sans problème.

- Appel indirect d'une fonction virtuelle pure avec définition : comme dans le second cas, la table virtuelle est utilisée et la fonction spéciale d'affichage d'erreur et de terminaison est appelée.

XXIII-E - Comment sont détruites les tables virtuelles ?

La destruction d'une variable suit le processus inverse de la construction : à l'entrée du destructeur, le vpointer est redirigé vers la classe dont le destructeur va être exécuté. Le comportement est donc identique que pour le constructeur : les fonctions virtuelles (non pures) appelées sont celles de la classe dont le destructeur est en train de s'exécuter et les fonctions virtuelles pures dans les classes abstraites provoquent l'appel de la fonction spéciale de trace et de terminaison.

XXIII-F - Qu'est-ce qu'un pointeur de fonction virtuelle ?

Les adresses des fonctions virtuelles peuvent être récupérées tout comme celles des adresses des fonctions non virtuelles :

Récupérer l'adresse d'une fonction :

#include <iostream>

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

virtual void virtual_function()
{
std::cout<<"base::virtual_function\n";
}
};
int main()
{
base b;
void (base::*p_function)(void);

p_function = &base::non_virtual_function;
(b.*p_function)();
p_function =  &base::virtual_function;
(b.*p_function)();

return 0;
}

Pour une fonction non virtuelle, l'affaire est simple : l'adresse positionnée dans p_function est donnée en dur à la compilation avec la fonction générée. En revanche, rien de tel ne peut être fait avec une fonction virtuelle, car l'adresse est résolue dynamiquement à l'exécution.
Visual C++ crée ce qu'on appelle un thunk. Il s'agit d'une fonction qui prend en paramètre un objet de la classe et appelle la fonction virtuelle à l'aide de la vtable. Dans notre exemple, il crée une fonction non virtuelle vcall_0 et donne son adresse à p_function. vcall_0 ressemble à ça :

Chunk :

void vcall_0(base &rb_)
{
rb_.virtual_function();
}

À chaque fonction virtuelle est associée un thunk avec la même signature (respectant les paramètres, le type retour, la constance) pour permettre cet appel.
De ce que j'ai compris, GCC utilise une méthode un peu plus subtile. Elle se base sur le fait que les adresses de fonctions sont alignées sur 4 octets. Donc elles sont toujours paires. Pour une fonction virtuelle, p_function reçoit le décalage en octet de l'entrée dans la vtable incrémenté de 1. Par exemple, pour la fonction virtuelle de la première entrée, la variable reçoit 1. Pour la fonction virtuelle de la troisième entrée, la variable reçoit (3-1)*4 + 1 = 9. 3, pour la troisième entrée, -1, car les tableaux commencent avec l'index 0, *4, car les pointeurs sont sur 32 bits.
Donc un pointeur de fonction désignant une fonction virtuelle a une valeur impaire. Lors de l'appel effectif de la fonction sur un objet, GCC commence par tester la parité du pointeur de fonction. Si la valeur du pointeur est paire, c'est une fonction non virtuelle, l'adresse utilisée correspond à cette valeur. Si la valeur du pointeur est impaire, GCC récupère la table des fonctions virtuelles de l'objet, décrémente la valeur du pointeur de 1, se décale pour atteindre l'entrée correspondante et récupère l'adresse effective à exécuter.
Visual C++ introduit un cout supplémentaire pour l'appel de fonctions virtuelles par pointeur de fonction, car un saut supplémentaire est utilisé vers le thunk. GCC ne génère pas de fonctions thunk spécifiques, en revanche il introduit une pénalité pour chaque appel de fonction non virtuelle via un pointeur de fonction puisque la parité est systématiquement testée pour savoir quelle action entreprendre. L'appel de fonctions non virtuelles avec un pointeur de fonction n'a pas de surcoût avec Visual C++. 2 compilateurs, 2 choix différents. Probablement que d'autres compilateurs ont fait des choix encore différents de ceux-là.

XXIV - À retenir

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.

L'appel d'une fonction virtuelle sur un pointeur ou une référence utilise le type dynamique et est résolu à l'exécution.

Le type statique d'un pointeur ou d'une référence est celui défini dans le code. Le type dynamique est celui de l'objet effectivement pointé/référencé.

Un destructeur d'une classe utilisée pour l'héritage public doit être virtuel s'il est public ou protégé s'il est non virtuel.

Un destructeur public et non virtuel indique que la classe ne doit pas être utilisée pour un héritage public.

Une fonction virtuelle ne peut être inlinée dans un appel polymorphe.

Appeler une fonction virtuelle dans un constructeur ou un destructeur n'appelle pas la fonction la plus spécialisée de l'objet en cours de construction, mais celle disponible pour le constructeur en cours d'exécution.

L'appel d'une fonction virtuelle pure dans le constructeur ou le destructeur d'une classe abstraite produit un comportement indéterminé.

Evitez d'introduire des masquages de fonctions.

Une fonction générique (template) ne peut pas être virtuelle.

Une classe générique peut avoir des fonctions membres virtuelles.

L'amitié ne s'hérite pas : déclarer amie une fonction virtuelle est faiblement utile.

Une fonction virtuelle ne peut s'engager à lever plus d'exceptions que celles précisées dans la définition la plus spécialisée de ses classes de base.

Les préconditions d'une fonction virtuelle dans la classe dérivée ne peuvent être plus restrictives que celle de la définition la plus spécialisée de ses classes de base. En revanche, elles peuvent être plus lâches autorisant l'utilisation de la spécialisation avec le type dérivé là où la classe de base ne sait pas faire.

Les postconditions d'une fonction virtuelle dans la classe dérivée ne peuvent être plus lâches que celle de la définition la plus spécialisée de ses classes de base. En revanche, elles peuvent être plus restrictives.

En sortie d'une fonction virtuelle, les invariants de la classe dérivée et de la classe de base doivent être respectés.

Un transtypage d'un pointeur ou d'une référence depuis la classe de base vers la classe dérivée est souvent signe d'un problème de conception.

XXV - Un peu de lecture

Normes et livres :

- La norme (2003) : Programming languages C++ - ISO/IEC 14882:2003

- Le draft C++0x : Working Draft, Standard for Programming Language C++

- Le langage C++, de Bjarne Stroustrup

- Conception et programmation orientées objet, de Bertrand Meyer.

- Design Patterns, Catalogue de modèles de conception réutilisables, de Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides

Articles :

- Programmation par contrat, application en C++, de Julien Blanc

- Open MultiMethods for C++, de Peter Pirkelbauer, Yuriy Solodkyy et Bjarne Stroustrup (2007)

- Présentation des classes de Traits et de Politiques en C++, de Alp Mestan (2007)

- « Pure Virtual Function Called »: An Explanation, de Paul S. R. Chisholm (2007)

- Le principe « ouvert/fermé », d'Emmanuel Deloget (2006)

- Never Call Virtual Functions during Construction or Destruction, de Scott Meyers (2005)

- Implicit Virtual, de Herb Sutter et Jim Hyslop (2005)

- Adaptations, de Herb Sutter et Jim Hyslop (2004)

- Virtually Misbehavin'. C++ implicit overriding at work de Jim Hyslop et Herb Sutter (2004)

- Virtuality, de Herb Sutter (2001)

- Virtually Yours, de Jim Hyslop et Herb Sutter (2000)

- Multiple Inheritance for C++, de Bjarne Stroustrup (1999)

- The Open-Closed Principle, de Robert C. Martin (1996)

XXVI - Remerciements

Je remercie Alp, r0d et Goten pour leurs relectures et leurs conseils avisés et ainsi que Luc Hermitte dont la compétence, l'étendue des connaissances et l'attention aux petits détails n'ont d'égales que la patience, la persévérance et la générosité à les partager. Sans eux imprécisions, approximations, oublis et erreurs seraient bien trop nombreux dans cet article.
Encore merci à dourouc05 et à koala01 pour la grand-mère et l'aurteaugraffe.
Enfin, d'une façon plus globale, je remercie les membres des forums de développez.com qui, par la qualité de leurs interventions, m'ouvrent constamment de nouvelles pistes de réflexion sur ma pratique de développement.

(1)Dans cet article, les termes classes et structures sont employés indifféremment. En effet, la seule différence en ce qui nous concerne entre class et struct est la visibilité par défaut : publique pour les membres et l'héritage pour les types définis avec struct et privée pour les membres et l'héritage pour les types définis avec class. Pour tout ce que nous allons dire par la suite, la chose est strictement identique que le type soit struct ou class. Cf. F.A.Q. : Quelle est la différence entre class et struct ?

(2)Attention, le mot-clé virtual peut être utilisé pour l'héritage virtuel afin de résoudre le problème des classes de base communes (héritage en losange) : Qu'est-ce que l'héritage virtuel et quelle est son utilité ? Cela n'a strictement aucune influence sur les fonctions virtuelles.

(3)On lira à profit les précisions de la F.A.Q. : Qu'est-ce que le polymorphisme ? et Qu'est-ce que la surcharge ?

(4)Bien sûr, la visibilité des fonctions (publique, protégée ou privée) combinée à celle de l'héritage limite les cas où l'appel est valide. On lira avec profit l'entrée de la F.A.Q. suivante : Que signifient public, private et protected ?

(5)Par facilité d'écriture, dans ce document, pour une variable Type *p_var, on dira que son type est Type alors que formellement p_var est de type Type* (pointeur de Type) et c'est *p_var qui est de type Type. De même, pour les références Type & r_var, on dira que le type de r_var est Type et non Type&. Enfin, de la même façon on fera pudiquement l'impasse sur les qualifications const et/ou volatile qui peuvent préciser le type réel d'une variable.

(6)Le principe ouvert/fermé ou OCP (open/close principle pour les amis de Lord Byron) énonce qu'une classe doit être ouverte à l'extension et fermée à la modification. Apparemment contradictoire, ce principe s'appuie justement sur le mécanisme des fonctions virtuelles pour permettre l'extension par les classes dérivées (ouverture), mais cela doit se faire sans modifier le code existant (fermeture). Pour plus d'information, le billet Le principe « ouvert/fermé » d'Emmanuel Deloget propose une présentation de ce principe. The Open-Closed Principle est une présentation de ce même principe en anglais.

(7)Un comportement indéterminé est un comportement non spécifié par la norme. En conséquence, les compilateurs peuvent le gérer comme ils le souhaitent : par une erreur ou un avertissement à la compilation, par une erreur à l'exécution ou un fonctionnement à priori conforme. Il ne faut pas invoquer de comportements indéterminés même s'ils ont l'air de fonctionner, car un tel comportement peut changer silencieusement (au moins jusqu'au bug) dès que l'on change de compilateur ou de version de compilateur.

(8)En fait, on peut : mais au prix de transtypage sauvage, d'opérateurs de conversion ou de retour de fonction. Toute solution qui doit mettre la puce à l'oreille du développeur : ceci est potentiellement dangereux donc probablement une erreur !

(9)Pour être exact, en l'absence de directive de compilation explicite, le compilateur décide seul si l'appel est inliné ou pas éventuellement sans tenir compte des desiderata du codeur. En fait le mot-clé inline a une autre utilité plus technique : détourner la règle O.D.R. (One Definition Rule) en particulier pour les templates.

(10)inline et rapidité ne sont pas aussi mécaniques qu'il y paraît : voir l'entrée de la F.A.Q. Les fonctions inline améliorent-elles les performances ?

(11)Les codes de cet article ont été testés avec Visual C++ Express 2008 et GCC 4.4.1. (MinGW) sous Windows. Quand on parlera de Visual C++, ce sera sauf mention complémentaire pour cette édition Express. Et quand on parlera de GCC toujours sauf mention complémentaire, cela désignera GCC 4.4.1. (MinGW).

(12)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.

(13)La programmation par contrat a été introduite et développée par Bertrand Meyer dans son livre Conception et programmation orientées objet dont la lecture est toujours recommandée.

(14)Dans toute cette section, il est supposé que les fonctions sont appelées avec un objet par référence ou par pointeur afin de faire jouer le mécanisme de résolution dynamique de l'appel des fonctions virtuelles. Sans quoi, la fonction de la classe de base est toujours appelée ce qui ne nous intéresse pas dans ce cas.

(15)Les préconditions sont bien sûr non contradictoires : pour être vraie Pi ne doit pas nécessité que Pj soit fausse.