C++ : les différentes initialisations

Rédigé par Nicolas K Aucun commentaire

En C++, différentes syntaxes permettent d’initialiser ses variables. Dans ce tuto, nous allons voir laquelle privilégier et pourquoi.

Plusieurs syntaxes permettent d’initialiser une variable :

type nom; // (1)
type nom(valeur); // (2)
type nom = valeur; // (3)
type nom{valeur}; // (4)
type nom = {valeur}; // (5)

Voyons quels sont les différences entre ces syntaxes. Pour cela, il faut faire la distinction entre les types fondamentaux et les classes.

Pour un type fondamental (int, double, …)

  • (1) n’initialise pas la variable (la valeur de nom est indéfinie)
  • (2) et (3) initialisent la variable avec la valeur convertie en le type donné même s’il y a perte de données. Exemple :
    int milleMilliards(1'000'000'000'000); // (2) cette valeur est un long, le compilateur la convertit en int : -727379968
    int milleMilliards2 = 1'000'000'000'000; // (3) idem
  • (4) initialise la variable avec la valeur. Si la valeur ne peut pas être convertie sans perte de données, provoque une erreur de compilation.
    int milleMilliards{1'000'000'000'000}; // (4) cette valeur est un long, le compilateur envoit une erreur.
  • (5) est équivalent à (4), cette syntaxe n’a pas d’intérêt puisque plus verbeuse.

    Pour un type fondamental, il est donc préférable de toujours utiliser la syntaxe (4)

Pour une classe

Dans le cas où le type est une classe, c’est un peu plus compliqué. Pour plus de simplicité, prenons donc un exemple simple :

#include <iostream>
class A {
    public:
    A() {
        std::cerr << "défaut" << std::endl;
    }
    A(A const& a) : a{a.a} {
        std::cerr << "copie" << std::endl;
    }
    A(int a) : a{a} {
        std::cerr << "conversion" << std::endl;
    }
    private:
    int a{};
};

Les initialisations ont avec cette classe les comportements suivants :

  • (1) appelle le constructeur par défaut (constructeur sans paramètre)
  • Concernant (2), le résultat va dépendre évidemment de la valeur :
    int main() {
        A a;
        A a2(a); // (2a) copie
        A a3(4); // (2b) conversion
        A a4(1'000'000'000'000); // (2c) conversion
        //A a4("Test"); // (2d) erreur de compilation
    }

    Juste une remarque pour (2c) : 1'000'000'000'000 peut être converti implicitement en int (avec perte d’information), ce qui explique que ça compile et que ça appelle le constructeur de conversion.

    Attention : dans certains cas, cette notation ne va pas déclarer une variable mais déclarer une fonction… C’est le cas notamment lorsqu’on ne donne pas de valeur comme dans l’exemple ci-dessous, mais ça peut également arriver dans des cas plus complexes.

    A a(); // n’appelle pas le constructeur par défaut mais déclare une fonction a sans paramètre ! 
  • Concernant (3), le résultat sera le même que (2) :
    int main() {
        A a;
        A a2 = a; // (2a) copie
        A a3 = 4; // (2b) conversion puis copie
        A a4 = 1'000'000'000'000; // (2c) conversion puis copie
        //A a4 = "Test"; // (2d) erreur de compilation
    }

    Remarque : pour les cas (3b) et (3c), il y a théoriquement deux étapes :

    • d’abord une construction par conversion qui va construire un objet temporaire de type A
    • puis une copie de cet objet temporaire dans la variable a2 (ou a3 respectivement).

    Les compilateurs optimisent par défaut ce comportement par ce qu’on appelle la copy-elision. Seul le constructeur de conversion est donc appelé sauf si vous interdisez cette optimisation dans les paramètres de compilation. Cependant, puisqu’une copie (ou un déplacement) est censée être effectué, le constructeur de copie (ou de déplacement) doit exister pour pouvoir utiliser cette syntaxe.

  • Concernant (4), voici les comportements obtenus :
    int main() {
        A a;
        A a2{a}; // (4a) copie
        A a3{4}; // (4b) conversion
        //A a4{1'000'000'000'000}; // (4c) erreur de compilation
        //A a4("Test"); // (4d) erreur de compilation
    }

    Comme vous pouvez le constater, le cas (4c) ne fonctionne pas ! En d’autres termes, avec l’initialisation (4), on ne peut pas avoir de conversion implicite, donc pas de perte de donnée implicite. Plutôt une bonne chose pour ne pas avoir de comportement inattendu non ?

  • Dernière syntaxe : la syntaxe (5). Comme sa prédécesseuse, elle n’autorise pas les pertes de données implicites. Mais attention : comme la syntaxe (3), pour fonctionner, la classe doit avoir un constructeur de copie et/ou de déplacement pour que ça compile car il y a en théorie de la même façon, une construction par conversion puis une par copie.

Derniers mots

Vous l’aurez compris, pour éviter les mauvaises surprises, il faut donc privilégier la syntaxe avec les accolades (appelée initialisation uniforme).

Attention toutefois, si vous souhaitez construire par exemple un std::vector à 5 valeurs, il faut utiliser les parenthèses :

std::vector<int> monVector(5); // monVector peut contenir 5 valeurs

En effet, le code précédent n’est pas équivalent au code suivant (avec accolades) :

std::vector<int> monVector{5}; // monVector contient 1 valeur : 5

Pourquoi ? Il y a une subtilité à connaître :

S’il existe un constructeur par liste d’initialisation (c’est à dire avec comme paramètre une std::initializer_list), il est prioritaire face aux autres constructeurs compatibles.

Ainsi, dans notre exemple, l’initialisation uniforme d’un std::vector fait appel au constructeur par liste d’initialisation si les éléments de la liste sont du type du vector. En d’autres termes, les éléments entre accolades sont considérés comme étant les éléments à insérer dans le vector s’ils sont du bon type. Cela permet donc d’initialiser un std::vector avec en ensemble d’éléments :

std::vector<int> monVector{1,2,3};

Et ce n’est pas fini ! Cette initialisation uniforme a d’autres avantages :

Autres avantages de cette initialisation uniforme :

  • avec cette syntaxe, contrairement à la syntaxe avec parenthèses (2), on peut également initialiser à la valeur par défaut :
    int valeur{}; // valeur vaut 0
    A a{}; // appel du constructeur par défaut
  • et enfin, cette syntaxe peut être utilisée partout :
    • pour initialiser un attribut (ce qui n’est pas possible avec la syntaxe (2) :
      
      class B {
          private:
          int b{7}; // b vaut 7 par défaut
      }
    • pour initialiser un attribut dans la liste d’initialisation du constructeur (ce qui n’est pas possible avec la syntaxe (3) :
      class B {
          public:
          B() : b{7} {
              // b est initialisé à 7
          }
          private:
          int b;
      };
    • et enfin comme valeur de retour d’une fonction :
      A maFonction() {
          return {7};
      }

Conclusion

Pour être tranquille d’esprit, toujours privilégier l’initialisation uniforme (sauf lorsqu’un constructeur par liste d’initialisation existe et que l’on souhaite faire appel à un autre constructeur).

Classé dans : Tutos Mots clés : aucun

Les commentaires sont fermés.

Fil RSS des commentaires de cet article