L'inizializzazione uniforme di C ++ 11 sostituisce la sintassi del vecchio stile?


172

Capisco che l'inizializzazione uniforme di C ++ 11 risolve alcune ambiguità sintattiche nel linguaggio, ma in molte presentazioni di Bjarne Stroustrup (in particolare quelle durante i colloqui di GoingNative 2012), i suoi esempi usano principalmente questa sintassi ora ogni volta che costruisce oggetti.

Si consiglia ora di utilizzare l'inizializzazione uniforme in tutti i casi? Quale dovrebbe essere l'approccio generale per questa nuova funzionalità per quanto riguarda lo stile di codifica e l'uso generale? Quali sono alcuni motivi per non usarlo?

Nota che nella mia mente sto pensando principalmente alla costruzione di oggetti come mio caso d'uso, ma se ci sono altri scenari da considerare per favore fatemelo sapere.


Questo potrebbe essere un argomento meglio discusso su Programmers.se. Sembra inclinarsi verso il lato buono soggettivo.
Nicol Bolas,

6
@NicolBolas: D'altra parte, la tua risposta eccellente potrebbe essere un ottimo candidato per il tag c ++ - faq. Non credo che abbiamo già avuto una spiegazione per questo post.
Matthieu M.

Risposte:


233

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 vec3due 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::stringsa 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::stringcostruttore 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 autodi 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 Bare il foovalore 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 vectorqualcosa 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 vectortipo, e quindi il compilatore sarebbe libero di scegliere dagli altri costruttori.


11
+1 inchiodato. Sto eliminando la mia risposta poiché la tua affronta tutti gli stessi punti in modo molto più dettagliato.
R. Martinho Fernandes,

21
L'ultimo punto è il motivo per cui mi piacerebbe davvero std::vector<int> v{100, std::reserve_tag};. Allo stesso modo con std::resize_tag. Attualmente sono necessari due passaggi per riservare lo spazio vettoriale.
Xeo,

6
@NicolBolas - Due punti: ho pensato che il problema con l'angoscia parse fosse foo (), non Bar (). In altre parole, se lo facessi int foo(10), non incontreresti lo stesso problema? In secondo luogo, un altro motivo per non usarlo sembra essere più un problema di over engineering, ma cosa succede se costruiamo tutti i nostri oggetti usando {}, ma un giorno dopo lungo la strada aggiungo un costruttore per le liste di inizializzazione? Ora tutte le mie dichiarazioni di costruzione si trasformano in dichiarazioni di elenco di inizializzatori. Sembra molto fragile in termini di refactoring. Qualche commento su questo?
void.pointer

7
@RobertDailey: "Se lo facessi int foo(10), non incontreresti lo stesso problema?" Il numero 10 è un valore letterale intero e un valore letterale intero non può mai essere un tipo di nome. L'analisi irritante deriva dal fatto che Bar()potrebbe essere un nome di battesimo o un valore temporaneo. Questo è ciò che crea l'ambiguità per il compilatore.
Nicol Bolas,

8
unpleasant behavior- c'è un nuovo termine standard da ricordare:>
visto il

64

Non sarò d'accordo con la sezione delle risposte di Nicol Bolas Riduce al minimo i nomi di caratteri ridondanti . Poiché il codice viene scritto una volta e letto più volte, dovremmo cercare di ridurre al minimo il tempo necessario per leggere e comprendere il codice, non il tempo necessario per scrivere il codice. Cercare di minimizzare semplicemente la digitazione sta cercando di ottimizzare la cosa sbagliata.

Vedi il seguente codice:

vec3 GetValue()
{
  <lots and lots of code here>
  ...
  return {x, y, z};
}

Qualcuno che legge il codice sopra per la prima volta probabilmente non capirà immediatamente la dichiarazione di ritorno, perché quando raggiungerà quella linea, avrà dimenticato il tipo di ritorno. Ora dovrà tornare alla firma della funzione o utilizzare alcune funzionalità IDE per vedere il tipo restituito e comprendere appieno l'istruzione return.

E anche qui non è facile per qualcuno che legge il codice per la prima volta capire cosa viene effettivamente costruito:

void DoSomething(const std::string &str);
...
const char* strValue = ...;
size_t strLen = ...;

DoSomething({strValue, strLen});

Il codice sopra si interromperà quando qualcuno decide che DoSomething dovrebbe supportare anche qualche altro tipo di stringa e aggiunge questo sovraccarico:

void DoSomething(const CoolStringType& str);

Se CoolStringType sembra avere un costruttore che accetta un const char * e un size_t (proprio come std :: string fa), la chiamata a DoSomething ({strValue, strLen}) causerà un errore di ambiguità.

La mia risposta alla domanda attuale:
No, l'inizializzazione uniforme non dovrebbe essere considerata come un sostituto della sintassi del costruttore vecchio stile.

E il mio ragionamento è questo:
se due affermazioni non hanno lo stesso tipo di intenzione, non dovrebbero apparire uguali. Esistono due tipi di nozioni di inizializzazione dell'oggetto:
1) Prendi tutti questi elementi e versali in questo oggetto che sto inizializzando.
2) Costruisci questo oggetto usando questi argomenti che ho fornito come guida.

Esempi di utilizzo della nozione n. 1:

struct Collection
{
    int first;
    char second;
    double third;
};

Collection c {1, '2', 3.0};
std::array<int, 3> a {{ 1, 2, 3 }};
std::map<int, char> m { {1, '1'}, {2, '2'}, {3, '3'} };

Esempio di utilizzo della nozione n. 2:

class Stairs
{
    std::vector<float> stepHeights;

public:
    Stairs(float initHeight, int numSteps, float stepHeight)
    {
        float height = initHeight;

        for (int i = 0; i < numSteps; ++i)
        {
            stepHeights.push_back(height);
            height += stepHeight;
        }
    }
};

Stairs s (2.5, 10, 0.5);

Penso che sia una brutta cosa che il nuovo standard permetta alle persone di inizializzare le scale in questo modo:

Stairs s {2, 4, 6};

... perché ciò offusca il significato del costruttore. Inizializzazione del genere sembra proprio la nozione n. 1, ma non lo è. Non sta riversando tre diversi valori di gradini negli oggetti, anche se sembra che lo siano. E, cosa ancora più importante, se è stata pubblicata un'implementazione della libreria di Stairs come sopra e i programmatori lo hanno utilizzato, e quindi se l'implementatore della libreria in seguito aggiunge un costruttore di inizializzatore_elenco a Stairs, tutto il codice che ha utilizzato Stairs con Uniform Initialization La sintassi si romperà.

Penso che la comunità C ++ dovrebbe concordare una convenzione comune su come viene utilizzata l'inizializzazione uniforme, cioè uniformemente su tutte le inizializzazioni o, come suggerisco fortemente, separando queste due nozioni di inizializzazione e chiarendo così l'intenzione del programmatore al lettore di il codice.


Dopo:
ecco ancora un altro motivo per cui non dovresti pensare all'inizializzazione uniforme come sostituto della vecchia sintassi e perché non puoi usare la notazione di parentesi graffa per tutte le inizializzazioni:

Supponiamo che la tua sintassi preferita per fare una copia sia:

T var1;
T var2 (var1);

Ora pensi di dover sostituire tutte le inizializzazioni con la nuova sintassi del controvento in modo da poter essere (e il codice apparirà) più coerente. Ma la sintassi usando le parentesi graffe non funziona se il tipo T è un aggregato:

T var2 {var1}; // fails if T is std::array for example

48
Se hai "<un sacco di codice qui>" il tuo codice sarà difficile da capire indipendentemente dalla sintassi.
Kevin Cline il

8
Oltre a IMO è dovere del tuo IDE informarti di che tipo sta tornando (ad es. Passando con il mouse). Ovviamente se non usi un IDE, ti sei preso il peso :)
abergmeier

4
@TommiT Sono d'accordo con alcune parti di ciò che dici. Tuttavia, nello stesso spirito come autocontro dichiarazione di tipo esplicito dibattito, direi di un equilibrio: inizializzatori uniformi roccia abbastanza grande tempo nel modello di meta-programmazione di situazioni in cui il tipo di solito è abbastanza evidente in ogni caso. -> decltype(....)Eviterà di ripetere il tuo dannato complicato per l'incantesimo, ad esempio per semplici modelli di funzioni online (mi ha fatto piangere).
visto il

5
" Ma la sintassi che utilizza le parentesi graffe non funziona se il tipo T è un aggregato: " Si noti che questo è un difetto riportato nel comportamento standard, piuttosto che intenzionale, previsto.
Nicol Bolas,

5
"Ora dovrà tornare alla firma della funzione" se devi scorrere, la tua funzione è troppo grande.
Miles Rout,

-3

Se i tuoi costruttori merely copy their parametersnelle rispettive variabili di classe in exactly the same orderin cui sono dichiarati all'interno della classe, l'uso dell'inizializzazione uniforme può eventualmente essere più veloce (ma può anche essere assolutamente identico) che chiamare il costruttore.

Ovviamente, questo non cambia il fatto che devi sempre dichiarare il costruttore.


2
Perché dici che può essere più veloce?
jbcoe,

Questo non è corretto Non v'è alcun obbligo di dichiarare un costruttore: struct X { int i; }; int main() { X x{42}; }. È inoltre errato che l'inizializzazione uniforme possa essere più rapida dell'inizializzazione del valore.
Tim
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.