C’è una differenza tra l’inizializzazione della copia e l’inizializzazione diretta?

Supponiamo che io abbia questa funzione:

void my_test() { A a1 = A_factory_func(); A a2(A_factory_func()); double b1 = 0.5; double b2(0.5); A c1; A c2 = A(); A c3(A()); } 

In ogni gruppo, queste affermazioni sono identiche? O c’è una copia extra (possibilmente ottimizzabile) in alcune delle inizializzazioni?

Ho visto persone dire entrambe le cose. Si prega di citare il testo come prova. Aggiungi anche altri casi per favore.

Aggiornamento C ++ 17

In C ++ 17, il significato di A_factory_func() cambiato dalla creazione di un object temporaneo (C ++ < = 14) per specificare solo l'inizializzazione di qualsiasi oggetto a cui questa espressione è inizializzata (in senso lato) in C ++ 17. Questi oggetti sono le variabili create da una dichiarazione (come a1 ) o oggetti artificiali creati quando l’inizializzazione finisce per essere scartata altrimenti o se un object è necessario per il riferimento A_factory_func(); come, in A_factory_func(); un object viene creato artificialmente, chiamato “materializzazione temporanea”, perché A_factory_func() non ha una variabile o riferimento che altrimenti richiederebbe l’esistenza di un object). Inoltre, questi oggetti sono chiamati “oggetti risultato”.

Come esempi nel nostro caso, nel caso delle regole speciali a1 e a2 diciamo che in tali dichiarazioni, l’object risultato di un inizializzatore di prvalore dello stesso tipo di a1 è variabile a1 , e quindi A_factory_func() inizializza direttamente l’object a1 . Qualsiasi cast di stile funzionale intermedio non avrebbe alcun effetto, perché A_factory_func(another-prvalue) passa “semplicemente” l’object risultato del valore esterno per essere anche l’object risultato del valore interno.


 A a1 = A_factory_func(); A a2(A_factory_func()); 

Dipende dal tipo A_factory_func() . Presumo che restituisca un A – quindi sta facendo lo stesso – tranne che quando il costruttore di copie è esplicito, allora il primo fallirà. Leggi 8.6 / 14

 double b1 = 0.5; double b2(0.5); 

Questo sta facendo lo stesso perché è un tipo built-in (questo significa non un tipo di class qui). Leggi 8.6 / 14 .

 A c1; A c2 = A(); A c3(A()); 

Questo non sta facendo lo stesso. Il primo valore predefinito viene inizializzato se A è un non POD e non esegue alcuna inizializzazione per un POD (Leggi 8.6 / 9 ). La seconda copia inizializza: Valore: inizializza un valore temporaneo e quindi copia quel valore in c2 (Leggi 5.2.3 / 2 e 8.6 / 14 ). Ciò richiederà ovviamente un costruttore di copie non esplicito (leggi 8.6 / 14 e 12.3.1 / 3 e 13.3.1.3/1 ). Il terzo crea una dichiarazione di funzione per una funzione c3 che restituisce un A e che accetta un puntatore di funzione a una funzione che restituisce un A (Leggi 8.2 ).


Esecuzione dell’inizializzazione diretta e copia dell’inizializzazione

Mentre sembrano identici e dovrebbero fare lo stesso, queste due forms sono notevolmente diverse in alcuni casi. Le due forms di inizializzazione sono l’inizializzazione diretta e della copia:

 T t(x); T t = x; 

C’è un comportamento che possiamo attribuire a ciascuno di essi:

  • L’inizializzazione diretta si comporta come una chiamata di funzione a una funzione sovraccaricata: le funzioni, in questo caso, sono i costruttori di T (compresi quelli explicit ) e l’argomento è x . La risoluzione del sovraccarico troverà il miglior costruttore di corrispondenza e, quando necessario, richiederà qualsiasi conversione implicita.
  • Copia inizializzazione costruisce una sequenza di conversione implicita: prova a convertire x in un object di tipo T (Quindi può copiare su quell’object nell’object inizializzato, quindi è necessario anche un costruttore di copia, ma questo non è importante sotto)

Come vedete, l’ inizializzazione della copia è in qualche modo una parte dell’inizializzazione diretta in relazione a possibili conversioni implicite: mentre l’inizializzazione diretta ha tutti i costruttori disponibili per la chiamata, e inoltre può eseguire qualsiasi conversione implicita per abbinare i tipi di argomenti, inizializzare la copia puoi semplicemente impostare una sequenza di conversione implicita.

Ho provato molto e ho ottenuto il seguente codice per produrre un testo diverso per ciascuna di queste forms , senza utilizzare l’ovvio tramite costruttori explicit .

 #include  struct B; struct A { operator B(); }; struct B { B() { } B(A const&) { std::cout < < " "; } }; A::operator B() { std::cout < < " "; return B(); } int main() { A a; B b1(a); // 1) B b2 = a; // 2) } // output:   

Come funziona, e perché produce quel risultato?

  1. Inizializzazione diretta

    Prima non sa nulla della conversione. Proverà semplicemente a chiamare un costruttore. In questo caso, il seguente costruttore è disponibile ed è una corrispondenza esatta :

     B(A const&) 

    Non c’è conversione, tanto meno una conversione definita dall’utente, necessaria per chiamare quel costruttore (si noti che nessuna conversione di qualifica const avviene qui). E così l’inizializzazione diretta lo chiamerà.

  2. Copia l’inizializzazione

    Come detto sopra, l’inizializzazione della copia costruirà una sequenza di conversione quando a non ha tipo B o derivato da esso (che è chiaramente il caso qui). Quindi cercherà i modi per fare la conversione e troverà i seguenti candidati

     B(A const&) operator B(A&); 

    Si noti come ho riscritto la funzione di conversione: il tipo di parametro riflette il tipo di this puntatore, che in una funzione membro non const è non-const. Ora, chiamiamo questi candidati con x come argomento. Il vincitore è la funzione di conversione: perché se abbiamo due funzioni candidate che accettano un riferimento allo stesso tipo, allora vince la versione meno cost (è, tra l’altro, anche il meccanismo che preferisce le chiamate di funzioni membro non const -const oggetti).

    Nota che se cambiamo la funzione di conversione come funzione membro const, allora la conversione è ambigua (perché entrambi hanno un tipo di parametro di A const& poi): Il compilatore Comeau lo rifiuta correttamente, ma GCC lo accetta in modalità non-pedante. Passare a -pedantic fa sì che -pedantic anche l’avviso di ambiguità appropriato.

Spero che questo aiuti un po ‘a chiarire come queste due forms differiscono!

L’assegnazione è diversa dall’inizializzazione .

Entrambe le seguenti linee eseguono l’ inizializzazione . Una singola chiamata del costruttore è fatta:

 A a1 = A_factory_func(); // calls copy constructor A a1(A_factory_func()); // calls copy constructor 

ma non è equivalente a:

 A a1; // calls default constructor a1 = A_factory_func(); // (assignment) calls operator = 

Al momento non ho un testo per dimostrarlo ma è molto facile sperimentare:

 #include  using namespace std; class A { public: A() { cout < < "default constructor" << endl; } A(const A& x) { cout << "copy constructor" << endl; } const A& operator = (const A& x) { cout << "operator =" << endl; return *this; } }; int main() { A a; // default constructor A b(a); // copy constructor A c = a; // copy constructor c = b; // operator = return 0; } 

double b1 = 0.5; è una chiamata implicita del costruttore.

double b2(0.5); è una chiamata esplicita.

Guarda il seguente codice per vedere la differenza:

 #include  class sss { public: explicit sss( int ) { std::cout < < "int" << std::endl; }; sss( double ) { std::cout << "double" << std::endl; }; }; int main() { sss ddd( 7 ); // calls int constructor sss xxx = 7; // calls double constructor return 0; } 

Se la tua class non ha costruttori espliciti, le chiamate esplicite e implicite sono identiche.

Di nota:

[12.2 / 1] I Temporaries of class type are created in various contexts: ... and in some initializations (8.5).

Vale a dire, per l’inizializzazione della copia.

[12.8 / 15] When certain criteria are met, an implementation is allowed to omit the copy construction of a class object ...

In altre parole, un buon compilatore non creerà una copia per l’inizializzazione della copia quando può essere evitata; invece chiamerà direttamente il costruttore – cioè, proprio come per l’inizializzazione diretta.

In altre parole, l’inizializzazione della copia è come l’inizializzazione diretta nella maggior parte dei casi in cui è stato scritto codice comprensibile. Poiché l’inizializzazione diretta potenzialmente causa conversioni arbitrarie (e quindi probabilmente sconosciute), preferisco sempre utilizzare l’inizializzazione della copia, quando ansible. (Con il bonus che sembra effettivamente l’inizializzazione.)

Goriness tecnico: [12.2 / 1 cont dall’alto] Even when the creation of the temporary object is avoided (12.8), all the semantic restrictions must be respected as if the temporary object was created.

Sono contento di non scrivere un compilatore C ++.

Primo raggruppamento: dipende da cosa restituisce A_factory_func . La prima riga è un esempio di inizializzazione della copia , la seconda riga è l’inizializzazione diretta . Se A_factory_func restituisce un object A quindi sono equivalenti, entrambi chiamano il costruttore di copie per A , altrimenti la prima versione crea un valore di tipo A da un operatore di conversione disponibile per il tipo restituito di A_factory_func o i costruttori A appropriati, quindi chiama il valore restituito copia il costruttore per build a1 da questo temporaneo. La seconda versione tenta di trovare un costruttore adatto che prenda qualsiasi A_factory_func restituisca A_factory_func , o che prende qualcosa a cui il valore di ritorno può essere convertito implicitamente.

Secondo raggruppamento: vale esattamente la stessa logica, tranne per il fatto che i tipi incorporati non hanno costruttori esotici quindi sono, in pratica, identici.

Terzo raggruppamento: c1 è inizializzato di default, c2 è inizializzato dalla copia da un valore inizializzato temporaneo. Tutti i membri di c1 che hanno il tipo di pod (o membri di membri, ecc. Ecc.) Potrebbero non essere inizializzati se i costruttori predefiniti dall’utente (se presenti) non li inizializzano esplicitamente. Per c2 , dipende dal fatto che esista un costruttore di copie fornito dall’utente e se esso inizializzi appropriatamente quei membri, ma i membri del temporaneo verranno tutti inizializzati (inizializzati a zero se non altrimenti inizializzati in modo esplicito). Come litb spotted, c3 è una trappola. In realtà è una dichiarazione di funzione.

Rispondere con rispetto a questa parte:

A c2 = A (); A c3 (A ());

Poiché la maggior parte delle risposte sono pre-c ++ 11 sto aggiungendo ciò che c ++ 11 ha da dire su questo:

Uno specificatore di tipo semplice (7.1.6.2) o un identificatore di tipo (14.6) seguito da una lista di espressioni parentesi crea un valore del tipo specificato data l’elenco di espressioni. Se l’elenco di espressioni è una singola espressione, l’espressione di conversione del tipo è equivalente (in definizione e se definito nel significato) all’espressione di cast corrispondente (5.4). Se il tipo specificato è un tipo di class, il tipo di class deve essere completo. Se l’elenco di espressioni specifica più di un singolo valore, il tipo deve essere una class con un costruttore adeguatamente dichiarato (8.5, 12.1) e l’espressione T (x1, x2, …) è equivalente in effetti alla dichiarazione T t (x1, x2, …); per qualche variabile temporanea inventata t, con il risultato che è il valore di t come valore nominale.

Quindi l’ottimizzazione o meno sono equivalenti secondo lo standard. Si noti che questo è in accordo con ciò che altre risposte hanno menzionato. Basta citare ciò che lo standard ha da dire per ragioni di correttezza.

Molti di questi casi sono soggetti all’implementazione di un object, quindi è difficile dare una risposta concreta.

Considera il caso

 A a = 5; A a(5); 

In questo caso assumendo un operatore di assegnazione e un costruttore di inizializzazione corretti che accettano un singolo argomento intero, il modo in cui implemento tali metodi influenza il comportamento di ciascuna riga. Tuttavia, è prassi comune che uno di questi chiami l’altro nell’implementazione per eliminare il codice duplicato (sebbene in un caso così semplice non ci sarebbe un vero scopo).

Modifica: come accennato in altre risposte, la prima riga chiamerà infatti il ​​costruttore di copie. Considerare i commenti relativi all’operatore di assegnazione come comportamento relativo a un compito autonomo.

Detto questo, il modo in cui il compilatore ottimizza il codice avrà quindi il suo impatto. Se ho il costruttore di inizializzazione che chiama l’operatore “=” – se il compilatore non fa ottimizzazioni, la linea superiore eseguirà 2 salti invece di uno nella riga inferiore.

Ora, per le situazioni più comuni, il compilatore ottimizzerà attraverso questi casi ed eliminerà questo tipo di inefficienze. Così efficacemente tutte le diverse situazioni che descrivi si presenteranno allo stesso modo. Se vuoi vedere esattamente cosa si sta facendo, puoi guardare il codice object o un output di assemblaggio del tuo compilatore.

Puoi vedere la sua differenza in tipi di costruttori explicit e implicit durante l’inizializzazione di un object:

Classi :

 class A { A(int) { } // converting constructor A(int, int) { } // converting constructor (C++11) }; class B { explicit B(int) { } explicit B(int, int) { } }; 

E nella funzione main :

 int main() { A a1 = 1; // OK: copy-initialization selects A::A(int) A a2(2); // OK: direct-initialization selects A::A(int) A a3 {4, 5}; // OK: direct-list-initialization selects A::A(int, int) A a4 = {4, 5}; // OK: copy-list-initialization selects A::A(int, int) A a5 = (A)1; // OK: explicit cast performs static_cast // B b1 = 1; // error: copy-initialization does not consider B::B(int) B b2(2); // OK: direct-initialization selects B::B(int) B b3 {4, 5}; // OK: direct-list-initialization selects B::B(int, int) // B b4 = {4, 5}; // error: copy-list-initialization does not consider B::B(int,int) B b5 = (B)1; // OK: explicit cast performs static_cast } 

Per impostazione predefinita, un costruttore è implicit modo da avere due modi per inizializzarlo:

 A a1 = 1; // this is copy initialization A a2(2); // this is direct initialization 

E definendo una struttura come explicit solo tu hai un modo come diretto:

 B b2(2); // this is direct initialization B b5 = (B)1; // not problem if you either use of assign to initialize and cast it as static_cast