Pourquoi l'initialisation de la liste (en utilisant des accolades) est-elle meilleure que les alternatives?

406
MyClass a1 {a};     // clearer and less error-prone than the other three
MyClass a2 = {a};
MyClass a3 = a;
MyClass a4(a);

Pourquoi?

Je n'ai pas pu trouver de réponse sur SO, alors laissez-moi répondre à ma propre question.

Oleksiy
la source
12
Pourquoi ne pas utiliser auto?
Mark Garcia
33
C'est vrai, c'est pratique, mais cela réduit la lisibilité à mon avis - j'aime voir de quel type est un objet lors de la lecture du code. Si vous êtes sûr à 100% de quel type d'objet il s'agit, pourquoi utiliser auto? Et si vous utilisez l'initialisation de liste (lisez ma réponse), vous pouvez être sûr qu'elle est toujours correcte.
Oleksiy
103
@Oleksiy: std::map<std::string, std::vector<std::string>>::const_iteratorvoudrait un mot avec vous.
Xeo
9
@Oleksiy Je recommande de lire ce GotW .
Rapptz
17
@doc, je dirais que using MyContainer = std::map<std::string, std::vector<std::string>>;c'est encore mieux (d'autant plus que vous pouvez le modéliser!)
JAB

Réponses:

357

Fondamentalement, copier et coller à partir de "The C ++ Programming Language 4th Edition" de Bjarne Stroustrup :

L'initialisation de liste ne permet pas de rétrécissement (§iso.8.5.4). C'est:

  • Un entier ne peut pas être converti en un autre entier qui ne peut pas contenir sa valeur. Par exemple, char to int est autorisé, mais pas int to char.
  • Une valeur à virgule flottante ne peut pas être convertie en un autre type à virgule flottante qui ne peut pas contenir sa valeur. Par exemple, flotter pour doubler est autorisé, mais pas double pour flotter.
  • Une valeur à virgule flottante ne peut pas être convertie en type entier.
  • Une valeur entière ne peut pas être convertie en type à virgule flottante.

Exemple:

void fun(double val, int val2) {

    int x2 = val; // if val==7.9, x2 becomes 7 (bad)

    char c2 = val2; // if val2==1025, c2 becomes 1 (bad)

    int x3 {val}; // error: possible truncation (good)

    char c3 {val2}; // error: possible narrowing (good)

    char c4 {24}; // OK: 24 can be represented exactly as a char (good)

    char c5 {264}; // error (assuming 8-bit chars): 264 cannot be 
                   // represented as a char (good)

    int x4 {2.0}; // error: no double to int value conversion (good)

}

La seule situation où = est préféré à {} consiste à utiliser un automot-clé pour obtenir le type déterminé par l'initialiseur.

Exemple:

auto z1 {99};   // z1 is an int
auto z2 = {99}; // z2 is std::initializer_list<int>
auto z3 = 99;   // z3 is an int

Conclusion

Préférez l'initialisation {} aux alternatives, sauf si vous avez une bonne raison de ne pas le faire.

Oleksiy
la source
52
Il y a aussi le fait que l'utilisation ()peut être analysée comme une déclaration de fonction. C'est déroutant et incohérent que vous puissiez dire T t(x,y,z);mais pas T t(). Et parfois, vous êtes certain x, vous ne pouvez même pas le dire T t(x);.
juanchopanza
84
Je suis fortement en désaccord avec cette réponse; l'initialisation contreventée devient un gâchis complet lorsque vous avez des types avec un ctor acceptant a std::initializer_list. RedXIII mentionne ce problème (et le balaye simplement), tandis que vous l'ignorez complètement. A(5,4)et A{5,4}peut appeler des fonctions complètement différentes, et c'est une chose importante à savoir. Cela peut même entraîner des appels qui ne semblent pas intuitifs. Dire que vous devriez préférer {}par défaut amènera les gens à mal comprendre ce qui se passe. Ce n'est pas de ta faute, cependant. Personnellement, je pense que c'est une fonctionnalité extrêmement mal pensée.
user1520427
13
@ user1520427 C'est pourquoi il y a la partie " sauf si vous avez une bonne raison de ne pas ".
Oleksiy
67
Bien que cette question soit ancienne, elle a pas mal de succès donc j'ajoute ceci ici juste pour référence (je ne l'ai vu nulle part ailleurs dans la page). A partir de C ++ 14 avec les nouvelles règles de déduction automatique de braced-init-list, il est maintenant possible d'écrire auto var{ 5 }et il ne sera pas déduit intplus que std::initializer_list<int>.
Edoardo Sparkon Dominici
13
Haha, d'après tous les commentaires, on ne sait toujours pas quoi faire. Ce qui est clair, c'est que la spécification C ++ est un gâchis!
DrumM
113

Il existe déjà d'excellentes réponses concernant les avantages de l'initialisation de liste, mais ma règle de base personnelle n'est PAS d'utiliser des accolades chaque fois que possible, mais de la rendre dépendante de la signification conceptuelle:

  • Si l'objet que je crée contient conceptuellement les valeurs que je passe dans le constructeur (par exemple, conteneurs, structures POD, atomiques, pointeurs intelligents, etc.), alors j'utilise les accolades.
  • Si le constructeur ressemble à un appel de fonction normal (il effectue des opérations plus ou moins complexes qui sont paramétrées par les arguments), alors j'utilise la syntaxe d'appel de fonction normale.
  • Pour l'initialisation par défaut, j'utilise toujours des accolades.
    D'une part, de cette façon, je suis toujours sûr que l'objet est initialisé, qu'il s'agisse par exemple d'une "vraie" classe avec un constructeur par défaut qui serait appelé de toute façon ou d'un type intégré / POD. Deuxièmement, il est - dans la plupart des cas - conforme à la première règle, car un objet initialisé par défaut représente souvent un objet "vide".

D'après mon expérience, cet ensemble de règles peut être appliqué de manière beaucoup plus cohérente que l'utilisation d'accolades par défaut, mais avoir à se souvenir explicitement de toutes les exceptions lorsqu'elles ne peuvent pas être utilisées ou ont une signification différente de la syntaxe "normale" d'appel de fonction avec parenthèses (appelle une surcharge différente).

Il s'intègre par exemple parfaitement avec les types de bibliothèques standard comme std::vector:

vector<int> a{10,20};   //Curly braces -> fills the vector with the arguments

vector<int> b(10,20);   //Parentheses -> uses arguments to parametrize some functionality,                          
vector<int> c(it1,it2); //like filling the vector with 10 integers or copying a range.

vector<int> d{};      //empty braces -> default constructs vector, which is equivalent
                      //to a vector that is filled with zero elements
MikeMB
la source
11
Entièrement d'accord avec la plupart de votre réponse. Cependant, ne pensez-vous pas que mettre des accolades vides pour le vecteur est simplement redondant? Je veux dire, c'est ok, quand vous devez initialiser un objet de type générique T, mais quel est le but de le faire pour du code non générique?
Mikhail
8
@Mikhail: C'est certainement redondant, mais j'ai l'habitude de toujours expliciter l'initialisation des variables locales. Comme je l'ai écrit, il s'agit principalement de cohérence, donc je ne l'oublie pas, quand c'est important. Ce n'est certainement rien que je mentionnerais dans une revue de code ou mettrais dans un guide de style.
MikeMB
4
ensemble de règles assez propre.
laike9m
5
C'est de loin la meilleure réponse. {} est comme l'héritage - facile à abuser, conduisant à un code difficile à comprendre.
UKMonkey
2
Exemple @MikeMB: const int &b{}<- n'essaie pas de créer une référence non initialisée, mais la lie à un objet entier temporaire. Deuxième exemple: struct A { const int &b; A():b{} {} };<- n'essaie pas de créer une référence non initialisée (comme le ()ferait), mais la lie à un objet entier temporaire, puis le laisse pendant. GCC même avec -Wallne met pas en garde pour le deuxième exemple.
Johannes Schaub - litb
92

Il y a BEAUCOUP de raisons d'utiliser l'initialisation de l'accolade, mais vous devez savoir que le initializer_list<>constructeur est préféré aux autres constructeurs , l'exception étant le constructeur par défaut. Cela entraîne des problèmes avec les constructeurs et les modèles où le Tconstructeur de type peut être soit une liste d'initialisation, soit un ancien ctor simple.

struct Foo {
    Foo() {}

    Foo(std::initializer_list<Foo>) {
        std::cout << "initializer list" << std::endl;
    }

    Foo(const Foo&) {
        std::cout << "copy ctor" << std::endl;
    }
};

int main() {
    Foo a;
    Foo b(a); // copy ctor
    Foo c{a}; // copy ctor (init. list element) + initializer list!!!
}

En supposant que vous ne rencontriez pas de telles classes, il y a peu de raisons de ne pas utiliser la liste des initialiseurs.

Rouge XIII
la source
20
C'est un point très important dans la programmation générique. Lorsque vous écrivez des modèles, n'utilisez pas de listes d'initiation contreventées (le nom de la norme pour { ... }) sauf si vous voulez la initializer_listsémantique (enfin, et peut-être pour la construction par défaut d'un objet).
Xeo
82
Honnêtement, je ne comprends pas pourquoi la std::initializer_listrègle existe même - elle ajoute simplement de la confusion et du désordre à la langue. Qu'est-ce qui ne va pas Foo{{a}}si vous voulez le std::initializer_listconstructeur? Cela semble tellement plus facile à comprendre que d'avoir la std::initializer_listpriorité sur toutes les autres surcharges.
user1520427
5
+1 pour le commentaire ci-dessus, car c'est vraiment un gâchis je pense !! ce n'est pas de la logique; Foo{{a}}suit une logique pour moi bien plus que celle Foo{a}qui se transforme en priorité de liste d'initialisation (les utilisateurs pourraient penser que hm ...)
Gabriel
32
Fondamentalement, C ++ 11 remplace un désordre par un autre désordre. Oh, désolé, il ne le remplace pas - il y ajoute. Comment savoir si vous ne rencontrez pas de telles classes? Et si vous commencez sans std::initializer_list<Foo> constructeur, mais qu'il va être ajouté à la Fooclasse à un moment donné pour étendre son interface? Ensuite, les utilisateurs de Fooclasse sont foutus.
doc
10
.. quelles sont les "NOMBREUSES raisons d'utiliser l'initialisation de l'accolade"? Cette réponse indique une raison ( initializer_list<>), qu'elle ne qualifie pas vraiment qui dit qu'elle est préférée, puis continue de mentionner un bon cas où elle n'est PAS préférée. Que manque-t-il que ~ 30 autres personnes (au 21/04/2016) ont trouvé utile?
dwanderson
0

C'est plus sûr aussi longtemps que vous ne construisez pas avec -Wno-restricting comme disons Google dans Chromium. Si vous le faites, alors c'est MOINS sûr. Sans cet indicateur, les seuls cas dangereux seront corrigés par C ++ 20.

Remarque: A) Les accolades sont plus sûres car elles ne permettent pas de rétrécissement. B) Les accolades sont moins sûres car elles peuvent contourner les constructeurs privés ou supprimés, et appeler implicitement les constructeurs marqués explicitement.

Ces deux combinés signifient qu'ils sont plus sûrs si ce qui est à l'intérieur sont des constantes primitives, mais moins sûrs si ce sont des objets (bien qu'ils soient fixes en C ++ 20)

Allan Jensen
la source
J'ai essayé de fouiner sur goldbolt.org pour contourner les constructeurs "explicites" ou "privés" en utilisant l'exemple de code fourni et en rendant l'un ou l'autre privé ou explicite, et j'ai été récompensé par les erreurs de compilation appropriées. Vous voulez sauvegarder cela avec un exemple de code?
Mark Storer
Voici le correctif du problème proposé pour C ++ 20: open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1008r1.pdf
Allan Jensen
1
Si vous modifiez votre réponse pour montrer de quelle (s) version (s) de C ++ vous parlez, je serais heureux de changer mon vote.
Mark Storer