[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 constructeur.

[ Haut | Bas | Section précédente | Section suivante ]


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

Une grandediffé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 | Section précédente | Section suivante ]


[10.3] Comment est-ce que je peux inciter un constructeur à appeler un autre constructeur comme primitive?

Aucun moyen.

Que les dragons me viennent en aide : si vous appelez un autre constructeur, le compilateur initialise un objet local provisoire; il n'initialise pas l'objet this. Vous pouvez combiner les deux constructeurs en utilisant un paramètre par défaut, ou vous pouvez partager leur code commun dans une fonction membre privée init().

[ Haut | Bas | Section précédente | Section suivante ]


[10.4] Est 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 | Section précédente | Section suivante ]


[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 | Section précédente | Section suivante ]


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

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 | Section précédente | Section suivante ]


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

Voilaà 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&eacte;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 | Section précédente | Section suivante ]


[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 | Section précédente | Section suivante ]


[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 | Section précédente | Section suivante ]


[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 | Section précédente | Section suivante ]


[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 | Section précédente | Section suivante ]


[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 | Section précédente | Section suivante ]


[10.13] 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 | Section précédente | Section suivante ]


[10.14] 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 | Section précédente | Section suivante ]


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 le 12 Nov 2002