Lo stile di codifica è in definitiva soggettivo ed è altamente improbabile che ne derivino sostanziali benefici in termini di prestazioni. Ma ecco cosa direi che guadagni dall'uso liberale di inizializzazione uniforme:
Riduce al minimo i nomi di caratteri ridondanti
Considera quanto segue:
vec3 GetValue()
{
return vec3(x, y, z);
}
Perché devo digitare vec3
due volte? C'è un punto a questo? Il compilatore sa bene e bene cosa restituisce la funzione. Perché non posso semplicemente dire "chiama il costruttore di ciò che restituisco con questi valori e restituiscilo?" Con l'inizializzazione uniforme, posso:
vec3 GetValue()
{
return {x, y, z};
}
Tutto funziona.
Ancora meglio è per gli argomenti delle funzioni. Considera questo:
void DoSomething(const std::string &str);
DoSomething("A string.");
Funziona senza dover digitare un typename, perché std::string
sa costruirsi da un modo const char*
implicito. È fantastico. E se la stringa provenisse, diciamo RapidXML. O una stringa Lua. Cioè, diciamo che in realtà conosco la lunghezza della stringa in avanti. Il std::string
costruttore che prende a const char*
dovrà prendere la lunghezza della stringa se passo solo a const char*
.
Tuttavia, esiste un sovraccarico che richiede esplicitamente una lunghezza. Ma per usarlo, avrei dovuto fare questo: DoSomething(std::string(strValue, strLen))
. Perché il nome extra è presente? Il compilatore sa quale sia il tipo. Proprio come con auto
, possiamo evitare di avere nomi extra:
DoSomething({strValue, strLen});
Funziona e basta. Nessun nome, nessun clamore, niente. Il compilatore fa il suo lavoro, il codice è più breve e tutti sono felici.
Certo, ci sono argomenti da sostenere che la prima versione ( DoSomething(std::string(strValue, strLen))
) è più leggibile. Cioè, è ovvio cosa sta succedendo e chi sta facendo cosa. Questo è vero, fino a un certo punto; la comprensione del codice uniforme basato sull'inizializzazione richiede l'esame del prototipo della funzione. Questo è lo stesso motivo per cui alcuni dicono che non dovresti mai passare parametri per riferimento non const: in modo da poter vedere sul sito della chiamata se un valore viene modificato.
Ma lo stesso si potrebbe dire auto
; sapere da cosa si ottiene auto v = GetSomething();
richiede guardare la definizione di GetSomething
. Ma questo non ha smesso auto
di essere usato con un abbandono quasi spericolato una volta che hai accesso ad esso. Personalmente, penso che andrà bene una volta che ti sarai abituato. Soprattutto con un buon IDE.
Mai ottenere il più fastidioso analisi
Ecco del codice
class Bar;
void Func()
{
int foo(Bar());
}
Pop quiz: cos'è foo
? Se hai risposto "una variabile", ti sbagli. In realtà è il prototipo di una funzione che assume come parametro una funzione che restituisce a Bar
e il foo
valore restituito della funzione è un int.
Questo si chiama "Most Vexing Parse" del C ++ perché non ha assolutamente senso per un essere umano. Ma le regole del C ++ lo richiedono tristemente: se può eventualmente essere interpretato come un prototipo di funzione, lo sarà . Il problema è Bar()
; potrebbe essere una delle due cose. Potrebbe essere un tipo chiamato Bar
, il che significa che sta creando un temporaneo. Oppure potrebbe essere una funzione che non accetta parametri e restituisce a Bar
.
L'inizializzazione uniforme non può essere interpretata come un prototipo di funzione:
class Bar;
void Func()
{
int foo{Bar{}};
}
Bar{}
crea sempre un temporaneo. int foo{...}
crea sempre una variabile.
Esistono molti casi in cui si desidera utilizzare Typename()
ma semplicemente non è possibile a causa delle regole di analisi di C ++. Con Typename{}
, non c'è ambiguità.
Ragioni per non farlo
L'unico vero potere a cui ti arrendi è il restringimento. Non è possibile inizializzare un valore più piccolo con uno più grande con inizializzazione uniforme.
int val{5.2};
Ciò non verrà compilato. Puoi farlo con l'inizializzazione vecchio stile, ma non l'inizializzazione uniforme.
Ciò è stato fatto in parte per far funzionare effettivamente le liste di inizializzatori. Altrimenti, ci sarebbero molti casi ambigui per quanto riguarda i tipi di elenchi di inizializzatori.
Certo, alcuni potrebbero sostenere che tale codice merita di non essere compilato. Personalmente capisco di essere d'accordo; il restringimento è molto pericoloso e può portare a comportamenti spiacevoli. Probabilmente è meglio cogliere quei problemi all'inizio nella fase di compilazione. Per lo meno, il restringimento suggerisce che qualcuno non sta pensando troppo al codice.
Nota che i compilatori ti avvertiranno di questo genere di cose se il tuo livello di avviso è alto. Quindi, davvero, tutto ciò che fa è trasformare l'avvertimento in un errore forzato. Alcuni potrebbero dire che dovresti farlo comunque;)
C'è un altro motivo per non:
std::vector<int> v{100};
Cosa fa questo? Potrebbe creare un vector<int>
con un centinaio di elementi predefiniti. Oppure potrebbe creare un vector<int>
elemento con 1 il cui valore è 100
. Entrambi sono teoricamente possibili.
In realtà, fa quest'ultimo.
Perché? Gli elenchi di inizializzatori utilizzano la stessa sintassi dell'inizializzazione uniforme. Quindi ci devono essere alcune regole per spiegare cosa fare in caso di ambiguità. La regola è piuttosto semplice: se il compilatore può usare un costruttore di elenchi di inizializzatori con un elenco inizializzato con parentesi graffe, lo farà . Poiché vector<int>
ha un costruttore di elenchi di inizializzatori che richiede initializer_list<int>
e {100} potrebbe essere un valido initializer_list<int>
, deve essere quindi .
Per ottenere il costruttore del dimensionamento, è necessario utilizzare ()
invece di {}
.
Nota che se questo fosse un vector
qualcosa che non era convertibile in un numero intero, ciò non accadrebbe. Un initializer_list non si adatterebbe al costruttore dell'elenco di inizializzatori di quel vector
tipo, e quindi il compilatore sarebbe libero di scegliere dagli altri costruttori.