Quando utilizzare l'inizializzatore racchiuso tra parentesi graffe?


94

In C ++ 11, abbiamo quella nuova sintassi per inizializzare le classi che ci offre un gran numero di possibilità su come inizializzare le variabili.

{ // Example 1
  int b(1);
  int a{1};
  int c = 1;
  int d = {1};
}
{ // Example 2
  std::complex<double> b(3,4);
  std::complex<double> a{3,4};
  std::complex<double> c = {3,4};
  auto d = std::complex<double>(3,4);
  auto e = std::complex<double>{3,4};
}
{ // Example 3
  std::string a(3,'x');
  std::string b{3,'x'}; // oops
}
{ // Example 4
  std::function<int(int,int)> a(std::plus<int>());
  std::function<int(int,int)> b{std::plus<int>()};
}
{ // Example 5
  std::unique_ptr<int> a(new int(5));
  std::unique_ptr<int> b{new int(5)};
}
{ // Example 6
  std::locale::global(std::locale("")); // copied from 22.4.8.3
  std::locale::global(std::locale{""});
}
{ // Example 7
  std::default_random_engine a {}; // Stroustrup's FAQ
  std::default_random_engine b;
}
{ // Example 8
  duration<long> a = 5; // Stroustrup's FAQ too
  duration<long> b(5);
  duration<long> c {5};
}

Per ogni variabile che dichiaro, devo pensare quale sintassi di inizializzazione dovrei usare e questo rallenta la mia velocità di codifica. Sono sicuro che non era l'intenzione di introdurre le parentesi graffe.

Quando si tratta di codice modello, la modifica della sintassi può portare a significati diversi, quindi è essenziale seguire la strada giusta.

Mi chiedo se esista una linea guida universale su quale sintassi si dovrebbe scegliere.


1
Un esempio di comportamento non intenzionale da {} inizializzazione: stringa (50, 'x') vs stringa {50, 'x'} qui
P i

Risposte:


64

Penso che la seguente potrebbe essere una buona linea guida:

  • Se il valore (singolo) con cui stai inizializzando è inteso come il valore esatto dell'oggetto, usa l' =inizializzazione copy ( ) (perché in caso di errore, non invocherai mai accidentalmente un costruttore esplicito, che generalmente interpreta il valore fornito diversamente). Nei luoghi in cui l'inizializzazione della copia non è disponibile, controlla se l'inizializzazione delle parentesi graffe ha la semantica corretta e, in tal caso, usala; altrimenti usa l'inizializzazione delle parentesi (se anche questa non è disponibile, sei sfortunato comunque).

  • Se i valori con cui stai inizializzando sono un elenco di valori da memorizzare nell'oggetto (come gli elementi di un vettore / matrice, o la parte reale / immaginaria di un numero complesso), usa l'inizializzazione delle parentesi graffe se disponibile.

  • Se i valori con cui si sta inizializzando non sono valori da memorizzare, ma descrivono il valore / stato previsto dell'oggetto, utilizzare le parentesi. Esempi sono l'argomento della dimensione di a vectoro l'argomento del nome file di un file fstream.


4
@ user1304032: una locale non è una stringa, quindi non useresti l'inizializzazione della copia. Una locale inoltre non contiene una stringa (potrebbe memorizzare quella stringa come dettaglio di implementazione, ma non è questo il suo scopo), quindi non useresti l'inizializzazione delle parentesi graffe. Pertanto la linea guida dice di utilizzare l'inizializzazione delle parentesi.
celtschk

2
Personalmente mi è piaciuta al massimo questa linea guida e funziona bene anche con codice generico. Ci sono alcune eccezioni ( T {}o ragioni sintattiche come l' analisi più fastidiosa ), ma in generale penso che questo sia un buon consiglio. Nota che questa è la mia opinione soggettiva, quindi dovresti dare un'occhiata anche alle altre risposte.
helami

2
@celtschk: non funzionerà per i tipi non copiabili e non mobili; type var{};fa.
ildjarn

2
@celtschk: non sto dicendo che è qualcosa che si verifica frequentemente, ma è meno digitando e funziona in più contesti, quindi qual è lo svantaggio?
ildjarn

2
Le mie linee guida certamente non richiedono mai l'inizializzazione della copia. ; -]
ildjarn

26

Sono abbastanza sicuro che non ci sarà mai una linea guida universale. Il mio approccio è quello di utilizzare sempre le parentesi graffe ricordando quello

  1. I costruttori dell'elenco di inizializzatori hanno la precedenza sugli altri costruttori
  2. Tutti i contenitori di libreria standard e std :: basic_string hanno costruttori di elenchi di inizializzatori.
  3. L'inizializzazione delle parentesi graffe non consente di restringere le conversioni.

Quindi le parentesi graffe rotonde e ricce non sono intercambiabili. Ma sapere dove differiscono mi permette di usare l'inizializzazione curly su parentesi tonde nella maggior parte dei casi (alcuni dei casi in cui non posso sono attualmente bug del compilatore).


6
Le parentesi graffe hanno lo svantaggio di poter chiamare per errore il costruttore dell'elenco. Le parentesi tonde no. Non è un motivo per utilizzare le parentesi tonde per impostazione predefinita?
helami

4
@user: int i = 0;Non penso che nessuno lo userebbe int i{0}lì, e potrebbe creare confusione (inoltre, 0è if type int, quindi non ci sarebbe alcun restringimento ). Per tutto il resto, seguirei il consiglio di Juancho: preferisci {}, fai attenzione ai pochi casi in cui non dovresti. Nota che non ci sono molti tipi che accettano elenchi di inizializzatori come argomenti del costruttore, puoi aspettarti che i contenitori e i tipi simili a contenitori (tupla ...) li abbiano, ma la maggior parte del codice chiamerà il costruttore appropriato.
David Rodríguez - dribeas

3
@ user1304032 dipende se ti interessa il restringimento. Lo faccio, quindi preferisco che il compilatore mi dica che int i{some floating point}è un errore, piuttosto che troncare silenziosamente.
juanchopanza

3
Riguardo a "preferisci {}, fai attenzione ai pochi casi in cui non dovresti": Diciamo che due classi hanno un costruttore semanticamente equivalente ma una classe ha anche un elenco di inizializzatori. I due costruttori equivalenti dovrebbero essere chiamati in modo diverso?
helami

3
@helami: "Supponiamo che due classi abbiano un costruttore semanticamente equivalente ma anche una classe ha un elenco di inizializzatori. I due costruttori equivalenti dovrebbero essere chiamati in modo diverso?" Diciamo che mi imbatto nell'analisi più irritante; ciò può accadere su qualsiasi costruttore per qualsiasi istanza. È molto più facile evitarlo se usi solo il {}significato di "inizializza" a meno che tu non possa assolutamente farlo .
Nicol Bolas

16

Al di fuori del codice generico (cioè i modelli), puoi (e lo faccio) usare le parentesi graffe ovunque . Un vantaggio è che funziona ovunque, ad esempio anche per l'inizializzazione in classe:

struct foo {
    // Ok
    std::string a = { "foo" };

    // Also ok
    std::string b { "bar" };

    // Not possible
    std::string c("qux");

    // For completeness this is possible
    std::string d = "baz";
};

o per argomenti di funzione:

void foo(std::pair<int, double*>);
foo({ 42, nullptr });
// Not possible with parentheses without spelling out the type:
foo(std::pair<int, double*>(42, nullptr));

Per le variabili non faccio molta attenzione tra gli stili T t = { init };o T t { init };, trovo che la differenza sia minore e nel peggiore dei casi si tradurrà solo in un utile messaggio del compilatore sull'uso improprio di un explicitcostruttore.

Per i tipi che accettano std::initializer_listanche se ovviamente a volte std::initializer_listsono necessari i non costruttori (l'esempio classico è std::vector<int> twenty_answers(20, 42);). Allora va bene non usare l'apparecchio.


Quando si tratta di codice generico (cioè nei modelli), l'ultimo paragrafo dovrebbe aver sollevato alcuni avvertimenti. Considera quanto segue:

template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args)
{ return std::unique_ptr<T> { new T { std::forward<Args>(args)... } }; }

Quindi auto p = make_unique<std::vector<T>>(20, T {});crea un vettore di dimensione 2 se Tè eg int, o un vettore di dimensione 20 se Tè std::string. Un segno molto rivelatore che sta succedendo qualcosa di molto sbagliato qui è che non c'è nessun tratto che può salvarti qui (ad esempio con SFINAE): std::is_constructibleè in termini di inizializzazione diretta, mentre stiamo usando l'inizializzazione di parentesi graffa che rimanda a dirigere- inizializzazione se e solo se non c'è nessun costruttore che prende std::initializer_listinterferenze. Allo stesso modo non std::is_convertibleè di alcun aiuto.

Ho studiato se è effettivamente possibile tirare a mano un tratto che può risolverlo, ma non sono eccessivamente ottimista al riguardo. In ogni caso non credo che ci mancherebbe molto, credo che il fatto che si make_unique<T>(foo, bar)traduca in una costruzione equivalente a T(foo, bar)sia molto intuitivo; soprattutto dato che make_unique<T>({ foo, bar })è abbastanza dissimile e ha senso solo se fooe barhanno lo stesso tipo.

Quindi per il codice generico uso solo le parentesi graffe per l'inizializzazione del valore (ad esempio T t {};o T t = {};), che è molto conveniente e penso superiore al modo C ++ 03 T t = T();. Altrimenti è la sintassi di inizializzazione diretta (cioè T t(a0, a1, a2);), o talvolta la costruzione predefinita ( T t; stream >> t;essendo l'unico caso in cui lo uso credo).

Ciò non significa che tutte le parentesi graffe siano cattive, considera l'esempio precedente con le correzioni:

template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args)
{ return std::unique_ptr<T> { new T(std::forward<Args>(args)...) }; }

Questo utilizza ancora le parentesi graffe per costruire il std::unique_ptr<T>, anche se il tipo effettivo dipende dal parametro del modello T.


@interjay Alcuni dei miei esempi potrebbero effettivamente dover utilizzare invece tipi non firmati, ad esempio make_unique<T>(20u, T {})per Tessere unsignedo std::string. Non troppo sicuro sui dettagli. (Si noti che ho anche commentato le aspettative riguardanti l'inizializzazione diretta rispetto all'inizializzazione delle parentesi graffe per quanto riguarda, ad esempio, le funzioni di inoltro perfetto.) std::string c("qux");Non è stato specificato per funzionare come inizializzazione in classe per evitare ambiguità con le dichiarazioni di funzioni membro nella grammatica.
Luc Danton

@interjay Non sono d'accordo con te sul primo punto, sentiti libero di controllare 8.5.4 Inizializzazione della lista e 13.3.1.7 Inizializzazione tramite inizializzazione della lista. Per quanto riguarda il secondo, è necessario dare un'occhiata più da vicino a ciò che ho scritto (che riguarda l' inizializzazione in classe ) e / o la grammatica C ++ (ad es. Membro-dichiaratore , che fa riferimento a parentesi graffa-o-uguale-inizializzatore ).
Luc Danton

Hmm, hai ragione - stavo testando con GCC 4.5 in precedenza, il che sembrava confermare ciò che stavo dicendo, ma GCC 4.6 è d'accordo con te. E mi è mancato il fatto che stavi parlando di inizializzazione in classe. Mie scuse.
Interjay
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.