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


244

Supponiamo di avere 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 raggruppamento, 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.


1
E c'è il quarto caso discusso da @JohannesSchaub - A c1; A c2 = c1; A c3(c1);.
Dan Nissenbaum,

1
Solo una nota del 2018: le regole sono cambiate in C ++ 17 , vedi, ad esempio, qui . Se la mia comprensione è corretta, in C ++ 17, entrambe le affermazioni sono effettivamente le stesse (anche se il ctor copia è esplicito). Inoltre, se l'espressione di init fosse di tipo diverso da A, l'inizializzazione della copia non richiederebbe l'esistenza del costatore copia / sposta. Questo è il motivo per cui std::atomic<int> a = 1;va bene in C ++ 17 ma non prima.
Daniel Langr

Risposte:


246

Aggiornamento C ++ 17

In C ++ 17, il significato di è A_factory_func()cambiato dalla creazione di un oggetto temporaneo (C ++ <= 14) alla specifica dell'inizializzazione di qualsiasi oggetto a cui questa espressione è inizializzata (parlando in modo approssimativo) in C ++ 17. Questi oggetti (chiamati "oggetti risultato") sono le variabili create da una dichiarazione (like a1), oggetti artificiali creati quando l'inizializzazione finisce per essere scartata o se è necessario un oggetto per l'associazione di riferimento (come, in A_factory_func();. Nell'ultimo caso, un oggetto viene creato artificialmente, chiamato "materializzazione temporanea", perché A_factory_func()non ha una variabile o un riferimento che altrimenti richiederebbe l'esistenza di un oggetto).

Come esempi nel nostro caso, nel caso di a1e a2regole speciali si dice che in tali dichiarazioni, l'oggetto risultato di un inizializzatore di valori dello stesso tipo a1è variabile a1e quindi A_factory_func()inizializza direttamente l'oggetto a1. Qualsiasi cast di stile funzionale intermedio non avrebbe alcun effetto, perché A_factory_func(another-prvalue)semplicemente "attraversa" l'oggetto risultato del valore esterno per essere anche l'oggetto risultato del valore interno.


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

Dipende dal tipo A_factory_func()restituito. Suppongo che ritorni 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 incorporato (questo qui non significa un tipo di classe). Leggi 8.6 / 14 .

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

Questo non sta facendo lo stesso. Il primo predefinito inizializza if se non Aè un POD e non esegue alcuna inizializzazione per un POD (Leggi 8.6 / 9 ). La seconda copia inizializza: Value-inizializza un temporaneo e quindi copia quel valore in c2(Leggi 5.2.3 / 2 e 8.6 / 14 ). Ciò ovviamente richiederà 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 c3che restituisce un Ae che accetta un puntatore a una funzione che restituisce un A(Leggi 8.2 ).


Approfondimento sulle inizializzazioni Direct e Copia inizializzazione

Mentre sembrano identici e si suppone che facciano lo stesso, queste due forme sono notevolmente diverse in alcuni casi. Le due forme di inizializzazione sono l'inizializzazione diretta e 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 sovraccarica: le funzioni, in questo caso, sono i costruttori di T(inclusi explicitquelli), e l'argomento è x. La risoluzione del sovraccarico troverà il miglior costruttore corrispondente e, quando necessario, eseguirà qualsiasi conversione implicita richiesta.
  • L'inizializzazione della copia crea una sequenza di conversione implicita: tenta di convertire xin un oggetto di tipo T. (Quindi può copiare su quell'oggetto nell'oggetto inizializzato, quindi è necessario anche un costruttore di copie - ma questo non è importante di seguito)

Come vedi, l' inizializzazione della copia è in qualche modo una parte dell'inizializzazione diretta per quanto riguarda le possibili conversioni implicite: mentre l'inizializzazione diretta ha tutti i costruttori disponibili da chiamare, e inoltre può fare qualsiasi conversione implicita che deve abbinare i tipi di argomento, copia l'inizializzazione può semplicemente impostare una sequenza di conversione implicita.

Ho provato duramente e ho ottenuto il seguente codice per generare un testo diverso per ognuna di quelle forme , senza usare "ovvi" tramite i explicitcostruttori.

#include <iostream>
struct B;
struct A { 
  operator B();
};

struct B { 
  B() { }
  B(A const&) { std::cout << "<direct> "; }
};

A::operator B() { std::cout << "<copy> "; return B(); }

int main() { 
  A a;
  B b1(a);  // 1)
  B b2 = a; // 2)
}
// output: <direct> <copy>

Come funziona e perché genera questo risultato?

  1. Inizializzazione diretta

    Prima non sa nulla della conversione. Tenterà semplicemente di chiamare un costruttore. In questo caso, è disponibile il seguente costruttore 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 qui non avviene nemmeno una conversione di qualifica const). E così l'inizializzazione diretta lo chiamerà.

  2. Copia inizializzazione

    Come detto sopra, l'inizializzazione della copia costruirà una sequenza di conversione quando anon ha digitato Bo derivato da essa (che è chiaramente il caso qui). Quindi cercherà i modi per fare la conversione e troverà i seguenti candidati

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

    Nota come riscrivo la funzione di conversione: il tipo di parametro riflette il tipo di thispuntatore, che in una funzione membro non const è non-const. Ora, chiamiamo questi candidati xcome argomento. Il vincitore è la funzione di conversione: perché se abbiamo due funzioni candidate che accettano entrambe un riferimento allo stesso tipo, allora vince la versione meno const (questo è, tra l'altro, anche il meccanismo che preferisce la funzione membro non const richiede -const oggetti).

    Nota che se cambiamo la funzione di conversione in una funzione membro const, allora la conversione è ambigua (perché entrambi hanno un tipo di parametro di A const&allora): il compilatore Comeau la rifiuta correttamente, ma GCC la accetta in modalità non pedante. Il passaggio a -pedanticrende anche l'emissione del giusto avviso di ambiguità.

Spero che questo aiuti in qualche modo a chiarire in che modo queste due forme differiscono!


Wow. Non mi ero nemmeno reso conto della dichiarazione di funzione. Devo praticamente accettare la tua risposta solo per essere l'unico a saperlo. C'è un motivo per cui le dichiarazioni di funzioni funzionano in questo modo? Sarebbe meglio se c3 fosse trattato in modo diverso all'interno di una funzione.
rlbond,

4
Bah, scusa gente, ma ho dovuto rimuovere il mio commento e postarlo di nuovo, a causa del nuovo motore di formattazione: è perché nei parametri di funzione, R() == R(*)()e T[] == T*. Ossia, i tipi di funzione sono tipi di puntatore di funzione e i tipi di array sono tipi di puntatore a elemento. Questo fa schifo. Può essere aggirato da A c3((A()));(parentesi attorno all'espressione).
Johannes Schaub - litb

4
Posso chiederti cosa significa "'Leggi 8.5 / 14'"? A cosa si riferisce? Un libro? Un capitolo? Un sito web?
AzP,

9
@AzP molte persone su SO spesso vogliono riferimenti alle specifiche C ++, ed è quello che ho fatto qui, in risposta alla richiesta di rlbond "Per favore, cita il testo come prova". Non voglio citare le specifiche, dal momento che questo gonfia la mia risposta ed è molto più lavoro per tenermi aggiornato (ridondanza).
Johannes Schaub - litb

1
@luca vi consiglio di iniziare una nuova domanda per quel modo che altri possano beneficiare della risposta la gente dà lontato
Johannes Schaub - litb

49

L'assegnazione è diversa dall'inizializzazione .

Entrambe le righe seguenti eseguono l' inizializzazione . Viene eseguita una singola chiamata del costruttore:

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

ma non equivale 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 <iostream>
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;
}

2
Buona referenza: "Il linguaggio di programmazione C ++, Edizione speciale" di Bjarne Stroustrup, sezione 10.4.4.1 (pagina 245). Descrive l'inizializzazione della copia e l'assegnazione della copia e perché sono fondamentalmente diversi (sebbene entrambi utilizzino l'operatore = come sintassi).
Naaff,

Minor nit, ma in realtà non mi piace quando la gente dice che "A a (x)" e "A a = x" sono uguali. Rigorosamente non lo sono. In molti casi faranno esattamente la stessa cosa, ma è possibile creare esempi in cui, a seconda dell'argomento, vengono effettivamente chiamati costruttori diversi.
Richard Corden,

Non sto parlando di "equivalenza sintattica". Semanticamente, entrambi i modi di inizializzazione sono gli stessi.
Mehrdad Afshari,

@MehrdadAfshari Nel codice di risposta di Johannes ottieni un output diverso in base a quale delle due che usi.
Brian Gordon,

1
@BrianGordon Sì, hai ragione. Non sono equivalenti. Avevo affrontato il commento di Richard nella mia modifica molto tempo fa.
Mehrdad Afshari,

22

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

double b2(0.5); è una chiamata esplicita.

Guarda il seguente codice per vedere la differenza:

#include <iostream>
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 classe non ha costruttori espliciti, le chiamate esplicite e implicite sono identiche.


5
+1. Bella risposta. Buono anche a notare la versione esplicita. A proposito, è importante notare che non è possibile avere entrambe le versioni di un sovraccarico di un singolo costruttore contemporaneamente. Quindi, non riuscirebbe a compilare nel caso esplicito. Se entrambi si compilano, devono comportarsi in modo simile.
Mehrdad Afshari,

4

Primo raggruppamento: dipende da cosa A_factory_funcritorna. La prima riga è un esempio di inizializzazione della copia , la seconda riga è l'inizializzazione diretta . Se A_factory_funcrestituisce un Aoggetto, allora sono equivalenti, entrambi chiamano il costruttore di copie per A, altrimenti la prima versione crea un valore di tipo Ada un operatore di conversione disponibile per il tipo di ritorno A_factory_funco Acostruttori appropriati , quindi chiama il costruttore di copie per costruire a1da questo temporaneo. La seconda versione tenta di trovare un costruttore adatto che prenda qualsiasi valore A_factory_funcrestituito o che richieda qualcosa in cui il valore restituito possa 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: c1viene inizializzato per impostazione predefinita, c2viene inizializzato per la copia da un valore inizializzato temporaneamente. Qualsiasi membro c1con tipo pod (o membri dei membri, ecc. Ecc.) Non può essere inizializzato se l'utente ha fornito i costruttori predefiniti (se presenti) non li inizializzano esplicitamente. Infatti c2, dipende se esiste un costruttore di copie fornito dall'utente e se inizializza in modo appropriato quei membri, ma i membri del temporaneo verranno tutti inizializzati (zero inizializzato se non altrimenti inizializzato esplicitamente). Come macchiato, c3è una trappola. In realtà è una dichiarazione di funzione.


4

Di nota:

[12.2 / 1] 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 evitato; invece chiamerà direttamente direttamente il costruttore - cioè, proprio come per l'inizializzazione diretta.

In altre parole, l'inizializzazione della copia è proprio come l'inizializzazione diretta nella maggior parte dei casi <opinion> in cui è stato scritto un codice comprensibile. Poiché l'inizializzazione diretta provoca potenzialmente conversioni arbitrarie (e quindi probabilmente sconosciute), preferisco usare sempre l'inizializzazione della copia quando possibile. (Con il vantaggio che in realtà sembra un'inizializzazione.) </opinion>

Follia tecnica: [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 ++.


4

Puoi vedere la sua differenza nei tipi di costruttore explicite implicitquando inizializzi un oggetto:

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 main funzione:

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 è implicitcosì che hai due modi per inizializzarlo:

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

E definendo una struttura come explicithai solo un modo 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

3

Rispondere rispetto a questa parte:

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

Poiché la maggior parte delle risposte è pre-c ++ 11, sto aggiungendo ciò che c ++ 11 ha da dire al riguardo:

Un identificatore di tipo semplice (7.1.6.2) o un identificatore di nome di tipo (14.6) seguito da un elenco di espressioni tra parentesi costruisce un valore del tipo specificato dato l'elenco di espressioni. Se l'elenco di espressioni è una singola espressione, l'espressione di conversione del tipo è equivalente (nella definizione e se definita nel significato) all'espressione di cast corrispondente (5.4). Se il tipo specificato è un tipo di classe, il tipo di classe deve essere completo. Se l'elenco di espressioni specifica più di un singolo valore, il tipo deve essere una classe con un costruttore adeguatamente dichiarato (8.5, 12.1) e l'espressione T (x1, x2, ...) è in effetti equivalente alla dichiarazione T t (x1, x2, ...); per alcune variabili temporanee inventate t, con il risultato è il valore di t come valore.

Quindi l'ottimizzazione o no sono equivalenti secondo lo standard. Si noti che ciò è conforme a quanto menzionato da altre risposte. Sto solo citando ciò che lo standard ha da dire per motivi di correttezza.


Nessuno dei "esempi di espressioni" specifica più di un singolo valore ". In che modo è pertinente?
underscore_d

0

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

Considera il caso

A a = 5;
A a(5);

In questo caso, assumendo un operatore di assegnazione adeguato e inizializzando il costruttore che accetta un singolo argomento intero, il modo in cui implemento tali metodi influenza il comportamento di ogni riga. È prassi comune, tuttavia, 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 menzionato in altre risposte, la prima riga chiamerà infatti il ​​costruttore della copia. Considera i commenti relativi all'operatore di assegnazione come comportamento relativo a un'assegnazione autonoma.

Detto questo, il modo in cui il compilatore ottimizza il codice avrà il suo impatto. Se ho il costruttore di inizializzazione che chiama l'operatore "=" - se il compilatore non esegue ottimizzazioni, la riga superiore eseguirà 2 salti anziché uno nella riga inferiore.

Ora, per le situazioni più comuni, il compilatore ottimizzerà questi casi ed eliminerà questo tipo di inefficienze. Così efficacemente tutte le diverse situazioni che descriverai saranno le stesse. Se vuoi vedere esattamente cosa viene fatto, puoi guardare il codice oggetto o l'output di un assieme del tuo compilatore.


Non è un'ottimizzazione . Il compilatore deve chiamare il costruttore allo stesso modo in entrambi i casi. Di conseguenza, nessuno di loro verrà compilato se lo hai operator =(const int)e no A(const int). Vedi la risposta di @ jia3ep per maggiori dettagli.
Mehrdad Afshari,

Credo che tu abbia ragione in realtà. Tuttavia, verrà compilato correttamente utilizzando un costruttore di copie predefinito.
dborba,

Inoltre, come ho già detto, è pratica comune che un costruttore di copie chiami un operatore di assegnazione, a quel punto entrano in gioco le ottimizzazioni del compilatore.
dborba,

0

Questo è tratto dal linguaggio di programmazione C ++ di Bjarne Stroustrup:

Un'inizializzazione con an = è considerata un'inizializzazione della copia . In linea di principio, una copia dell'inizializzatore (l'oggetto da cui stiamo copiando) viene posizionata nell'oggetto inizializzato. Tuttavia, tale copia può essere ottimizzata via (scelta) e un'operazione di spostamento (basata sulla semantica di spostamento) può essere utilizzata se l'inizializzatore è un valore. Tralasciando il = si rende esplicita l'inizializzazione. L'inizializzazione esplicita è nota come inizializzazione diretta .

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.