[10] Constructeurs

(Une partie de C++ FAQ Lite fr, Copyright © 1991-2002, Marshall Cline, cline@parashift.com)

Traduit de l'anglais par Jérôme Lecomte

Les FAQs de la section [10]


[10.1] Quelle est le contrat avec les constructeurs?
Les constructeurs construisent les objet à partir de rien.

Les constructeurs sont une sorte de fonction "init". Ils transforment une pile de bits arbitraire en un objet vivant. Au minimum ils initialisent les champs de l'objet. Ils peuvent également allouer des ressources (mémoire, fichiers, sémaphores, sockets, etc..).

"ctor" est une abréviation typique pour le mot "constructeur".

[ Haut | Bas | Rechercher ]


[10.2] Y a-t-il une différence quelconque entre List x; et list x();?
Une grande différence!

Supposez que list soit le nom d'une certaine classe. Si la fonction f() déclare un objet local de type list et appelé x:

void f()
{
	list x; // objet local nommé x (de la classe list)
	// ...
}

La fonction g() en revanche déclare une fonction appelée x() qui retourne un objet de type list:

void g()
{
	list x(); // fonction nommé x (qui retourne une list)
	// ...
}

[ Haut | Bas | Rechercher ]


[10.3] Est-ce qu'un constructeur peut appeler un autre constructeur de la meme classe ?
Aucun moyen.

Prenons un exemple: Imaginons que vous vouliez que le contructeur Foo::Foo(char) appele le constructeur Foo:Foo(char, int) dans le but d'initialiser l'objet this. Malheureusement il n'y a pas de moyen de faire cela en C++.

Certains le font. Malheureusement cela ne fait pas ce qu'ils veulent. Par exemple, la ligne Foo(x,0); n'appelle pas Foo::Foo(char,int) sur this. Ce que cela fait c'est de creer un objet temporaire avec le constructeur Foo::Foo(char, int), mais cet objet est detruit immediatement apres le ';'.

class Foo {
public:
	Foo(char x);
	Foo(char x, int y);
	...
};

Foo::Foo(char x)
{
	...
	Foo(x, 0);  // this line does NOT help initialize the this object!!
	...
}
Vous pouvez combiner les deux constructeurs en utilisant un paramètre par défaut.
class Foo {
public:
	Foo(char x, int y=0);  // this line combines the two constructors
	...
};
Si cela ne vous conviens pas, par exemple il n'y a pas de valeur par defaut possible, vous pouvez partager leur code commun dans une fonction membre privée.
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)
{
	...
}

[ Haut | Bas | Rechercher ]


[10.4] Est ce que le constructeur par défaut pour Fred toujours Fred::Fred()?
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
	// ...
};

[ Haut | Bas | Rechercher ]


[10.5] Quel constructeur est appelé quand je crée un tableau d'objets Fred?
Le constructeur par défautFred.

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);
/ / ... supposent qu' il n'y a aucun constructeur de défaut dedans classe Fred ...
};

main()
{
Fred a[10 ]; / / ERREUR: Fred n'a pas un constructeur par défaut
Fred * p = new Fred[10 ]; / / ERREUR: Fred n'a pas un constructeur de défaut
}

Cependant si vous créez un vector<Fred STL 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
using namespace std;

main()
{
vector<Fred a(10, Fred(5,7));
/ / 10 Fred objets dans le vecteur a seront initialisé avec Fred(5,7).
/ / ...
}

[ Haut | Bas | Rechercher ]


[10.6] Mes constructeurs doivent-t-ils utiliser les listes d'initialisation ou l'affectation?
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_(quoiquecesoit) { }. D'un point de vue exécution, il est important de noter que l'expression quoiquecesoit ne générera pas nécessairement la création d'un objet séparé et sa copie dans x_: si les types sont identiques le résultat de ... quoiquecesoit... sera construit directement à dans x_.

En revanche le constructeur suivant utilise l'affectation: Fred::Fred() { x _ = quoiquecesoit; }. Dans ce cas-ci l'expression quoiquecesoit contraint la création d'un objet séparé et provisoire, passé ensuite a l'opérateur d'assignation de x_, avant d'être détruit au ;. C'est inefficace.

Il y a encore une autre source d'inefficacité : dans le deuxième cas (l'affectation), le constructeur par défaut de l'objet (implicitement appelé devant le corps du constructeur "{") pourrait, par exemple, assigner une quantité de mémoire par défaut ou ouvrir un fichier par défaut. Tout ce travail pourrait être pour rien si l'expression quoiquecesoit et/ou l'opérateur d'affectation donnait lieu a la fermeture du fichier et/ou la libération de cette mémoire (par exemple, si le constructeur par défaut n'assignait pas un bloc de mémoire assez grand ou s' il ouvrait le mauvais fichier).

Conclusion: toutes choses égales par ailleurs, votre code tournera plus vite si vous utilisez les listes d'initialisation plutôt que l'assignation.

_PAR([[ 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'initialization plutô que par affectation par soucis de consitence. Un autre argument lié à a la symetrie: la valeur des membres de données const et non statiques ne peuvent pas être modifiées dans le constructeur, donc pour conserver la symetrie, je recommende d'initialiser tout dans la list d'initialisation.

[ Haut | Bas | Rechercher ]


[10.7] Puis-je utiliser le pointeur this dans un constructeur ?

Certains pensent qu'on ne devrait pas utiliser le pointeur this dans un constructeur parce que l'objet n'est pas completement formé. Pourtant il possible d'utiliser le pointeur this dans le corp du constructeur et même dans la liste d'initializaton si on est prudent.

Voilà quelque chose qui fonctionne toujours : le (corps du) constructeur (ou une fonction appelée depuis le constructeur) peuvent acceder 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 fiabilité. C'est parce que tous ces membres de donnée sont garantis avoir été completement construits au moment ou le (corps du) constructeur commence à être executer.

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'executer 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 (e.g. this->method(), ou implicitement sans utiliser le pointeur this (e.g.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'appeleur 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. Vous êtes prevenus.

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 (e.g. 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é! Et si vous connaissez ces règles, s'il vous plait faites attention.

[ Haut | Bas | Rechercher ]


[10.8] Qu'est-ce que l'idiom du constructeur nommé (Named Constructor)?
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 voie de différencier entre les divers constructeurs d'une classe se fait via la liste de paramètres. Mais s' il y a beaucoup de constructeurs, les différences entre les constructeurs devient quelque peu subtile et sujette a erreur.

Avec l'idiom 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 "constructors nommé". 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çon d'indiquer une coordonnée dans un espace bi-dimensionel : 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ésenatant un point n'importent pas; 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 ont 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); // Coordinné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'idiom du constructeur nommé:

#include <math.h // Pour avoir sin() et cos()

class Point {
public:
static Point rectangular(float x, float y); // Coords rectangulaires
static Point polar(float radius, float angle); // Coords polaires
// Ces méthodes static sont les "constructeurs només"
// ...
private:
Point(float x, float y); // coordonnées rectangulaires
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:

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'idiom du constructeur nommé peut aussi être utilisé pour vous assurer que les objets d'une classe sont toujours créés avec new .

[ Haut | Bas | Rechercher ]


[10.9] Pourquoi ne puis-je pas initialiser mon membre static dans la liste d'initialisation de mon constructeur?
Parce que vous devez explicitementdéfinir les membres staticde votre classe.

Fred.h:

class Fred {
public:
Fred();
// ...
private:
int i_;
static int j_;
};

Fred.cpp (ou Fred.C ou autre ):

Fred::Fred()
: i_(10) // OK: vous pouvez (et vous devriez) initialiser les données membre 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 static de cette façon:
int Fred::j_ = 42;

[ Haut | Bas | Rechercher ]


[10.10] Pourquoi les classes avec des membres static me donnent des erreurs au moment du link?
Parce que les données membres static 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 le générateur de liens (linker). Par exemple :

// Fred.h

class Fred {
public:
// ...
private:
static int j_; // Fred::j_ : donnée membre declaré static
// ...
};

Le générateur de lien 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_ = quelque_expression_evaluant_un_int;

// Alternativement, si vous désirez utiliser la valeur par défaut 0 pour les ints 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 (ou Fred.C ou l'extension de fichier que vous utilisez).

[ Haut | Bas | Rechercher ]


[10.11] Qu'est-ce que le "fiasco dans l'ordre d'initialisation des variables static"?
Un moyen subtile de tuer votre projet.

Le fiasco de l'ordre d'initialisation des static est un moyen subtile et un aspect habituellement mal compris du C++. Malheureusement il est très difficile à détecter -- les erreurs se manifeste avant que le main() commence.

En bref, supposez que vous avez deux objets static x et y qui sont définis dans deux fichiers sources séparés, disons x.cpp et y.cpp. Supposez maintenant que le constructeur de l'objet y appelle une méthode de l'objet x.

Voilà. C'est aussi simple que ça.

La tragédie est que vous avez 50%-50% de chances de mourir. Si 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 cuît. 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 prochaîne FAQ .

Note: Le fiasco de l'ordre d'initialisation des static ne s'applique pas au types de données prédefinis/intrinsèques comme int ou char*. Par exemple si vous créez un objet staticfloat, il n'y a jamais de problèmes avec l'ordre d'initializarion. Les seules fois où l'ordre d'initialisation statique est vraiment un fiasco est lorsque vos objets globaux ou static ont un constructeur.

[ Haut | Bas | Rechercher ]


[10.12] Comment j'empêche le "fiasco dans l'ordre d'initialisation des variables static"?
Utilisez l'idiom de "construction à la première utilisation", qui consiste simplement à emballer (wrap) vos objets staticà l'interieur 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éfinie l'objet x :

// Fichier x.cpp
#include "Fred.hpp"
Fred x;

Le fichier y.cpp définie l'objet y:

// Fichier y.cpp
#include "Barney.hpp"
Barney y;

Pour être complet, le constructeur de Barney pourraît ressembler à quelque chose comme :

// Fichier Barney.cpp
#include "Barney.hpp"

Barney::Barney()
{
// ...
x.goBowling();
// ...
}

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 completement portable est de remplacer l'objet (de type Fred) global x, par une fonction globale x(), qui retourne par réference l'objet Fred.

// File x.cpp

#include "Fred.hpp"

Fred& x()
{
static Fred* ans = new Fred();
return *ans;
}

Puisque les objet locaux static sont construits la première fois (et seulement la première fois) que le flux de contrôle passe sur la déclaration, l'instruction ci-dessus new Fred() sera non seulement executé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():

// Fichier Barney.cpp
#include "Barney.hpp"

Barney::Barney()
{
// ...
x().goBowling();
// ...
}

Le nom est Construction à la première utilisation car cela fait exactement ce que ça dit : l'objet global Fred est construit à sa première utilisation.

Le défaut de cette approche est que l'objet Fred n'est jamais detruit. Le livre C++ FAQ contient une seconde technique qui solutionne ce souscis (mais au risque de générer un fiasco dans l'ordre de de-initialisation des variables statiques").

Notez que vous avez pas besoin de faire ça pour les types intrinsèques/prédefinis commeint ou char*. Par exemple si vous créez un staticfloat ou un float global, il n'y a pas besoin de l'emballer dans une fonction. La seule fois où l'initialisation statique peut vraiment tourner au fiasco est lorsque vos objets static ou globaux ont un constructeur.

[ Haut | Bas | Rechercher ]


[10.13] Why doesn't the construct-on-first-use idiom use a static object instead of a static pointer?

Short answer: it's possible to use a static object rather than a static pointer, but doing so opens up another (equally subtle, equally nasty) problem.

Long answer: sometimes people worry about the fact that the previous solution "leaks." In many cases, this is not a problem, but it is a problem in some cases. Note: even though the object pointed to by ans in the previous FAQ is never deleted, the memory doesn't actually "leak" when the program exits since the operating system automatically reclaims all the memory in a program's heap when that program exits. In other words, the only time you'd need to worry about this is when the destructor for the Fred object performs some important action (such as writing something to a file) that must occur sometime while the program is exiting.

In those cases where the construct-on-first-use object (the Fred, in this case) needs to eventually get destructed, you might consider changing function x() as follows:

// File x.cpp
#include "Fred.hpp"

Fred& x()
{
	static Fred ans;  // was static Fred* ans = new Fred();
	return ans;       // was return *ans;
}

However there is (or rather, may be) a rather subtle problem with this change. To understand this potential problem, let's remember why we're doing all this in the first place: we need to make 100% sure our static object (a) gets constructed prior to its first use and (b) doesn't get destructed until after its last use. Obviously it would be a disaster if any static object got used either before construction or after destruction. The message here is that you need to worry about two situations (static initialization and static deinitialization), not just one.

By changing the declaration from static Fred* ans = new Fred(); to static Fred ans;, we still correctly handle the initialization situation but we no longer handle the deinitialization situation. For example, if there are 3 static objects, say a, b and c, that use ans during their destructors, the only way to avoid a static deinitialization disaster is if ans is destructed after all three.

The point is simple: if there are any other static objects whose destructors might use ans after ans is destructed, bang, you're dead. If the constructors of a, b and c use ans, you should normally be okay since the runtime system will, during static deinitialization, destruct ans after the last of those three objects is destructed. However if a and/or b and/or c fail to use ans in their constructors and/or if any code anywhere gets the address of ans and hands it to some other static object, all bets are off and you have to be very, very careful.

There is a third approach that handles both the static initialization and static deinitialization situations, but it has other non-trivial costs. I'm too lazy (and busy!) to write any more FAQs today so if you're interested in that third approach, you'll have to buy a book that describes that third approach in detail. The C++ FAQs book is one of those books, and it also gives the cost/benefit analysis to decide if/when that third approach should be used.

[ Haut | Bas | Rechercher ]


[10.14] Comment j'empêche le "fiasco dans l'ordre d'initialisation des variables static" pour mes membres de donnée static?
Utilisez simplement la technique décrite ci-dessus , mais cette fois utilisez une fonction membre staticplutôt qu'une fonction globale..

Supposez que vous avez une classe X possedant un objet staticFred :

// Fichier X.hpp

class X {
public:
// ...

private:
static Fred x_;
};

Naturellement ce membre static est initialisé séparement :

// Fichier X.cpp

#include "X.hpp"

Fred X::x_;

Naturellement aussi l'objet Fred sera utilisé dans une ou plusieurs des méthodes de X:

void X::someMethod()
{
x_.goBowling();
}

Mais maintenant le "scenario catastrophe" est que quelqu'un, quelque part, appelle de quelque manière cette méthode avant que l'objet Fred soit construit. Par exemple, si quelqu'un crée un objet static X et invoque la méthode someMethod() pendant l'initialisation static, alors vous êtes à la merci du compilateur : c'est à dire si le compilateur construira X::x_ avant ou après que someMethod() soit appelé. (Notez que le comité ANSI/ISO C++ travaille sur ce problème, mais que les compilateurs ne sont pas en général n'implantent pas ces changements; surveillez cet espace pour une mise-à-jour dans le futur.)

Dans tous les cas, c'est toujours portable et sûre de modifier le membre de donnée X::x_static en une fonction membre static:

// Fichier X.hpp

class X {
public:
// ...

private:
static Fred& x();
};

Naturellement ce membre static est initialisé séparement:

// Fichier X.cpp

#include "X.hpp"

Fred& X::x()
{
static Fred* ans = new Fred();
return *ans;
}

Il ne reste plus qu'a remplacer x_ par x():

void X::someMethod()
{
x().goBowling();
}

Si vous êtes super sensible à la performance de votre programme et que vous êtes souieux de délai introduit par un appel de fonction suplémentaire à chaque invoquation de X::someMethod() vous pouvez mettre un static Fred& à la place. Comme vous vous en souvenez, les variables locales static sont seulement initialisées une fois (la première fois que le flux de contrôle passe sur la déclaration), ceci appelera donc X::x() une fois seulement au premier appel de X::someMethod() :

void X::someMethod()
{
static Fred& x = X::x();
x.goBowling();
}

Notez que vous avez pas besoin de faire ça pour les types intrinsèques/prédefinis commeint ou char*. Par exemple si vous créez un staticfloat ou un float global, il n'y a pas besoin de l'emballer dans une fonction. La seule fois où l'initialisation statique peut vraiment tourner au fiasco est lorsque vos objets static ou globaux ont un constructeur.

[ Haut | Bas | Rechercher ]


[10.15] Do I need to worry about the "static initialization order fiasco" for variables of built-in/intrinsic types?

Yes.

If you initialize your built-in/intrinsic type using a function call, the static initialization order fiasco is able to kill you just as bad as with user-defined/class types. For example, the following code shows the failure:

#include <iostream>

int f();  // forward declaration
int g();  // forward declaration

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

The output of this little program will show that it uses y before initializing it. The solution, as before, is the Construct On First Use Idiom:

#include <iostream>

int f();  // forward declaration
int g();  // forward declaration

int& x()
{
	static int ans = f();
	return ans;
}

int& y()
{
	static int 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;
}

Of course you might be able to simplify this by moving the initialization code for x and y into their respective functions:

#include <iostream>

int& y();  // forward declaration

int& x()
{
   static int ans;

   static bool firstTime = true;
   if (firstTime) {
     firstTime = false;
     std::cout << "using 'y' (which is " << y() << ")\n";
     ans = 3*y() + 7;
   }

   return ans;
 }

int&y()
{
	static int ans;

	static bool firstTime = true;
	if (firstTime) {
		firstTime = false;
		std::cout << "initializing 'y'\n";
		ans = 5;
	}

	return ans;
 }

And, if you can get rid of the print statements you can further simplify these to something really simple:

int&y();  // forward declaration

int&x()
{
	static int ans = 3*y() + 7;
	return ans;
}

int& y()
{
	static int ans = 5;
	return ans;
}

Furthermore, since y is initialized using a constant expression, it no longer needs its wrapper function -- it can be a simple variable again.

[ Haut | Bas | Rechercher ]


[10.16] Comment est-ce que je dois réagir à un constructeur qui échoue?

Lancer (throw) une exception. Voir [17.1] pour les détails.

[ Haut | Bas | Rechercher ]


[10.17] What is the "Named Parameter Idiom"?

It's a fairly useful way to exploit method chaining .

The fundamental problem solved by the Named Parameter Idiom is that C++ only supports positional parameters. For example, a caller of a function isn't allowed to say, "Here's the value for formal parameter xyz, and this other thing is the value for formal parameter pqr." All you can do in C++ (and C and Java) is say, "Here's the first parameter, here's the second parameter, etc." The alternative, called named parameters and implemented in the language Ada, is especially useful if a function takes a large number of mostly default-able parameters.

Over the years people have cooked up lots of workarounds for the lack of named parameters in C and C++. One of these involves burying the parameter values in a string parameter then parsing this string at run-time. This is what's done in the second parameter of fopen(), for example. Another workaround is to combine all the boolean parameters in a bit-map, then the caller or's a bunch of bit-shifted constants together to produce the actual parameter. This is what's done in the second parameter of open(), for example. These approaches work, but the following technique produces caller-code that's more obvious, easier to write, easier to read, and is generally more elegant.

The idea, called the Named Parameter Idiom, is to change the function's parameters to methods of a newly created class, where all these methods return *this by reference. Then you simply rename the main function into a parameterless "do-it" method on that class.

We'll work an example to make the previous paragraph easier to understand.

The example will be for the "open a file" concept. Let's say that concept logically requires a parameter for the file's name, and optionally allows parameters for whether the file should be opened read-only vs. read-write vs. write-only, whether or not the file should be created if it doesn't already exist, whether the writing location should be at the end ("append") or the beginning ("overwrite"), the block-size if the file is to be created, whether the I/O is buffered or non-buffered, the buffer-size, whether it is to be shared vs. exclusive access, and probably a few others. If we implemented this concept using a normal function with positional parameters, the caller code would be very difficult to read: there'd be as many as 8 positional parameters, and the caller would probably make a lot of mistakes. So instead we use the Named Parameter Idiom.

Before we go through the implementation, here's what the caller code might look like, assuming you are willing to accept all the function's default parameters:

	File f = OpenFile("foo.txt");

That's the easy case. Now here's what it might look like if you want to change a bunch of the parameters.

 File f = OpenFile("foo.txt").
            readonly().
            createIfNotExist().
            appendWhenWriting().
            blockSize(1024).
            unbuffered().
            exclusiveAccess();

Notice how the "parameters", if it's fair to call them that, are in random order (they're not positional) and they all have names. So the programmer doesn't have to remember the order of the parameters, and the names are (hopefully) obvious.

So here's how to implement it: first we create a new class (OpenFile) that houses all the parameter values as private data members. Then all the methods ( readonly(), blockSize(unsigned), etc.) return *this (that is, they return a reference to the OpenFile object, allowing the method calls to be chained . Finally we make the required parameter (the file's name, in this case) into a normal, positional, parameter on OpenFile's constructor.

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]
	...
};

The only other thing to do is make the constructor for class File to take an OpenFile object:

class File {
public:
	File(const OpenFile& params);
	 // vacuums the actual params out of the OpenFile object

	...
};

Note that OpenFile declares File as its friend , that way OpenFile doesn't need a bunch of (otherwise useless) public: get methods .

Since each member function in the chain returns a reference, there is no copying of objects and the chain is highly efficient. Furthermore, if the various member functions are inline, the generated object code will probably be on par with C-style code that sets various members of a struct. Of course if the member functions are not inline, there may be a slight increase in code size and a slight decrease in performance (but only if the construction occurs on the critical path of a CPU-bound program; this is a can of worms I'll try to avoid opening; read the C++ FAQs book for a rather thorough discussion of the issues), so it may, in this case, be a tradeoff for making the code more reliable.

[ Haut | Bas | Rechercher ]


E-mail Marshall Cline Ecrire à l'auteur, au traducteur, ou en savoir plus sur la traduction.
C++ FAQ Lite fr | Table des matières | Index | A propos de l'auteur | © | Téléchargez votre propre copie ]
Dernière révision Sun Apr 13 23:54:12 PDT 2003