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'initialiation 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 :
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++.
Pourtaint, certaines personnes le font, mais cela ne fait pas ce qu'elles désirent. Par exemple,
line Foo(x, 0);
n'appelle pas
Foo::Foo(char,int)
de l'objet désigné par 'this'. Par contre,
Foo::Foo(char,int)
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); // this line does NOT help initialize this object !!
...
}
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); // this line combines the two constructors
...
};
Si cela ne fonctionne pas, par exemple si 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)
{
...
}
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(); // constructeur par défaut : peut s'appeler sans args // ...
};
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); // constructeur par défaut : peut s'appeler sans args // ...
};
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.
classe Fred {
public:
Fred(int i, int j);
// ... suppose qu'il n'y a aucun constructeur par défaut
};
main()
{
Fred a[10]; // ERREUR : Fred n'a pas de constructeur par défaut
Fred * p = new Fred[10]; / / ERREUR: Fred n'a pas de constructeur par défaut
}
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
les tableaux sont 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>
main()
{
std::vector <Fred> a(10, Fred(5,7));
// 10 objets Fred dans le vecteur a seront initialisés avec 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);
//...assume there is no default constructor in class Fred...
};
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)
};
// The 10 Fred objects in array a will be initialized with 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.
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 bases :
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 recommende d'initialiser tout dans la liste d'initialisation.
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'initialiaation si on est prudent.
Voilà quelque chose qui fonctionne toujours : le (corps du) constructeur (ou une fonction appelée depuis le constructeur) peuvent
accéder aux membres de donnée déclarés dans une classe de base et/ou aux membres de donnée déclarés dans la classe elle-même en toute
sécurité. C'est parce que tous ces membres de donnée sont assurés d'avoir été complètement construits au moment où le (corps du)
constructeur commence à être exécuté.
Voilà quelque chose qui ne fonctionne jamais : le (corps du) constructeur (ou une fonction appelée par lui) ne peut pas descendre
dans une classe dérivée en appelant une méthode virtual qui est redéfinie dans une classe dérivée. Si votre but était d'exécuter le code
de la fonction virtuelle, ça ne fonctionnera pas. Notez que vous n'obtiendrez pas la version de la classe dérivée indépendemment 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. La clé est que même si l'appelant est en train de construire un objet d'un type dérivé, pendant la construction de la
classe de base, votre objet n'appartient pas encore à cette classe dérivée.
Voilà quelque chose qui fonctionne parfois : si vous passez n'importe quel membre de donnée de l'objet au constructeur d'initialisation
d'un autre membre de donnée, vous devez vous assurer que l'autre membre de donnée a déjà été initialisé. La bonne nouvelle est que vous
pouvez déterminer si l'autre membre de donnée a (ou non) déjà été initialisé 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érifier l'ordre si vous avez de l'héritage multiple et/ou de l'héritage virtuel !), ensuite viennent les
membres de donnée définis dans la classe qui est initialisée dans l'ordre dans lequel ils apparaissent dans la déclaration de la classe).
Si vous ne connaissez pas ces règles alors ne passez aucun membre de donnée depuis l'objet this (cela ne dépend pas de l'utilisation
explicite de this) vers l'initialiseur d'un autre membre de donnée ! Et si vous connaissez ces règles, s'il vous plaît faites attention.
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 méthodes declarée static dans la section public: qui renvoient un objet. Ces méthodes statiques sont connus comme
"constructeurs nommés". En général il y a une telle méthode 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 bi-dimensionnel : coordonnées rectangulaires (X+Y), coordonnées polaires (Distance+Angle).
(Ne vous inquiétez pas si vous ne pouvez pas vous rappeler ces derniers ; les conditions particulières des systèmes de coordonnées
représentant un point importent peu ; l'important est 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); // Coordonnées rectangulaires
Point(float r, float a); // Coordonnées polaires (distance et angle) // ERROR: Surcharge ambiguë: Point::Point(float, float)
};
main()
{
Point p = Point(5.7, 1.2); // Ambigu : De quel système de coordonnées parle-t-on ?
}
Une manière de résoudre cette ambiguïté est d'utiliser l'idiome du constructeur nommé :
#include <math.h> // Pour avoir sin() et cos()class Point {
public:
static Point rectangular(float x, float y); // Coords rectangulairesstatic Point polar(float radius, float angle); // Coords polaires // Ces méthodes static sont les "constructeurs nommés" // ...private:
Point(float x, float y); // coordonnées rectangulairesfloat 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 :
main()
{
Point p1 = Point::rectangular(5.7, 1.2); // Evidemment rectangulaire
Point p2 = Point::polar(5.7, 1.2); // Evidemment polaire
}
Faîtes attention à déclarer vos constructeurs dans la section protected: si vous vous attendez à ce que Fred 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
Parce que vous devez définir explicitement les membres statiques de votre classe.
Fred.h
class Fred {
public:
Fred();
// ...private:
int i_;
staticint j_;
};
Fred.cpp
Fred::Fred()
: i_(10) // OK : vous pouvez (et vous devriez) initialiser les données membres de cette façon
j_(42) // Error : vous ne pouvez pas initialiser une donnée static comme ça.
{
// ...
}
// Vous devez définir les données statiques de cette façon :int Fred::j_ = 42;
Parce que les données membres statiques doivent être explicitement définies dans exactement une unité de compilation.
Si vous n'avez pas fait cela, vous avez certainement eu une erreur du type "undefined external" (réference externe non définie) par l'
éditeur de liens (linker).
Fred.h
class Fred {
public:
...
private:
staticint j_; // Declare la donnée membre static Fred::j_
...
};
L'éditeur de liens vous grondera "Fred::j_ is not defined" (Fred::j_ n'est pas défini) à moins que vous ne définissiez
(par opposition à déclariez) Fred::j_ dans (exactement) un de vos fichiers source
Fred.cpp
#include "Fred.h"int Fred::j_ = some_expression_evaluating_to_an_int;
// A côté de ça, si vous souhaitez utiliser la valeur implicite des entiers static :
// int Fred::j_;
La place habituelle pour définir une donnée membre static de la classe Fred est dans le fichier Fred.cpp
Le fiasco dans l'ordre d'initialisation des variables statiques est un des aspects les plus subtiles et habituellement mal compris du C++.
Malheureusement, il est très difficile à détecter étant donné que l'erreur se produit avant même le début de l'exécution du main().
Supposons que l'on ait deux objects statiques x et y qui se trouvent dans des fichiers sources séparés (x.cpp et y.cpp) Supposons ensuite
que l'initialisation de l'objet y (typiquement le constructeur de l'objet y) appelle une méthode de l'objet x.
C'est aussi simple que cela.
La tragédie est que vous avez 50% de chances de vous planter. S'il arrive que l'unité de compilation correspondant à x.cpp soit
initialisée avant celle correspondant à y.cpp, tout va bien. Mais si l'unité de compilation correspondant à y.cpp est initialisée
d'abord, alors le constructeur de y sera en route avant le constructeur de x, et vous êtes cuit. C'est à dire que le constructeur de y
appelera une méthode de l'objet x, alors que l'objet x n'a pas encore été construit.
Si vous pensez que c'est "excitant" de jouer à la roulette russe avec la moitié du barillet chargé, vous pouvez vous arrêter de lire ici.
Si au contraire vous aimez augmenter vos chances de survie en prévenant les désastres de manière systématique, vous serez probablement
intéressé par la question suivante.
Note : le fiasco dans l'ordre d'initialisation des variables statiques peut aussi se produire, dans certains cas, avec les types de base.
Utilisez l'idiome de "construction à la première utilisation", qui consiste simplement à emballer (wrap) vos objets statiques à
l'intérieur d'une fonction.
Par exemple, supposez que vous ayez deux classes, Fred et Barney. Il y a un objet Fred global appelé x, et un objet Barney global
appelé y. Le constructeur de Barney invoque la méthode goBowling() (va jouer au bowling) de l'objet x. Le fichier x.cpp définitl'objet x
x.cpp
#include "Fred.hpp"
Fred x;
Le fichier y.cpp définit l'objet y:
y.cpp
#include "Barney.hpp"
Barney y;
Pour être complet, le constructeur de Barney pourraît ressembler à quelque chose comme :
Comme décrit ci-dessus , le désastre intervient si y est construit avant x, ce qui arrive 50% du temps puisqu'ils sont dans deux
fichiers sources différents.
Il y a beaucoup de solutions à ce problème, mais une solution très simple et complètement portable est de remplacer l'objet
(de type Fred) global x, par une fonction globale x(), qui retourne par réference l'objet Fred.
x.cpp
#include "Fred.hpp"
Fred& x()
{
static Fred* ans = new Fred();
return *ans;
}
Puisque les objet locaux statiques sont construits la première fois (et seulement la première fois) où le flux de contrôle passe sur la
déclaration, l'instruction new Fred() sera non seulement exécutée une fois : la première fois que x() est appelée,
mais chaque appel suivant retournera le même objet de type Fred (celui pointé par ans). Tout ce qu'il reste à faire est de changer x
en x() :
C'est ce qu'on appele "Idiome de la construction à la première utilisation", parce que c'est exactement ce qu'il fait : l'objet global Fred
est créé lors de sa première utilisation.
Le défaut de cette approche est que l'objet Fred n'est jamais detruit. Il existe une seconde technique qui solutionne ce problème mais il
faut l'utiliser prudemment étant donné qu'il risque de créer un autre problème très sale.
Note : le fiasco dans l'ordre d'initialisation des variables statiques peut aussi se produire, dans certains cas, avec les types de base.
La réponse courte : il est possible d'utiliser un objet statique plutôt qu'un pointeur statique, mais faire cela ouvre la porte à un autre
problème aussi subtile que pervers.
La réponse longue : Parfois, les gens s'inquiètent des problèmes de fuite mémoire de la solution précédente. Dans la plupart des cas, ce
n'est pas un problème, mais par contre cela peut en être un dans d'autres circonstances. Note : Même si l'objet pointé par ans
dans la question précédente n'est jamais libéré, la mémoire n'est pas perdue quand le programme se termine, étant donné que l'OS récupère
automatiquement l'entièreté de la mémoire allouée au programme quand celui-ci se termine. En d'autres mots, le seul moment ou vous devez
vous en inquiéter est celui où le destructeur de l'objet Fred effectue certaines actions importantes (par exemple, écrire quelque chose dans un
fichier) qui doit être effectué lorsque le programme se termine.
Dans ce cas, où l'objet construit à la première utilisation (Fred dans ce cas) a éventuellement besoin d'être détruit, vous pouvez changer
la fonction x() comme suit :
x.cpp
#include "Fred.hpp"
Fred& x()
{
static Fred ans; // was static Fred* ans = new Fred();return ans; // was return *ans;
}
Cependant, il apparait (ou du moins, il peut apparaître) un problème relativement subtile avec ce changement. Pour comprendre ce
problème potentiel, il faut se souvenir pourquoi nous faisons cela : Nous avons besoin d'être sûr à 100 % que notre objet statique
est construit avant sa toute première utilisation
n'a pas besoin d'être détruit après sa dernière utilisation
Il est évident qu'il serait désatreux qu'un objet statique soit utilisé avant sa construction ou après sa destruction. L'idée ici est
que vous devez vous inquiéter de deux situations et non pas simplement d'une des deux.
En changeant la déclaration
static Fred* ans = new Fred();
en
static Fred ans;
nous gérons toujours correctement l'initialisation, mais nous ne gérons plus la libération. Par exemple, si il y a 3 objets statiques,
a, b et c, qui utilisent ans dans leur destructeur, la seule façon d'éviter un désastre à la libération et que ans soit
détruit après la destruction du dernier des 3 objets.
La situation est simple ; si il y a un autre objet statique dont le destructeur a besoin de ans après qu'il ait été détruit, vous
êtes mort. Si le constructeur de a,b et c utilisent ans, tout devrait être correct vu que le système d'exécution détruira
ans après que le dernier des 3 objets ait été détruit. Cependant, a et/ou b et/ou c n'arrive pas à utiliser ans dans leur
constructeur et/ou un bout de code quelque part prend l'adresse de ans et le passe à un autre objet statique, soyez très très
prudent.
Il y a une troisième approche qui gère aussi bien l'initialisation que la libération, mais elle a d'autres influences non triviales.
Mais je n'ai pas le courage de la décrire ici, je vous renvoie donc vers un bon livre de C++.
Simplement en utilisant la méthode décrite juste avant, mais cette fois, en utilisant une fonction membres statique plutôt qu'une fonction
globale.
Supposons que l'on ait une classe X qui a un objet Fred statique
x.hpp
class X {
public:
...
private:
static Fred x_;
};
Naturellement, le membre statique est initialisé séparément :
x.cpp
#include "X.hpp"
Fred X::x_;
L'objet Fred va être utilisé dans une ou plusieurs méthodes de X :
void X::someMethod()
{
x_.goBowling();
}
Maintenant, le scénario catastrophe se présentera si quelqu'un appelle cette méthode avant que l'objet Fred ne soit complètement construit.
Par exemple, si quelqu'un d'autre crée un objet X statique et invoque sa méthode someMethod() pendant l'initialisation statique,
nous voila à la merci du compilateur, suivant qu'il construise X::x_ avant ou après que someMethod() ait été appelée. (Il est à noter
que le comité ANSI/ISO C++ travaille sur ce point, mais les compilateurs actuels ne gèrent pas ce cas, ce sera probablement une mise à jour
future.)
Quoi qu'il en soit, il est portable et prudent de transformer la donnée membre statique X::x_ en une fonction membre statique
x.hpp
class X {
public:
...
private:
static Fred& x();
};
Naturellement, ce membre statique est initialisé séparément.
x.cpp
#include "X.hpp"
Fred& X::x()
{
static Fred* ans = new Fred();
return *ans;
}
Il suffit ensuite simplement de remplacer toutes les utilisations de x_ par c()
void X::someMethod()
{
x().goBowling();
}
Si les performances sont très importantes à vos yeux et que l'appel d'une fonction supplémentaire à chaque invocation de
X::someMethod() vous est insupportable, vous pouvez toujours prévoir un static Fred&. Comme les données statiques locales
ne sont jamais initialisées qu'une seule fois (la première fois que leur déclaration est rencontrée), X::x() n'est appelé qu'une fois,
lors du premier appel de X::someMethod().
void X::someMethod()
{
static Fred& x = X::x();
x.goBowling();
}
Note : le fiasco dans l'ordre d'initialisation des variables statiques peut aussi se produire, dans certains cas, avec les types de base.
Si vous initialisez les types de base en utilisant une fonction, le "fiasco dans l'ordre d'initialisation des variables statiques" peut
vous causer des problèmes aussi bien qu'avec des classes définies par vous.
Le code suivant expose le problème
#include <iostream>
int f(); // forward declarationint g(); // forward declarationint x = f();
int y = g();
int f()
{
std::cout << "using 'y' (which is " << y << ")\n";
return 3*y + 7;
}
int g()
{
std::cout << "initializing 'y'\n";
return 5;
}
La sortie de ce petit programme montre qu'il utilise y avant qu'il ait été initialisé. La solution, comme précédemment, et d'utiliser
l'idiome de la construction à la première utilisation.
#include <iostream>
int f(); // forward declarationint g(); // forward declarationint& x()
{
staticint ans = f();
return ans;
}
int& y()
{
staticint ans = g();
return ans;
}
int f()
{
std::cout << "using 'y' (which is " << y() << ")\n";
return 3*y() + 7;
}
int g()
{
std::cout << "initializing 'y'\n";
return 5;
}
il est possible de simplifier ce code en déplacant le code d'initialisation pour x et y dans leur fonction respective
De plus, étant donné que y est initialisé via une expression constante, elle n'a plus besoin de sa fonction d'enrobage (wrapper),
elle eut donc redevenir une simple variable.
C'est une méthode très utile pour exploiter le chaînage des méthodes.
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ètres 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 méthodes d'une nouvelle classe, dans laquelle toutes ces méthodes 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éé si il n'existe pas déjà ; si 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'inplémentaion, 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
(il ne sont pas positionnés) et qu'ils sont nommés. Le programmeur n'a donc pas besoin de se souvenir 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 méthodes (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 méthodes). 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);
// sets all the default values for each data member
OpenFile& readonly(); // changes readonly_ to true
OpenFile& createIfNotExist();
OpenFile& blockSize(unsigned nbytes);
...
private:
friend File;
bool readonly_; // defaults to false [for example]
...
unsigned blockSize_; // defaults to 4096 [for example]
...
};
La seule autre chose à faire est que le constructeur de File accepte un objet OpenFile.
class File {
public:
File(const OpenFile& params);
// vacuums the actual params out of the OpenFile object
...
};
A noter que OpenFile déclare File en tant que classe amie. De cette façon, OpenFile n'a pas besoin de définir un série d'accesseurs .
Etant donné que chaque fonction membre de la chaîne renvoit 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.
Ce document issu de http://www.developpez.com est soumis à la licence
GNU FDL traduit en français
ici.
Permission vous est donnée de distribuer, modifier des copies de cette page tant que cette note apparaît clairement.
Certaines parties de ce document sont sous copyright Marshall Cline