IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Sommaire > Les classes en C++ > Les constructeurs
        Qu'est-ce qu'un constructeur ?
        Y a-t-il une différence quelconque entre List x; et list x(); ?
        Un constructeur d'une classe peut-il appeler un autre constructeur de la même classe pour initialiser 'this' ?
        Est-ce que le constructeur par défaut pour Fred est toujours Fred::Fred() ?
        Quel constructeur est appelé quand je crée un tableau d'objets Fred ?
        Mes constructeurs doivent-ils utiliser les listes d'initialisation ou l'affectation ?
        Puis-je utiliser le pointeur this dans un constructeur ?
        Qu'est-ce que l'idiome du constructeur nommé (Named Constructor)?
        Pourquoi ne puis-je pas initialiser un membre statique dans la liste d’initialisation ?
        Pourquoi les classes avec des membres statiques me donnent-elles des erreurs lors de l'édition des liens ?
        Qu'est-ce que le "fiasco dans l'ordre d'initialisation des variables statiques" ?
        Comment puis-je éviter le "fiasco dans l'ordre d'initialisation des variables statiques" ?
        Pourquoi l'idiome de construction à la première utilisation utilise-t-il un pointeur statique plutôt qu'un object statique ?
        Comment puis-je éviter le "fiasco dans l'ordre d'initialisation des variables statiques" pour les données membres statiques ?
        Dois-je me préocuper du "fiasco dans l'ordre d'initialisation des variables statiques" pour les types de base ?
        Que faire en cas d'échec du constructeur ?
        Qu'est ce que "l'idiome des paramètres nommés" ?



Qu'est-ce qu'un constructeur ?
auteur : LFE
auteur : Gilles Louise
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 :

class MaClasse { // .... MaClasse(); // .... };
lien : Qu'est-ce qu'un destructeur ?

Y a-t-il une différence quelconque entre List x; et list x(); ?
auteur : Marshall Cline
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ée x (qui retourne une list) // ... }

Un constructeur d'une classe peut-il appeler un autre constructeur de la même classe pour initialiser 'this' ?
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++.

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) { ... }

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

Quel constructeur est appelé quand je crée un tableau d'objets Fred ?
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.

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.


Mes constructeurs doivent-ils utiliser les listes d'initialisation ou l'affectation ?
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 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.


Puis-je utiliser le pointeur this dans un constructeur ?
auteur : Marshall Cline
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.


Qu'est-ce que l'idiome du constructeur nommé (Named Constructor)?
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 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 rectangulaires static 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 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'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


Pourquoi ne puis-je pas initialiser un membre statique dans la liste d’initialisation ?
auteur : Marshall Cline
Parce que vous devez définir explicitement les membres statiques de votre classe.

Fred.h
class Fred { public: Fred(); // ... private: int i_; static int 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;

Pourquoi les classes avec des membres statiques me donnent-elles des erreurs lors de l'édition des liens ?
auteur : Marshall Cline
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: static int 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


Qu'est-ce que le "fiasco dans l'ordre d'initialisation des variables statiques" ?
auteur : Marshall Cline
Un moyen subtile de planter votre programme.

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.


Comment puis-je éviter le "fiasco dans l'ordre d'initialisation des variables statiques" ?
auteur : Marshall Cline
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 :

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 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() :

Barney.cpp
#include "Barney.hpp" Barney::Barney() { // ... x().goBowling(); // ... }
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.


Pourquoi l'idiome de construction à la première utilisation utilise-t-il un pointeur statique plutôt qu'un object statique ?
auteur : Marshall Cline
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++.


Comment puis-je éviter le "fiasco dans l'ordre d'initialisation des variables statiques" pour les données membres statiques ?
auteur : Marshall Cline
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.


Dois-je me préocuper du "fiasco dans l'ordre d'initialisation des variables statiques" pour les types de base ?
auteur : Marshall Cline
Oui.

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 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; }
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 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; }
il est possible de simplifier ce code en déplacant le code d'initialisation pour x et y dans leur fonction respective

#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; }
Et en supprimant les affichages, nous pouvons simplifier le code en quelque chose de très simple

int& y(); // forward declaration int& x() { static int ans = 3*y() + 7; return ans; } int& y() { static int ans = 5; return ans; }
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.


Que faire en cas d'échec du constructeur ?
auteur : Marshall Cline
Dans ce cas, le mieux est de lancer une exception.


Qu'est ce que "l'idiome des paramètres nommés" ?
auteur : Marshall Cline
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