| auteurs : LFE, Gilles Louïse |
Un constructeur est ce qui construit un objet, initialise éventuellement les membres de la classe, alloue de la mémoire, etc...
On peut le comparer à une fonction d'initialisation de la classe.
On le reconnaît au fait qu'il porte le même nom que la classe elle-même. Sa déclaration se fait de la façon suivante :
class MaClasse
{
public :
MaClasse ();
} ;
|
|
lien : Qu'est-ce qu'un destructeur ?
|
| auteur : LFE |
Un constructeur par défaut est un constructeur qui peut être appelé sans paramètre. A noter qu'il peut s'agir d'un constructeur sans
paramètres, ou d'un constructeur dont les paramètres ont des valeurs par défaut.
class MaClasse1
{
public :
MaClasse1 ();
} ;
class MaClasse2
{
public :
MaClasse2 ( int i = 1 );
} ;
class MaClasse3
{
public :
MaClasse3 ( int i );
} ;
|
|
| auteur : LFE |
Un constructeur de copie, comme son nom l'indique, sert à copier l'objet, à partir d'un autre objet. Ce constructeur est utilisé pour
- la duplication d'un objet lors de la création d'un nouvel objet
- lors du passage en paramètre à une fonction, un template, ... pour générer l'objet utilisé en interne par cette fonction
- au retour d'une fonction renvoyant un objet, dans le but de mettre cet objet à disposition sur la pile pour
l'appelant
class MaClasse
{
public :
MaClasse ( const MaClasse & );
} ;
|
|
| auteurs : JolyLoic, Luc Hermitte |
Le constructeur est une fonction spéciale appelée automatiquement à certains
endroits, une fonction init n'est aucune sémantique spéciale, l'utilisateur
doit l'appeler manuellement (risque d'oubli). En revanche, un constructeur ne
peut pas être polymorphe, ni appeler une fonction de sa classe de manière
polymorphe. L'appel de la fonction init est donc une nécessité dans les cas
extrêment rares où l'on a besoin de "postconstruction polymorphe".
Cela revient à dire qu'il faut utiliser uniquement un constructeur. Dans certains cas,
on n'a pas assez d'infos à la construction de l'objet pour tout initialiser,
d'où une fonction init (toutefois, cela reste assez rare).
Pour signaler une erreur dans le constructeur, il peut lancer une
exception, et dans ce cas, l'objet n'est pas construit du tout.
Si on suit le code exemple suivant :
T * t = new T ();
t- > init ();
return t;
|
Si init lève une exception, la mémoire (ou autres ressources) allouée pour t n'est jamais libérée. Pour
résoudre ce problème, il existe deux solutions :
- Fusionner le résidu init() dans le constructeur principal des classes qui sont refactorisées.
- Utiliser les "auto_ptr<>".
|
| auteurs : Marshall Cline, Aurélien Regat-Barrel |
Une grande différence !
Supposez que MaClasse soit le nom d'une classe. Dans l'exemple suivant :
MaClasse x;
MaClasse y ();
|
x est bien un objet de type MaClasse, alors que y est la déclaration d'une fonction qui retourne un objet de type MaClasse !
Et ceci même si l'on se trouve dans le corps d'une fonction :
# include "MaClasse.h"
int main ()
{
MaClasse x;
MaClasse y ();
}
|
|
| auteur : Marshall Cline |
Absolument pas.
Prenons un exemple. Supposons que l'on veuille que le constructeur Foo::Foo(char) appelle un autre constructeur de la même classe,
Foo::Foo( char, int ) de façon que Foo::Foo( char, int ) initialise l'objet this. Malheureusement, ce n'est pas possible
en C++.
Pourtant, certaines personnes le font, mais cela ne fait pas ce qu'elles désirent. Par exemple,
n'appelle pas
de l'objet désigné par this. Par contre,
est appelé pour initialiser un objet local (et pas celui désigné par 'this') et qui est ensuite détruit immédiatement à la fin de l'appel
class Foo
{
public :
Foo ( char x );
Foo ( char x, int y );
} ;
Foo:: Foo ( char x )
{
Foo ( x, 0 );
}
|
Il est cependant possible de combiner deux constructeurs grâce aux paramètres par défaut
class Foo
{
public :
Foo ( char x, int y = 0 );
} ;
|
Si cela ne fonctionne pas, par exemple s'il n'y a pas une valeur du paramètre par défaut qui permet de combiner les deux constructeurs,
il est possible de partager leur code commun via une fonction membre privée d'initialisation.
class Foo
{
public :
Foo ( char x );
Foo ( char x, int y );
private :
void init ( char x, int y );
} ;
Foo:: Foo ( char x )
{
init ( x, int ( x ) + 7 );
}
Foo:: Foo ( char x, int y )
{
init ( x, y );
}
void Foo:: init ( char x, int y )
{
}
|
|
| auteur : Marshall Cline |
Non. Un "constructeur par défaut" est un constructeur qui peut s'appeler sans arguments. Ainsi un constructeur qui ne prend aucun
argument est certainement un constructeur par défaut :
class Fred
{
public :
Fred ();
} ;
|
Toutefois il est possible (et probable) qu'un constructeur par défaut prenne des arguments, s'ils sont spécifiés par défaut :
class Fred
{
public :
Fred ( int i = 3 , int j = 5 );
} ;
|
|
| auteur : Marshall Cline |
Le constructeur par défaut.
Il n'y a aucun moyen de demander au compilateur d'appeler un constructeur différent. Si votre class Fred n'a pas de constructeur
par défaut, une tentative de créer un tableau de Fred, se soldera par une erreur de compilation.
class Fred
{
public :
Fred ( int i, int j );
} ;
int main ()
{
Fred a[ 10 ];
Fred * p = new Fred[ 10 ];
}
|
Cependant si vous créez un vector <Fred> plutôt qu'un tableau standard de Fred (ce que vous devriez faire de toute façon puisque utiliser
les tableaux est mauvais), vous n'avez plus besoin d'avoir un constructeur par défaut dans class Fred, puisque vous passez un objet
Fred au vector pour initialiser les éléments :
# include <vector>
int main ()
{
std:: vector < Fred> a (10 , Fred (5 ,7 ));
}
|
Même si la plupart du temps, il vaut mieux utiliser un vecteur plutôt qu'un tableau, il y a certaines circonstances ou le tableau est la
meilleure chose à utiliser. Dans ce cas, il existe "l'initialisation explicite de tableau" :
class Fred
{
public :
Fred (int i, int j);
} ;
int main ()
{
Fred a[10 ] = {
Fred (5 ,7 ), Fred (5 ,7 ), Fred (5 ,7 ), Fred (5 ,7 ), Fred (5 ,7 ),
Fred (5 ,7 ), Fred (5 ,7 ), Fred (5 ,7 ), Fred (5 ,7 ), Fred (5 ,7 )
} ;
}
|
Il n'est bien sur pas obligatoire de mettre un Fred(5,7) pour chaque entrée, vous pouvez en spécifier n'importe quel nombre. L'important
est que cette syntaxe est possible mais pas aussi belle que la syntaxe du vecteur.
Souvenez-vous : les tableaux sont mauvais, à moins qu'il y ait une raison valable d'en utiliser un, utilisez plutôt un vecteur.
|
| auteur : Marshall Cline |
Les listes d'initialisation. En fait, les constructeurs devraient initialiser tous les objets membre dans la liste d'initialisation.
Par exemple, ce constructeur initialise l'objet membre x_ en utilisant une liste d'initialisation :
Fred:: Fred () : x_ (n' importe quoi)
{
}
|
Le bénéfice de faire cela est une performance accrue. Par exemple, si l'expression n'importe quoi est identique à la variable membre
x_, le résultat de l'expression n'importe quoi sera intégré directement dans x_ (le compilateur ne fait pas une copie séparée de
l'objet). Même si les types ne sont pas identiques, le compilateur est habituellement capable de faire un meilleur travail à partir
des listes d'initialisation qu'à partir des affectations.
L'autre façon (inefficace) de faire un constructeur est d'utiliser les affections
Fred:: Fred () : x_ ()
{
x_ = n' importe quoi;
}
|
Dans ce cas, l'expression n'importe quoi provoque la création d'un objet temporaire, et cet objet temporaire est passé à l'opérateur
d'assignation de l'objet. Cet objet temporaire est ensuite détruit, ce qui est inefficace.
Comme si ce n'était pas suffisant, il y a une autre source d'inefficacité lors de l'utilisation de l'affectation dans le constructeur,
l'objet membre sera construit entièrement par le constructeur par défaut, ce qui peut par exemple allouer de la mémoire, ouvrir des fichiers,
par défaut. Tout ce travail pourrait être inutile si l'expression n'importe quoi faisait fermer ces fichiers, désallouer cette
mémoire (par exemple si la mémoire allouée par le constructeur par défaut n'était pas suffisante, ou que ce ne soit pas le bon fichier.)
Conclusion : toutes choses restant égales par ailleurs, votre code tournera plus vite si vous utilisez les listes d'initialisation plutôt
que l'assignation.
Note : Il n'y a pas de différence de performance si le type de x_ est de base, comme int, ou char *, ou float. Mais même dans ce
cas, ma préférence personnelle est d'initialiser ses données dans la liste d'initialisation plutôt que par affectation par soucis de
cohérence. Un autre argument lié à la symétrie en faveur de l'utilisation des listes d'initialisation même pour les types de base :
la valeur des membres de données constantes non statiques ne peut pas être initialisée dans le constructeur, donc pour conserver la
symétrie, je recommande d'initialiser tout dans la liste d'initialisation.
|
| auteurs : Marshall Cline, Aurélien Regat-Barrel |
Certains pensent qu'on ne devrait pas utiliser le pointeur this dans un constructeur parce que l'objet n'est pas complètement formé.
Pourtant il est possible d'utiliser le pointeur this dans le corps du constructeur et même dans la liste d'initialisation si l'on est prudent.
Voilà quelque chose qui fonctionne toujours : le corps du constructeur (ou d'une fonction appelée depuis le constructeur) peut
accéder aux données membres déclarées dans une classe de base et/ou à celles déclarées dans la classe elle-même en toute
sécurité. Ceci parce que le langage nous assure que toutes ces données membres ont été complètement construites au moment où le corps du
constructeur est exécuté.
Voilà quelque chose qui ne fonctionne jamais : le corps du constructeur (ou d'une fonction qu'il appelle) ne peut pas descendre
dans une classe dérivée en appelant une fonction membre virtual qui est redéfinie dans une classe dérivée. Si votre but était d'exécuter le code
de la fonction virtuelle, cela ne fonctionnera pas.
Notez que vous n'obtiendrez pas la version de la classe dérivée indépendamment de
la manière d'appeler la fonction membre virtuelle : en utilisant explicitement this (this->method()), ou implicitement sans utiliser
le pointeur this (method()), ou même en appelant quelque autre fonction qui appelle la fonction membre virtuelle en question a partir du
pointeur this.
Ceci parce que l'appelant est en train de construire un objet d'un type dérivé, et donc que la construction de ce dernier n'est pas encore terminée.
Donc votre objet qui fait office de classe de base n'appartient pas encore à cette classe dérivée.
Voilà quelque chose qui fonctionne parfois : si vous passez n'importe quelle donnée membre de l'objet au constructeur d'initialisation
d'une autre donnée membre, vous devez vous assurer que l'autre donnée membre a déjà été initialisée. La bonne nouvelle est que vous
pouvez déterminer si l'autre donnée membre a (ou non) déjà été initialisée en utilisant des règles du langage indépendantes du
compilateur que vous utilisez. La mauvaise nouvelle est qu'il vous faut connaître ces règles. Les sous-objets de la classe de base
sont initialisés en premier (vérifiez l'ordre si vous avez de l'héritage multiple et/ou de l'héritage virtuel !), ensuite viennent les
données membres définies dans la classe qui est initialisée dans l'ordre dans lequel elles apparaissent dans la déclaration de la classe.
Si vous ne connaissez pas ces règles alors ne passez aucune donnée membre depuis l'objet this (cela ne dépend pas de l'utilisation
explicite de this) vers l'initialiseur d'une autre donnée membre ! Si vous connaissez ces règles, soyez tout de même très vigilants.
|
| auteurs : JolyLoic, Laurent Gomila |
Oui, c'est possible, mais attention, ça ne fait pas toujours ce qu'on pense.
La première approche consiste à comprendre que lors de l'appel du
constructeur d'une classe de base, la classe dérivée n'a pas encore été
construite. Donc, c'est la méthode spécialisée à ce niveau qui est appelée.
Voyons en détail :
La règle est que le type dynamique d'une variable en cours de construction
est celui du constructeur qui est en train d'être exécuté. Pour bien
comprendre ce qui se passe, il faut donc revenir sur la différence entre le type statique
d'une variable, et son type dynamique.
Prenons par exemple trois classes, C qui dérive de B qui dérive de A. Par exemple, dans :
La variable a possède comme type statique (son type déclaré dans le programme) A*.
Par contre, son type dynamique est B*. Une fonction virtuelle est simplement une
fonction dont on va chercher le code en utilisant le type dynamique de la variable,
au lieu de son type statique, comme une fonction classique.
Maintenant, quand on crée un objet de type C, les choses se passent ainsi :
- On alloue assez de mémoire pour un objet de la taille de C.
- On initialise la sous partie correspondant à A de l'objet.
- On appelle le corps du constructeur de A. Pendant cet appel, l'objet crée a pour type dynamique A.
- On initialise la sous partie correspondant à B de l'objet.
- On appelle le corps du constructeur de B. Pendant cet appel, l'objet crée a pour type dynamique B.
- On initialise la sous partie correspondant à C de l'objet.
- On appelle le corps du constructeur de C. Pendant cet appel, l'objet crée a pour type dynamique C.
Donc, dans le corps du constructeur de la classe B, un appel d'une fonction virtuelle
appellera la version de la fonction définie dans la classe B (ou à défaut celle définie
dans A si la fonction n'a pas été définie dans B), et non pas celle définie dans la
classe C.
D'ailleurs, si la fonction est virtuelle pure dans B, ça causera quelques problèmes,
puisqu'on tentera alors d'appeler une fonction qui n'existe pas. En général, le programme
va planter, si on a de la chance, il affichera une message du style "Pure function called".
La problèmatique est exactement la même pour les destructeurs, mais dans l'ordre inverse.
Pourquoi cette règle ? Une fonction définie dans C a accès aux données membre de C.
Or, on a vu que au moment où on exécute l'appel au corps du constructeur de B,
ces dernières ne sont pas encore créées. On a donc préféré jouer la sécurité.
|
lien : Dans quel ordre sont construits les différents composants d'une classe ?
|
| auteur : Marshall Cline |
Une technique qui fournit des exécutions plus intuitives et/ou plus sûres de construction pour des utilisateurs de votre classe.
Le problème est que les constructeurs ont toujours le même nom que la classe. Par conséquent la seule façon de les différencier se fait
via la liste des paramètres. Mais s'il y a beaucoup de constructeurs, les différences entre les constructeurs deviennent quelque peu
subtiles et sujettes à erreur.
Avec l'idiome du constructeur nommé, vous déclarez les constructeurs de toute la classe dans l'une des sections private ou protected.
Vous fournissez des fonctions déclarées statiques dans la section public: qui renvoient un objet. Ces fonctions statiques sont connues comme
"constructeurs nommés". En général il y a une telle fonction statique pour chaque manière différente de construire l'objet.
Par exemple, supposez que nous construisions une classe Point qui représente une position sur le plan X/Y. Il s'avère qu'il y a deux
façons d'indiquer une coordonnée dans un espace bidimensionnel : les coordonnées cartésiennes (X+Y) et les coordonnées polaires (Distance+Angle).
(Pour cet exemple, l'essentiel est de retenir qu'il y a plusieurs façons de créer un point). Malheureusement les paramètres pour
ces deux systèmes de coordonnées sont identiques : deux réels. Ceci créerait une ambiguïté dans les constructeurs surchargés :
class Point
{
public :
Point (float x, float y);
Point (float r, float a);
} ;
int main ()
{
Point p = Point (5 .7 , 1 .2 );
}
|
Une manière de résoudre cette ambiguïté est d'utiliser l'idiome du constructeur nommé :
# include <cmath> // Pour avoir sin() et cos()
class Point
{
public :
static Point rectangular (float x, float y);
static Point polar (float radius, float angle);
private :
Point (float x, float y);
float x_, y_;
} ;
inline Point:: Point (float x, float y)
: x_ (x), y_ (y)
{
}
inline Point Point:: rectangular (float x, float y)
{
return Point (x, y);
}
inline Point Point:: polar (float radius, float angle)
{
return Point ( radius * cos (angle), radius * sin (angle) );
}
|
Maintenant les utilisateurs du point ont une syntaxe claire et non ambiguë pour créer des points dans l'un ou l'autre système de
coordonnées :
int main ()
{
Point p1 = Point:: rectangular (5 .7 , 1 .2 );
Point p2 = Point:: polar (5 .7 , 1 .2 );
}
|
Faites attention à déclarer vos constructeurs dans la section protected : si vous vous attendez à ce que Point ait des classes dérivées.
L'idiome du constructeur nommé peut aussi être utilisé pour vous assurer que les objets d'une classe sont toujours créés avec l'opérateur new.
|
| auteur : Marshall Cline |
C'est une méthode très utile pour exploiter le chaînage des fonctions.
Le principal problème solutionné par l'idiome des paramètres nommés est que le C++ ne supporte que les "paramètres par position".
Par exemple, une fonction appelante ne peut pas dire "Voici la valeur pour le paramètre xyz, et voici autre chose pour le paramètre pqr".
Tout ce que vous pouvez faire en C++ (ou en C ou en Java) est "voici le premier paramètre, le second, etc...." L'alternative, appelée
"paramètres nommés" et implémentée en Ada, est particulièrement utile si une fonction prend un nombre important de paramètres dont la
plupart supportent des valeurs par défaut.
Au cours des années, de nombreuses personnes ont mis au point des astuces pour contourner ce manque de paramètres nommés en C et en C++.
Une d'entre elles implique d'intégrer la valeur du paramètre dans une chaîne et de découper cette chaîne à l'exécution. C'est ce qui se
passe pour le second paramètre de la fonction fopen(), par exemple. Une autre astuce est de combiner tous les paramètres booléens dans
un champ de bits, et la fonction fait un ou logique pour obtenir la valeur du paramètre désiré. C'est ce qui se passe avec le second
paramètre de la fonction open(). Cette approche fonctionne, mais la technique exposée ci-dessous produit un code plus simple à écrire et
à lire, plus élégant.
L'idée est de transformer les paramètres de la fonction en des fonctions membres d'une nouvelle classe, dans laquelle toutes ces fonctions renvoient
*this par référence. Vous renommez ensuite simplement la fonction principale en une fonction sans paramètre de cette classe.
Prenons un exemple pour rendre les choses plus claires.
L'exemple sera pour le concept 'ouvrir un fichier'. Ce concept a besoin logiquement d'un paramètre pour le nom du fichier, et éventuellement
d'autres paramètres pour spécifier si le fichier doit être ouvert en lecture, en écriture, ou encore en lecture/écriture ; si le fichier
doit être créé s'il n'existe pas déjà ; s'il doit être ouvert en ajout (append) ou en sur-écriture (overwrite) ; la taille des blocs à lire
ou écrire ; si les entrées-sorties sont bufferisées ou non ; la taille du buffer ; si le mode est partagé ou exclusif ; et probablement d'autres
encore. Si nous implémentions ce concept via une fonction classique avec des paramètres par position, l'appel de cette fonction serait
assez désagréable à lire. Il y aurait au moins 8 paramètres, ce qui est source d'erreur. Utilisons donc à la place l'idiome des paramètres
nommés.
Avant de nous lancer dans l'implémentation, voici à quoi devrait ressembler le code appelant, en supposant que l'on accepte toutes les
valeurs par défaut des paramètres.
File f = OpenFile (" foo.txt " );
|
C'est le cas le plus simple. Maintenant, voyons ce que cela donne si nous voulons changer certains des paramètres.
File f = OpenFile (" foo.txt " ).
readonly ().
createIfNotExist ().
appendWhenWriting ().
blockSize (1024 ).
unbuffered ().
exclusiveAccess ();
|
Il est à noter comment les "paramètres", pour autant que l'on puisse encore les appeler comme cela, peuvent être dans un ordre aléatoire
(ils ne sont pas positionnés) et qu'ils sont nommés. Le programmeur n'a donc pas besoin de se souvenir de l'ordre des paramètres ; de plus
les noms sont évidents.
Voici comment l'implémenter. Nous créons d'abord une nouvelle classe OpenFile qui contient toutes les valeurs des paramètres en
tant que membres de données privées. Ensuite, toutes les fonctions membres (readonly(), blockSize(unsigned), etc ....) renvoient *this
(c'est-à-dire qu'elles renvoient une référence sur l'objet OpenFile, autorisant ainsi le chaînage des appels des fonctions). Pour terminer,
nous spécifions les paramètres requis (le nom du fichier dans le cas présent) dans un paramètre normal (c'est-à-dire par position) passé
au constructeur de OpenFile/
class File;
class OpenFile
{
public :
OpenFile (const string& filename);
OpenFile& readonly ();
OpenFile& createIfNotExist ();
OpenFile& blockSize ( unsigned nbytes );
private :
friend File;
bool readonly_;
unsigned blockSize_;
} ;
|
La seule autre chose à faire est que le constructeur de File accepte un objet OpenFile.
class File
{
public :
File (const OpenFile& params);
} ;
|
A noter que OpenFile déclare File en tant que classe amie. De cette façon, OpenFile n'a pas besoin de définir une série d'accesseurs.
Etant donné que chaque fonction membre de la chaîne renvoie une référence, il n'y a pas de copie d'objets et le tout est très efficace.
De plus, si les différentes fonctions membres sont déclarées inline, le code généré ressemblera probablement à du code C qui positionne
certains membres d'une structure. Bien sur, si les fonctions membres ne sont pas déclarées inline, il risque d'y avoir une légère
augmentation de la taille du code et une légère perte de performance (mais uniquement si la construction se passe dans certaines
circonstances, comme nous l'avons vu précédemment). Cela peut donc, dans ce cas être un compromis qui rendra le code plus robuste.
|
| auteur : 3DArchi |
Les constructeurs sont appelés dans l'ordre suivant :
- 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.
# include <iostream>
# include <string>
struct MembreA{
MembreA (){ std:: cout< < " MembreA " < < std:: endl;}
} ;
struct A {
A (){ std:: cout< < " A " < < std:: endl;}
MembreA m;
} ;
struct MembreB{
MembreB (){ std:: cout< < " MembreB " < < std:: endl;}
} ;
struct B : A {
B (){ std:: cout< < " B " < < std:: endl;}
MembreB m;
} ;
struct MembreC{
MembreC (){ std:: cout< < " MembreC " < < std:: endl;}
} ;
struct C : A {
C (){ std:: cout< < " C " < < std:: endl;}
MembreC m;
} ;
struct MembreD{
MembreD (){ std:: cout< < " MembreD " < < std:: endl;}
} ;
struct D : B, C { D (){
std:: cout< < " D " < < std:: endl;}
MembreD m;
} ;
struct MembreE{ MembreE (){
std:: cout< < " MembreE " < < std:: endl;}
} ;
struct E : virtual A { E (){
std:: cout< < " E " < < std:: endl;}
MembreE m;
} ;
struct MembreF{ MembreF (){
std:: cout< < " MembreF " < < std:: endl;}
} ;
struct F : virtual A {
F (){ std:: cout< < " F " < < std:: endl;}
MembreF m;
} ;
struct MembreG{
MembreG (){ std:: cout< < " MembreG " < < std:: endl;}
} ;
struct G {
G (){ std:: cout< < " G " < < std:: endl;}
MembreG m;
} ;
struct MembreH{
MembreH (){ std:: cout< < " MembreH " < < std:: endl;}
} ;
struct H : G, F {
H (){ std:: cout< < " H " < < std:: endl;}
MembreH m;
} ;
struct MembreI{
MembreI (){ std:: cout< < " MembreI " < < std:: endl;}
} ;
struct I : E, G, F {
I (){ std:: cout< < " I " < < std:: endl;}
MembreI m;
} ;
template < class T> void Creation ()
{
std:: cout< < " Creation d'un " < < typeid (T).name ()< < " : " < < std:: endl;
T t;
}
int main ()
{
Creation< A> ();
Creation< B> ();
Creation< C> ();
Creation< D> ();
Creation< E> ();
Creation< F> ();
Creation< G> ();
Creation< H> ();
Creation< I> ();
return 0 ;
}
|
Ce code produit comme sortie :
Creation d'un struct A :
MembreA
A
Creation d'un struct B :
MembreA
A
MembreB
B
Creation d'un struct C :
MembreA
A
MembreC
C
Creation d'un struct D :
MembreA
A
MembreB
B
MembreA
A
MembreC
C
MembreD
D
Creation d'un struct E :
MembreA
A
MembreE
E
Creation d'un struct F :
MembreA
A
MembreF
F
Creation d'un struct G :
MembreG
G
Creation d'un struct H :
MembreA
A
MembreG
G
MembreF
F
MembreH
H
Creation d'un struct I :
MembreA
A
MembreE
E
MembreG
G
MembreF
F
MembreI
I
|
Remarque 1 : La subtilité réside dans la primauté accordée à
l'héritage virtuel sur l'héritage non virtuel.
Remarque 2 : L'ordre de construction est fixé par la norme et ne dépend pas
des listes d'initialisation du code :
struct Membre1
{
Membre1 (){ std:: cout< < " Membre1 " < < std:: endl;}
} ;
struct Membre2
{
Membre2 (){ std:: cout< < " Membre2 " < < std:: endl;}
} ;
struct A {
A (){ std:: cout< < " A " < < std:: endl;}
} ;
struct B {
B (){ std:: cout< < " B " < < std:: endl;}
} ;
struct C : A,B {
C ()
:m2 (),B (),m1 (),A ()
{ std:: cout< < " C " < < std:: endl;}
Membre1 m1;
Membre2 m2;
} ;
int main ()
{
C c;
return 0 ;
}
|
Ce code produit :
Certains compilateurs peuvent sortir un avertissement lorsque la liste
d'initialisation ne suit pas l'ordre de déclaration mais ce n'est pas
toujours le cas. Pour les listes d'initialisation, une bonne pratique
est de toujours suivre l'ordre défini par la norme pour éviter tout
risque de confusion.
Remarque 3 : Pour l'héritage virtuel, le constructeur appelé
est celui spécifié par le type effectivement instancié et non par
celui spécifié par le type demandant l'héritage. Si le type instancié
ne spécifie pas de constructeur, alors c'est celui par défaut :
struct A
{
A (std:: string appelant_= " defaut " )
{
std:: cout< < " A construit par " < < appelant_< < std:: endl;
}
} ;
struct B : virtual A
{
B ()
:A (" B " )
{
}
} ;
struct C : B
{
C ()
{
}
} ;
struct D : B
{
D ()
:A (" D " )
{
}
} ;
template < class T> void Creation ()
{
std:: cout< < " Creation d'un " < < typeid (T).name ()< < " : " < < std:: endl;
T t;
}
int main ()
{
Creation< B> ();
Creation< C> ();
Creation< D> ();
return 0 ;
}
|
Ce code produit :
Creation d'un struct B :
A construit par B
Creation d'un struct C :
A construit par defaut
Creation d'un struct D :
A construit par D
|
Conclusion :
Le constructeur d'une classe doit monter sa liste d'initialisation
suivant cet ordre :
- les constructeurs des classes héritées virtuellement
dans tout l'arbre d'héritage en profondeur croissante et
de gauche à droite ;
- les constructeurs des classes de base directement
héritées dans l'ordre de gauche à droite ;
- les membres dans l'ordre de leur déclaration.
Ceci a comme conséquences :
- Ce sont d'éventuelles contraintes dans l'ordre de
construction qui imposeront l'ordre d'héritage (et non des
approches de type d'abord le public, puis le privé).
- Toute dépendance de construction entre les variables
membres devra être explicitement commentée à défaut de pouvoir
être évitée. Par cette documentation, les lecteurs du code
sont avertis qu'il s'agit d'un comportement compris et
maîtrisé par le développeur : la fiabilitié est accrue et
la maintenance est facilitée.
|
Consultez les autres F.A.Q.
|
|