Perché l'inizializzazione dell'elenco (usando le parentesi graffe) è migliore delle alternative?


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

Perché?

Non sono riuscito a trovare una risposta su SO, quindi lasciami rispondere alla mia domanda.


12
Perché non usare auto?
Mark Garcia,

33
È vero, è conveniente, ma a mio avviso riduce la leggibilità: mi piace vedere che tipo di oggetto è quando legge il codice. Se sei sicuro al 100% del tipo di oggetto, perché usare auto? E se usi l'inizializzazione dell'elenco (leggi la mia risposta), puoi essere sicuro che sia sempre corretto.
Oleksiy,

103
@Oleksiy: std::map<std::string, std::vector<std::string>>::const_iteratorvorrei una parola con te.
Xeo,

9
@Oleksiy Consiglio di leggere questo GotW .
Rapptz,

17
@doc Direi che using MyContainer = std::map<std::string, std::vector<std::string>>;è ancora meglio (soprattutto perché puoi modellarlo !)
JAB

Risposte:


357

Fondamentalmente copia e incolla da "The C ++ Programming Language 4th Edition" di Bjarne Stroustrup :

L'inizializzazione dell'elenco non consente il restringimento (§iso.8.5.4). Questo è:

  • Un numero intero non può essere convertito in un altro numero intero che non può contenere il suo valore. Ad esempio, è consentito char to int, ma non int a char.
  • Un valore in virgola mobile non può essere convertito in un altro tipo in virgola mobile che non può contenere il suo valore. Ad esempio, float per raddoppiare è consentito, ma non raddoppiare per float.
  • Un valore a virgola mobile non può essere convertito in un tipo intero.
  • Un valore intero non può essere convertito in un tipo a virgola mobile.

Esempio:

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)

}

L' unica situazione in cui = è preferito su {} è quando si utilizza la autoparola chiave per ottenere il tipo determinato dall'inizializzatore.

Esempio:

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

Conclusione

Preferisci {} l'inizializzazione rispetto alle alternative a meno che tu non abbia un motivo valido per non farlo.


52
C'è anche il fatto che l'utilizzo ()può essere analizzato come una dichiarazione di funzione. È confuso e incoerente che si possa dire T t(x,y,z);ma no T t(). E a volte, certo x, non puoi nemmeno dirlo T t(x);.
juanchopanza,

84
Non sono assolutamente d'accordo con questa risposta; l'inizializzazione rinforzata diventa un disastro completo quando si hanno tipi con un ctor che accetta a std::initializer_list. RedXIII menziona questo problema (e lo elimina solo), mentre lo ignori completamente. A(5,4)e A{5,4}può chiamare funzioni completamente diverse, e questa è una cosa importante da sapere. Può anche provocare chiamate che sembrano non intuitive. Dire che dovresti preferire {}di default porterà le persone a fraintendere quello che sta succedendo. Non è colpa tua, però. Personalmente penso che sia una funzionalità estremamente mal pensata.
user1520427

13
@ user1520427 Ecco perché c'è la parte "a meno che tu non abbia una forte ragione per non ".
Oleksiy,

67
Anche se questa domanda è vecchia ha un discreto successo, quindi sto aggiungendo questo qui solo per riferimento (non l'ho visto altrove nella pagina). Da C ++ 14 con le nuove Regole per la deduzione automatica da braced-init-list è ora possibile scrivere auto var{ 5 }e non sarà più dedotto intcome std::initializer_list<int>.
Edoardo Sparkon Dominici,

13
Haha, da tutti i commenti non è ancora chiaro cosa fare. Ciò che è chiaro è che la specifica C ++ è un casino!
DrumM,

113

Esistono già ottime risposte sui vantaggi dell'utilizzo dell'inizializzazione dell'elenco, tuttavia la mia regola empirica personale NON è quella di utilizzare le parentesi graffe quando possibile, ma invece di renderlo dipendente dal significato concettuale:

  • Se l'oggetto che sto creando concettualmente contiene i valori che sto passando nel costruttore (ad es. Container, strutture POD, atomica, puntatori intelligenti ecc.), Allora sto usando le parentesi graffe.
  • Se il costruttore assomiglia a una normale chiamata di funzione (esegue alcune operazioni più o meno complesse che sono parametrizzate dagli argomenti) allora sto usando la normale sintassi della chiamata di funzione.
  • Per l'inizializzazione predefinita uso sempre parentesi graffe.
    Per uno, in questo modo sono sempre sicuro che l'oggetto viene inizializzato indipendentemente dal fatto che sia ad esempio una classe "reale" con un costruttore predefinito che verrebbe chiamato comunque o un tipo incorporato / POD. In secondo luogo, nella maggior parte dei casi, è coerente con la prima regola, poiché un oggetto inizializzato predefinito rappresenta spesso un oggetto "vuoto".

Nella mia esperienza, questo set di regole può essere applicato in modo molto più coerente rispetto all'uso di parentesi graffe per impostazione predefinita, ma dover ricordare esplicitamente tutte le eccezioni quando non possono essere utilizzate o hanno un significato diverso rispetto alla "normale" sintassi di chiamata di funzione con parentesi (chiama un sovraccarico diverso).

Ad esempio si adatta perfettamente a tipi di libreria standard come 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

11
Totalmente d'accordo con la maggior parte della tua risposta. Tuttavia, non pensi che mettere delle parentesi graffe vuote per il vettore sia semplicemente ridondante? Voglio dire, va bene, quando è necessario inizializzare un oggetto di tipo T generico, ma qual è lo scopo di farlo per un codice non generico?
Mikhail,

8
@Mikhail: è sicuramente ridondante, ma è mia abitudine rendere esplicita l'inizializzazione della variabile locale. Come ho scritto, si tratta principalmente di coerenza, quindi non lo dimentico, quando importa. Non è certamente nulla di cui parlerei in una recensione di codice o che inserissi una guida di stile.
MikeMB,

4
set di regole abbastanza pulito.
laike9m

5
Questa è di gran lunga la risposta migliore. {} è come l'eredità - facile da abusare, che porta a codice difficile da capire.
UKMonkey

2
Esempio @MikeMB: const int &b{}<- non tenta di creare un riferimento non inizializzato, ma lo lega a un oggetto intero temporaneo. Secondo esempio: struct A { const int &b; A():b{} {} };<- non tenta di creare un riferimento non inizializzato (come ()farebbe), ma associarlo a un oggetto intero temporaneo e quindi lasciarlo penzolare. GCC anche con -Wallnon avvisa per il secondo esempio.
Johannes Schaub - litb

92

Esistono MOLTE ragioni per utilizzare l'inizializzazione del controvento, ma è necessario tenere presente che il initializer_list<>costruttore è preferito agli altri costruttori , con l'eccezione che è il costruttore predefinito. Ciò porta a problemi con costruttori e modelli in cui il tipo Tcostruttore può essere un elenco di inizializzatori o un semplice vecchio ctor.

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!!!
}

Supponendo che non si incontrino tali classi, ci sono pochi motivi per non utilizzare l'elenco degli inizializzatori.


20
Questo è un punto molto importante nella programmazione generica. Quando scrivi modelli, non usare gli elenchi di controventi-init (il nome dello standard per { ... }) a meno che tu non voglia la initializer_listsemantica (bene, e forse per la costruzione di default di un oggetto).
Xeo,

82
Onestamente non capisco perché la std::initializer_listregola esista - aggiunge solo confusione e confusione alla lingua. Cosa c'è che non va Foo{{a}}se vuoi il std::initializer_listcostruttore? Sembra molto più facile da capire che avere la std::initializer_listprecedenza su tutti gli altri sovraccarichi.
user1520427

5
+1 per il commento sopra, perché è davvero un casino penso !! non è una logica; Foo{{a}}segue una logica per me molto più di quella Foo{a}che si trasforma in precedenza nella lista degli inizializzatori (up potrebbe pensare l'utente hm ...)
Gabriel

32
Fondamentalmente C ++ 11 sostituisce un pasticcio con un altro pasticcio. Oh, scusa se non lo sostituisce, ma lo aggiunge. Come puoi sapere se non incontri tali lezioni? Che cosa succede se si avvia senza std::initializer_list<Foo> costruttore, ma verrà aggiunto alla Fooclasse ad un certo punto per estendere la sua interfaccia? Quindi gli utenti di Fooclasse vengono incasinati.
doc

10
.. quali sono i "MOLTI motivi per utilizzare l'inizializzazione del controvento"? Questa risposta indica un motivo ( initializer_list<>), che in realtà non qualifica chi dice che è preferito, e quindi procede a menzionare un buon caso in cui NON è preferito. Cosa mi manca che ~ 30 altre persone (dal 21-04-2016) hanno trovato utile?
Dwanderson,

0

È solo più sicuro finché non si costruisce con -Wno-restringimento come dire Google fa in Chromium. Se lo fai, allora è MENO sicuro. Senza quella bandiera, tuttavia, gli unici casi non sicuri verranno risolti da C ++ 20.

Nota: A) Le parentesi graffe sono più sicure perché non consentono il restringimento. B) Le parentesi graffe sono meno sicure perché possono bypassare costruttori privati ​​o eliminati e chiamare implicitamente costruttori espliciti contrassegnati.

Quei due combinati significano che sono più sicuri se ciò che è dentro sono costanti primitive, ma meno sicuri se sono oggetti (sebbene risolti in C ++ 20)


Ho provato a cercare su goldbolt.org di bypassare i costruttori "espliciti" o "privati" usando il codice di esempio fornito e rendendo l'uno o l'altro privato o esplicito, e sono stato premiato con gli errori del compilatore appropriati. Ti interessa eseguire il backup con un codice di esempio?
Mark Storer,

Questa è la soluzione per il problema proposto per C ++ 20: open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1008r1.pdf
Allan Jensen,

1
Se modifichi la tua risposta per mostrare di quale versione di C ++ stai parlando, sarei felice di cambiare il mio voto.
Mark Storer,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.