IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

FAQ C++ moderne et erreurs courantes

Ce ou ces sujets font partie d'une FAQ en cours d'écriture, vos retours sont les bienvenus.

La liste d'initialisation est l'endroit où le constructeur crée tous ses membres ainsi que (éventuellement) la classe de base dont la classe hérite directement. Elle se situe avant l'accolade ouvrante qui contient le code du constructeur :

.hpp
Sélectionnez 
class MaClasse {
public:
    MaClasse(/* éventuels paramètres */)     
    /* ICI */
    {
        // PAS LÀ
    }
};
ou dans le .cpp :
.cpp
Sélectionnez 
MaClasse::MaClasse(/* éventuels paramètres */)
/* ICI */
{
    // PAS LÀ
}

La liste d'initialisation débute par deux points : et sépare chaque initialisation par une virgule ,. Chaque initialisation étant de la forme suivante (avec des parenthèses, ou des accolades depuis C++11 permettant de nouvelles formes d'initialisation) et construisant chaque membre dans le même ordre que leur déclaration :

 
Sélectionnez
ClasseDeBase(/* ses paramètres constructeurs */)
 
Sélectionnez
variable_membre(/* ses paramètres constructeurs / sa valeur initiale */)

Si l'on a par exemple une classe de base avec le constructeur ClasseDeBase(int a, int b) nous pourrons l'initialiser ainsi :

 
Sélectionnez
ClasseDeBase(1, 2)

Si l'on a par exemple un membre int value_;, nous l'initialiserons ainsi :

 
Sélectionnez
value_(42)

Si l'on a par exemple un membre std::vector<int> vec_; que l'on souhaite initialiser avec 10 fois la valeur 5, nous aurons :

 
Sélectionnez
vec_(10, 5)

ou avec les valeurs {1,2,3,4,5} :

 
Sélectionnez
vec_{1,2,3,4,5}

Le tout cumulé nous donnera :

 
Sélectionnez
class MaClasse : public ClasseDeBase {
    int value_;
    std::vector<int> vec_;
 
public:
    MaClasse(/* éventuels paramètres */)     
      : ClasseDeBase(1, 2)
      , value_(42)
      , vec_{1,2,3,4,5}
    {
        // PAS LÀ
    }
};
ou dans le .cpp :
 
Sélectionnez
MaClasse::MaClasse(/* éventuels paramètres */)
  : ClasseDeBase(1, 2)
  , value_(42)
  , vec_{1,2,3,4,5}
{
    // PAS LÀ
}

La liste d'initialisation est le seul endroit où vous pouvez appeler les constructeurs de vos membres, en leur fournissant les bons paramètres. Ceci est d'autant plus vrai pour les éléments…

  • non copiables/déplaçables : dans le corps du constructeur, l'instruction membre = sa_valeur; fera appel à l'operator = qui sera delete ;
  • qui n'ont pas de constructeurs par défaut, car non mentionnés dans cette liste, la default initialization sera appliquée s'ils n'ont pas été initialisés à la déclaration.

Comme vous pourrez le voir dans les démonstrations qui suivent, si vous tentez d'initialiser des éléments du 1., 2. ou 4. dans le corps {} du constructeur, il est déjà trop tard et vous aurez une erreur de compilation.

  1. les références : elles doivent obligatoirement désigner un objet dès leur construction, cf. '[…]' declared as reference but not initialized.

  2. les classes de base et membres ne possédant pas de constructeur par défaut : vous êtes obligé de leur passer des paramètres :

  3. les types primitifs, pointeurs nus et tableaux C : non initialisés, vous aurez un UB dès que vous voudrez lire leurs valeurs / accéder à l'adresse pointée :

  4. les classes non copiables/déplaçables, en particulier les classes ayant une sémantique d'entité, (auquel s'ajoute éventuellement le 2.) : vous ne pourrez pas faire appel à l'opérateur d'affectation par copie et/ou déplacement dans le corps du constructeur :

Passons maintenant aux erreurs courantes (se basant sur l'exemple complet qui suit) :

Code erroné Explications
Correction
 
Sélectionnez
Derivee(/* … */)
{
    /* … */
}

Non mentionnés dans la liste d'initialisation,

  • Base : c'est le constructeur par défaut de la classe de Base qui sera appelé. Et si comme ici vous n'en avez pas. vous aurez une erreur de compilation (« no matching function for call to 'Base::Base()' ») vous indiquant que le compilateur ne l'a pas trouvé ;
  • vec_ et str_ appelleront leur constructeur par défaut (tableau et chaîne vides) ;
  • i_ non initialisé, sa lecture entraînera un UB.

Retenez que :

  • la liste d'initialisation est l'endroit où sont construits les membres et classe de base, c'est déjà trop tard dans le corps {} du constructeur ;
  • s'il ne sont pas initialisés dans la liste d'initialisation, ils sont initialisés par défaut (default initialization), ce qui produira une erreur de compilation pour :

    • des membres et classe de base qui n'ont pas de constructeur par défaut,
    • des références,

    et un UB à la lecture :

    • des types primitifs (int, double, …),
    • des pointeurs nus,
    • des tableaux C.
 
Sélectionnez
Derivee(/* … */)
  : Base(/* paramètres à passer au constructeur */)
  // éventuelle initialisation de vec_ et str_ si vous ne les voulez pas vides
  , i_(/* valeur initiale */)
{ }
 
Sélectionnez
Derivee(/* … */)
{ 
    Base(1, 2);
}

ou idem avec un constructeur délégué :

 
Sélectionnez
Derivee(/* … */)
{ 
    Derivee(/* … */);
}

Vous remarquez déjà que vous avez commis l'erreur qui précède, il n'y a rien dans la liste d'initialisation.

Que fait exactement cette ligne Base(1, 2); ? Elle crée un objet temporaire non nommé de type Base (indépendant de l'instance courante) qui sera immédiatement détruit passé cette ligne. Ce code est équivalent à écrire :

 
Sélectionnez
Derivee(/* … */)
{ 
    {
        Base temp(1, 2);
    } // détruit ici
}

En aucun cas, elle n'initialise quoique ce soit de la classe de base de l'instance qui est construite.

 
Sélectionnez
Derivee(/* … */)
  : Base(1, 2)
{ }

Il en va de même avec le constructeur délégué.

 
Sélectionnez
Derivee(/* … */)
/* … */
{ 
    str_ = "Hello World!";
}

Au lieu d'être simplement initialisé à sa construction, str_ est construit par défaut dans la liste d'initialisation, puis modifié pour contenir la chaîne "Hello World!".

Cette manipulation reste faisable avec les types copiables, mais elle peut engendrer un coût inutile.
Par contre elle est (en plus des points du premier élément) impossible pour les classes non copiables/movables. Car, oui dans le corps du constructeur, c'est une affectation (ou copie) que vous ferez (operator =), et non plus une construction.

 
Sélectionnez
Derivee(/* … */)
  /* … */
  , str_("Hello World!")
{ }
 
Sélectionnez
Derivee(/* … */)
/* … */
{ 
    vec_(10);
    i_(42);
}

Vous faites ici un appel comme si vos membres i_ et vec_ étaient des fonctions ou possédaient un membre operator()(int). N'étant pas des fonctions et ne possédant pas de tel opérateur, vous avez alors une erreur de compilation :

Message d'erreur GCC
Cacher/Afficher le codeSélectionnez
Message d'erreur Clang
Cacher/Afficher le codeSélectionnez
Message d'erreur VS français
Cacher/Afficher le codeSélectionnez
Message d'erreur VS anglais
Cacher/Afficher le codeSélectionnez

Et ceci est extrêmement sujet à erreur si votre membre possède effectivement un tel opérateur : vous construisez votre membre par défaut et appelez cet opérateur, et ceci sans aucune erreur de compilation !

 
Sélectionnez
Derivee(/* … */)
  /* … */
  , vec_(10) // initialisé avec 10 éléments ayant pour valeur 0
  , i_(42)   // initialisé à la valeur 42
{ }
 
Sélectionnez
Derivee(/* … */)
  /* … */
{ 
    int i_(42);
}

Ceci déclare une nouvelle variable locale indépendante qui porte le même nom que votre membre, mais n'a aucun lien avec. Sa modification n'entraînera pas celle de votre membre. Étant déclarée dans un scope plus proche que votre membre, c'est elle qui sera utilisée (et initialisée) à la place tout au long du constructeur, puis détruite lorsqu'on en sortira. Votre membre, lui n'aura pas été initialisé. (Cf. ce sujet[Erreur courante] Membres de classe vs variables locales.)

 
Sélectionnez
Derivee(/* … */)
  /* … */
  , i_(42)   // initialisé à la valeur 42. Cf. suivant, notez l'absence du type devant
{ }
 
Sélectionnez
Derivee(/* … */)
/* … */
int i_(42);
{ }

Vous ne devez pas reporter le type int, seul le nom de la variable i_ doit apparaître :

 
Sélectionnez
Derivee(/* … */)
  /* … */
  , i_(42)
{ }

Où puis-je utiliser la liste d'initialisation ?

Une liste d'initialisation peut seulement être utilisée dans les constructeurs de classe ou de structure, à savoir :

  • le constructeur par défaut ;
  • le constructeur par copie ;
  • le constructeur par déplacement (move) ;
  • n'importe quel autre constructeur que vous définirez.

Par contre, elle ne peut en aucun cas être utilisée ailleurs dans un opérateur ou une fonction membre classique, et encore moins dans une fonction libre.

L'ordre d'initialisation des membres est-il important ?

Quelque soit l'ordre dans lequel vous initialiserez vos membres et classe de base au sein de la liste d'initialisation, le compilateur initialisera d'abord la classe de base, puis les membres dans l'ordre dans lequel vous les avez déclarés. Dans la plupart des cas, cela n'aura pas d'importance, mais dans certains cas comme dans le suivant, cela pourra induire en erreur de les initialiser dans le désordre :

 
Sélectionnez
class MaClasse {
    int premier; // déclaré en premier
    int second;
 
public:
    MaClasse(int i) : second(i++), premier(i++) { }
};
 
Sélectionnez
MaClasse mc(0); // premier = 0 ; second = 1 et non l'inverse !

Si tel est le cas, votre compilateur (avec les bonnes options) vous préviendra avec le message :

 
Sélectionnez
warning: 'MaClasse::second' will be initialized after
[…]
warning: 'MaClasse::premier' [-Wreorder]
[…]
warning: when initialized here
[…]
Mis à jour le 2 juillet 2019  par Winjerome

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2021 Winjerome. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.