Initialisation par valeur (value-initialization) avec C++ moderne

Andrzej Krzemieński

Traducteur

ahoff

Il y a quelque temps, je vous ai montré comment boost::value_initalized pouvait être utilisé pour initialiser la valeur d'objets dans des composants génériques en C++03. En C++11, cela n'est pratiquement pas nécessaire. C++11 vous donne la possibilité de forcer l'initialisation de la valeur de vos objets dans tout contexte d'initialisation possible.

Commentez

- Titre : Initialisation par valeur (value-initialization) avec C++ moderne

- Auteur : Andrzej Krzemieński

- Traducteur : ahoff

- Parution : 3 avril 2019

- Licence : 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 ® 2019 Andrzej Krzemieński. Aucune reproduction, même partielle, ne peut être faite de ce site et 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.

Sommaire

- I. Initialisation par valeur (value-initialization)

- II. La syntaxe de l'initialisation par valeur

I - Initialisation par valeur (value-initialization)

Êtes-vous familier avec le terme « initialisation par valeur » (value initialization) ? Il est parfois confondu avec le terme « initialisation par défaut » (default initialization). Par exemple, dans le code suivant :

1. struct widget

2. {

3. int x;

4. int y;

5. std::string title;

6. };

8. void default_initialization()

9. {

10. int i;        // initialise i par défaut

11. std::mutex m; // initialise m par défaut

12. widget w;     // initialise w par défaut

13. int a[9];     // initialise a par défaut

14. }

La variable i est initialisée par défaut. Sa valeur est indéterminée, aucune initialisation n'étant effectuée. Ce choix est fait par souci de performances. En effet, parfois nous n'avons pas besoin d'une valeur initiale. Prenons par exemple le code suivant :

int i;         // non initialisée
std::cin >> i; // lit la valeur initiale

Poursuivons avec le même exemple : m est initialisée par défaut, mais, std::mutex possédant un constructeur par défaut, ce constructeur est choisi pour l'initialisation par défaut. Dans le cas de w, parce que le type widget ne fournit (/dispose) pas d’une implémentation customisée du constructeur par défaut (NdT : un constructeur est dit « par défaut » s’il peut être appelé sans paramètres), un constructeur par défaut est implicitement généré (et appelé) par le compilateur : il appelle les constructeurs par défaut des sous-objets membres, qui ne réalisent aucune initialisation pour x et y (car il s'agit d'entiers int) et appelle le constructeur par défaut implémenté pour title.

Dans le cas de a, chaque élément du tableau est initialisé par défaut, et laissé dans un état indéterminé.

Comme nous l'avons vu, pour les types fondamentaux, l'initialisation par défaut laisse les objets dans un état indéterminé. L'initialisation par valeur nous permet de préciser que, pour les types fondamentaux, les objets devraient être initialisés avec la valeur 0. Il était possible, dans certains contextes, de faire ça en C++03 :

1. struct value_initialization

2. {

3. int i;

4. std::mutex m;

5. widget w;

6. int a[9];

8. value_initialization()

9. : i() // initialise i par valeur

10. , m() // initialise m par valeur

11. , w() // initialise w par valeur

12. , a() // initialise a par valeur

13. };

Au sein de la liste d'initialisation du constructeur, () signifie « initialisé par valeur ». C'est-à-dire que i sera initialisée avec la valeur 0, que le constructeur par défaut de m sera appelé, que x et y, les membres de w, seront initialisés avec la valeur 0 et que le constructeur par défaut de la classe string sera utilisé pour title. Notons que les variables de type entier sont initialisées avec la valeur 0.

Si nous voulons déclencher une telle initialisation de valeur pour les objets automatiques en C++03, cela est possible, mais nous devons, pour chaque objet, sélectionner la syntaxe d'initialisation appropriée :

1. void manual_value_initialization()

2. {

3. int i = 0;     // "=0" pour les scalaires

4. std::mutex m;  // rien ici

5. widget w = {}; // initialisation par agrégat

6. int a[9] = {}; // initialisation par agrégat

7. }

Mais que faire s'il s'agit d'une fonction template :

1. template <typename T>

2. void manual_value_initialization()

3. {

4. T t = /* QUEL TYPE ? */;

5. }

Puisque T peut être int, std::mutex, widget ou int[9], quelle syntaxe devons-nous choisir pour l'initialisation par valeur ?

Aucune des syntaxes présentées ci-dessus n'est compatible avec les quatre types.

II - La syntaxe de l'initialisation par valeur

En C++11, le problème énoncé ci-dessus est résolu en utilisant des accolades :

1. template <typename T>

2. void cpp11_value_initialization()

3. {

4. T t {}; // initialisation par valeur

5. }

{} signifie simplement « initialise par valeur », mais, contrairement à (), fonctionne dans n'importe quel contexte d'initialisation.

Revenons à notre exemple avec la classe value_initialization. Il est risqué d'utiliser des types fondamentaux comme membres de la classe, car il est possible d'oublier de les initialiser par valeur dans le(s) constructeur(s). C'est particulièrement vrai lorsque l'on ajoute un nouveau membre et que le corps du constructeur est dans un autre fichier cpp. Dans un tel cas, il est possible d'oublier d'ajouter l'initialisation par valeur dans tous les constructeurs.

C++11 nous donne la possibilité de résoudre ce problème :

1. struct cpp11_value_initialization

2. {

3. int i {};

4. std::mutex m {};

5. widget w {};

6. int a[9] {};

8. // le constructeur par défaut n'est pas nécessaire

9. };

Dans l'exemple ci-dessus, int i {}; indique, sauf indication contraire dans le constructeur, que i sera initialisée par valeur lors de l'initialisation. De même, dans un « cas générique » :

1. template <typename T>

2. class C

3. {

4. T member {};

5. // ...

6. };

Notons que si une classe définit (implicitement ou explicitement) un constructeur par défaut, les accolades vides {} ne sont pas interprétées comme un initializer_list vide (voir ce billet pour plus d'informations).

C'est pour cette raison que la syntaxe d'initialisation par accolades est appelée « initialisation uniforme » (uniform intialization), car elle fonctionne dans tous les contextes d'initialisation. Cela inclut la liste d'initialisation des constructeurs, la déclaration de variables, fussent-elles automatiques, temporaires, globales ou allouées sur le tas et les membres de classe. Notons que, dans le dernier exemple, si nous avions utilisé () pour « initialiser » member, nous aurions déclaré une fonction membre au lieu d'un membre. C'est pour cette raison que l'initialisation avec () est dite « non uniforme ».

« uniforme » ne sous-entend pas que () et {} feront la même chose. Voir ce billet pour un exemple.

La syntaxe avec les accolades vides autorise une manière curieuse de créer des tags. Un tag est une structure qui ne stocke aucune valeur, mais dont le type est utilisé pour sélectionner la surcharge d'un constructeur ou d'une fonction. Vous pouvez les retrouver dans la Bibliothèque Standard, par exemple std::allocator_arg, std::nothrow, std::piecewise_construct, std::defer_lock. De tels tags peuvent être déclarés en une ligne :

constexpr struct allocator_arg_t{} allocator_arg{};

Les premières accolades vides définissent la classe allocator_arg_t comme vide, les secondes initialisent par valeur la constante globale allocator_arg.