Gibt es einen Unterschied zwischen der Kopierinitialisierung und der direkten Initialisierung?

244

Angenommen, ich habe diese Funktion:

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());
}

Sind diese Aussagen in jeder Gruppierung identisch? Oder gibt es in einigen Initialisierungen eine zusätzliche (möglicherweise optimierbare) Kopie?

Ich habe Leute gesehen, die beide Dinge gesagt haben. Bitte zitieren Sie den Text als Beweis. Fügen Sie bitte auch andere Fälle hinzu.

rlbond
quelle
1
Und es gibt den vierten Fall, der von @JohannesSchaub - diskutiert wird A c1; A c2 = c1; A c3(c1);.
Dan Nissenbaum
1
Nur ein Hinweis für 2018: Die Regeln haben sich in C ++ 17 geändert , siehe z . B. hier . Wenn mein Verständnis korrekt ist, sind in C ++ 17 beide Anweisungen praktisch gleich (auch wenn der Kopiercode explizit ist). Wenn der Init-Ausdruck von einem anderen Typ wäre als A, würde die Kopierinitialisierung außerdem das Vorhandensein eines Kopier- / Verschiebungskonstruktors erfordern. Aus diesem Grund std::atomic<int> a = 1;ist es in C ++ 17 in Ordnung, aber nicht vorher.
Daniel Langr

Antworten:

246

C ++ 17 Update

In C ++ 17 wurde die Bedeutung von A_factory_func()von der Erstellung eines temporären Objekts (C ++ <= 14) geändert, um lediglich die Initialisierung des Objekts anzugeben, für das dieser Ausdruck in C ++ 17 (lose gesagt) initialisiert wird. Diese Objekte (als "Ergebnisobjekte" bezeichnet) sind die Variablen, die durch eine Deklaration (wie a1) erstellt wurden, künstliche Objekte, die erstellt wurden, wenn die Initialisierung verworfen wurde, oder wenn ein Objekt für die Referenzbindung benötigt wird (wie z A_factory_func();. B. in . Im letzten Fall Ein Objekt wird künstlich erstellt, was als "temporäre Materialisierung" bezeichnet wird, da A_factory_func()es keine Variable oder Referenz gibt, für deren Existenz sonst ein Objekt erforderlich wäre.

Als Beispiele in unserem Fall besagen im Fall von a1und a2Sonderregeln, dass in solchen Deklarationen das Ergebnisobjekt eines prvalue-Initialisierers vom gleichen Typ wie a1variabel a1ist und daher A_factory_func()das Objekt direkt initialisiert a1. Eine Zwischenbesetzung im funktionalen Stil hätte keine Auswirkung, da A_factory_func(another-prvalue)nur das Ergebnisobjekt des äußeren Wertes "durchlaufen" wird, um auch das Ergebnisobjekt des inneren Wertes zu sein.


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

Hängt davon ab, welcher Typ A_factory_func()zurückgibt. Ich gehe davon aus, dass es ein zurückgibt A- dann macht es dasselbe - außer dass, wenn der Kopierkonstruktor explizit ist, der erste fehlschlägt. Lesen Sie 8.6 / 14

double b1 = 0.5;
double b2(0.5);

Dies geschieht genauso, da es sich um einen integrierten Typ handelt (dies bedeutet hier keinen Klassentyp). Lesen Sie 8.6 / 14 .

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

Dies ist nicht dasselbe. Der erste Standard initialisiert, wenn Aes sich um einen Nicht-POD handelt, und führt keine Initialisierung für einen POD durch (Lesen Sie 8.6 / 9 ). Die zweite Kopie wird initialisiert: Der Wert initialisiert eine temporäre Kopie und kopiert diesen Wert dann in c2(Lesen Sie 5.2.3 / 2 und 8.6 / 14 ). Dies erfordert natürlich einen nicht expliziten Kopierkonstruktor (Lesen Sie 8.6 / 14 und 12.3.1 / 3 und 13.3.1.3/1 ). Die dritte Methode erstellt eine Funktionsdeklaration für eine Funktion c3, die eine zurückgibt Aund einen Funktionszeiger auf eine Funktion zurückgibt , die a zurückgibt A(Read 8.2 ).


Eintauchen in Initialisierungen Direkt- und Kopierinitialisierung

Während sie identisch aussehen und dasselbe tun sollen, unterscheiden sich diese beiden Formen in bestimmten Fällen erheblich. Die beiden Formen der Initialisierung sind Direkt- und Kopierinitialisierung:

T t(x);
T t = x;

Es gibt ein Verhalten, das wir jedem von ihnen zuschreiben können:

  • Die direkte Initialisierung verhält sich wie ein Funktionsaufruf einer überladenen Funktion: Die Funktionen sind in diesem Fall die Konstruktoren von T(einschließlich explicitderjenigen), und das Argument lautet x. Die Überlastungsauflösung findet den am besten passenden Konstruktor und führt bei Bedarf alle erforderlichen impliziten Konvertierungen durch.
  • Die Kopierinitialisierung erstellt eine implizite Konvertierungssequenz: Sie versucht, xin ein Objekt vom Typ zu konvertieren T. (Es kann dann über dieses Objekt in das zu initialisierende Objekt kopiert werden, sodass auch ein Kopierkonstruktor benötigt wird - dies ist jedoch unten nicht wichtig.)

Wie Sie sehen, ist die Kopierinitialisierung in gewisser Weise Teil der direkten Initialisierung im Hinblick auf mögliche implizite Konvertierungen: Während bei der direkten Initialisierung alle Konstruktoren zum Aufrufen verfügbar sind und darüber hinaus jede implizite Konvertierung durchgeführt werden kann, die zum Abgleichen der Argumenttypen erforderlich ist, wird die Kopierinitialisierung durchgeführt kann nur eine implizite Konvertierungssequenz einrichten.

Ich habe mich sehr bemüht und den folgenden Code erhalten, um für jedes dieser Formulare einen anderen Text auszugeben , ohne das "Offensichtliche" durch explicitKonstruktoren zu verwenden.

#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>

Wie funktioniert es und warum gibt es dieses Ergebnis aus?

  1. Direkte Initialisierung

    Es weiß zunächst nichts über Konvertierung. Es wird nur versucht, einen Konstruktor aufzurufen. In diesem Fall ist der folgende Konstruktor verfügbar und stimmt genau überein :

    B(A const&)

    Es ist keine Konvertierung erforderlich, geschweige denn eine benutzerdefinierte Konvertierung, die zum Aufrufen dieses Konstruktors erforderlich ist (beachten Sie, dass auch hier keine Konvertierung der Konstantenqualifizierung stattfindet). Die direkte Initialisierung nennt es also.

  2. Initialisierung kopieren

    Wie oben erwähnt, erstellt die Kopierinitialisierung eine Konvertierungssequenz, wenn asie nicht typisiert Boder davon abgeleitet wurde (was hier eindeutig der Fall ist). Es wird also nach Möglichkeiten suchen, die Konvertierung durchzuführen, und die folgenden Kandidaten finden

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

    Beachten Sie, wie ich die Konvertierungsfunktion umgeschrieben habe: Der Parametertyp spiegelt den Typ des thisZeigers wider , der in einer Nicht-Konstanten-Elementfunktion auf Nicht-Konstante steht. Nun nennen wir diese Kandidaten xals Argument. Der Gewinner ist die Konvertierungsfunktion: Wenn wir zwei Kandidatenfunktionen haben, die beide einen Verweis auf denselben Typ akzeptieren, gewinnt die Version mit weniger Konstanten (dies ist übrigens auch der Mechanismus, der Nicht-Konstanten-Mitgliedsfunktionen bevorzugt, ruft Nicht auf -const Objekte).

    Beachten Sie, dass die Konvertierung mehrdeutig ist, wenn wir die Konvertierungsfunktion in eine const-Member-Funktion ändern (da beide einen Parametertyp von A const&then haben): Der Comeau-Compiler lehnt sie ordnungsgemäß ab, GCC akzeptiert sie jedoch im nicht pedantischen Modus. Durch Umschalten auf -pedanticwird jedoch auch die richtige Mehrdeutigkeitswarnung ausgegeben.

Ich hoffe, dies hilft etwas, um klarer zu machen, wie sich diese beiden Formen unterscheiden!

Johannes Schaub - litb
quelle
Beeindruckend. Ich habe nicht einmal von der Funktionsdeklaration erfahren. Ich muss Ihre Antwort so ziemlich akzeptieren, nur weil ich der einzige bin, der davon weiß. Gibt es einen Grund, warum Funktionsdeklarationen so funktionieren? Es wäre besser, wenn c3 innerhalb einer Funktion anders behandelt würde.
Rlbond
4
Bah, sorry Leute, aber ich musste meinen Kommentar entfernen und ihn wegen der neuen Formatierungs-Engine erneut posten: Es liegt an den Funktionsparametern R() == R(*)()und T[] == T*. Das heißt, Funktionstypen sind Funktionszeigertypen und Arraytypen sind Zeiger-zu-Element-Typen. Das ist scheiße. Es kann A c3((A()));umgangen werden (parens um den Ausdruck).
Johannes Schaub - litb
4
Darf ich fragen, was "'Read 8.5 / 14'" bedeutet? Worauf bezieht sich das? Ein Buch? Ein Kapitel? Eine Website?
AzP
9
@AzP Viele Leute auf SO möchten oft Verweise auf die C ++ - Spezifikation, und das habe ich hier getan, als Antwort auf die Anfrage von rlbond "Bitte zitieren Sie Text als Beweis." Ich möchte die Spezifikation nicht zitieren, da dies meine Antwort aufbläht und viel mehr Arbeit bedeutet, um auf dem neuesten Stand zu bleiben (Redundanz).
Johannes Schaub - litb
1
@ Luca Ich empfehle, eine neue Frage dafür zu beginnen, damit andere von der Antwort profitieren können, die die Leute auch geben
Johannes Schaub - litb
49

Die Zuordnung unterscheidet sich von der Initialisierung .

Die beiden folgenden Zeilen führen die Initialisierung durch . Ein einzelner Konstruktoraufruf wird ausgeführt:

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

aber es ist nicht gleichbedeutend mit:

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

Ich habe momentan keinen Text, um dies zu beweisen, aber es ist sehr einfach zu experimentieren:

#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;
}
Mehrdad Afshari
quelle
2
Gute Referenz: "The C ++ Programming Language, Special Edition" von Bjarne Stroustrup, Abschnitt 10.4.4.1 (Seite 245). Beschreibt die Initialisierung und Zuweisung von Kopien und warum sie sich grundlegend unterscheiden (obwohl beide den Operator = als Syntax verwenden).
Naaff
Minor nit, aber ich mag es wirklich nicht, wenn Leute sagen, dass "A a (x)" und "A a = x" gleich sind. Streng genommen sind sie nicht. In vielen Fällen tun sie genau das Gleiche, aber es ist möglich, Beispiele zu erstellen, bei denen je nach Argument tatsächlich verschiedene Konstruktoren aufgerufen werden.
Richard Corden
Ich spreche nicht von "syntaktischer Äquivalenz". Semantisch sind beide Initialisierungsarten gleich.
Mehrdad Afshari
@MehrdadAfshari Im Antwortcode von Johannes erhalten Sie unterschiedliche Ausgaben, je nachdem, welche der beiden Sie verwenden.
Brian Gordon
1
@ BrianGordon Ja, du hast recht. Sie sind nicht gleichwertig. Ich hatte Richards Kommentar in meiner Bearbeitung vor langer Zeit angesprochen.
Mehrdad Afshari
22

double b1 = 0.5; ist ein impliziter Aufruf des Konstruktors.

double b2(0.5); ist ein expliziter Aufruf.

Sehen Sie sich den folgenden Code an, um den Unterschied zu erkennen:

#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;
}

Wenn Ihre Klasse keine expliziten Konstruktoren hat, sind explizite und implizite Aufrufe identisch.

Kirill V. Lyadvinsky
quelle
5
+1. Gute Antwort. Gut, auch die explizite Version zu beachten. Übrigens ist zu beachten, dass nicht beide Versionen eines einzelnen Konstruktors gleichzeitig überladen werden können. Es würde also im expliziten Fall einfach nicht kompiliert werden können. Wenn beide kompilieren, müssen sie sich ähnlich verhalten.
Mehrdad Afshari
4

Erste Gruppierung: Es kommt darauf an, was A_factory_funczurückkommt. Die erste Zeile ist ein Beispiel für die Kopierinitialisierung , die zweite Zeile ist die direkte Initialisierung . Wenn A_factory_funckehrt ein AObjekt dann sind sie gleichwertig, sie beide Aufruf der Kopierkonstruktor für A, andernfalls wird die erste Version erstellt eine rvalue des Typs Avon einem verfügbaren Konvertierungsoperatoren für den Rückgabetyp A_factory_funcoder entsprechenden AKonstrukteuren, und ruft dann die Kopie Konstruktor Konstrukt a1aus dieser vorübergehend. Die zweite Version versucht, einen geeigneten Konstruktor zu finden, der alle A_factory_funcRückgaben akzeptiert oder etwas verwendet, in das der Rückgabewert implizit konvertiert werden kann.

Zweite Gruppierung: Es gilt genau die gleiche Logik, außer dass eingebaute Typen keine exotischen Konstruktoren haben und daher in der Praxis identisch sind.

Dritte Gruppierung: c1Wird standardmäßig initialisiert und c2wird von einem temporär initialisierten Wert kopierinitialisiert. Mitglieder c1mit Pod-Typ (oder Mitglieder von Mitgliedern usw. usw.) können möglicherweise nicht initialisiert werden, wenn der vom Benutzer angegebene Standardkonstruktor (falls vorhanden) diese nicht explizit initialisiert. Denn c2es hängt davon ab, ob es einen vom Benutzer bereitgestellten Kopierkonstruktor gibt und ob dieser diese Mitglieder entsprechend initialisiert, aber die Mitglieder des temporären werden alle initialisiert (nullinitialisiert, wenn nicht ausdrücklich anders initialisiert). Wie Litb entdeckt, c3ist eine Falle. Es ist eigentlich eine Funktionsdeklaration.

CB Bailey
quelle
4

Bemerkenswert:

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

Dh zur Kopierinitialisierung.

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

Mit anderen Worten, ein guter Compiler erstellt keine Kopie für die Kopierinitialisierung, wenn dies vermieden werden kann. Stattdessen wird der Konstruktor einfach direkt aufgerufen - dh genau wie bei der Direktinitialisierung.

Mit anderen Worten, die Kopierinitialisierung ist in den meisten Fällen wie die Direktinitialisierung, wenn verständlicher Code geschrieben wurde. Da die Direktinitialisierung möglicherweise willkürliche (und daher wahrscheinlich unbekannte) Konvertierungen verursacht, bevorzuge ich nach Möglichkeit immer die Kopierinitialisierung. (Mit dem Bonus, dass es tatsächlich wie eine Initialisierung aussieht.) </ Opinion>

Technische Güte: [12.2 / 1 Fortsetzung von oben] 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.

Ich bin froh, dass ich keinen C ++ - Compiler schreibe.

John H.
quelle
4

Sie können den Unterschied zwischen explicitund implicitKonstruktortypen erkennen, wenn Sie ein Objekt initialisieren:

Klassen :

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

class B
{
    explicit B(int) { }
    explicit B(int, int) { }
};

Und in der main Funktion:

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
}

Standardmäßig ist ein Konstruktor so, implicitdass Sie zwei Möglichkeiten haben, ihn zu initialisieren:

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

Und indem Sie eine Struktur als explicitnur definieren, haben Sie einen Weg als direkt:

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
BattleTested
quelle
3

Antwort in Bezug auf diesen Teil:

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

Da die meisten Antworten vor C ++ 11 sind, füge ich hinzu, was C ++ 11 dazu zu sagen hat:

Ein einfacher Typspezifizierer (7.1.6.2) oder Typennamenspezifizierer (14.6) gefolgt von einer Ausdrucksliste in Klammern erstellt einen Wert des angegebenen Typs in der Ausdrucksliste. Wenn die Ausdrucksliste ein einzelner Ausdruck ist, entspricht der Typkonvertierungsausdruck (in der Definition und wenn in der Bedeutung definiert) dem entsprechenden Besetzungsausdruck (5.4). Wenn der angegebene Typ ein Klassentyp ist, muss der Klassentyp vollständig sein. Wenn die Ausdrucksliste mehr als einen einzelnen Wert angibt, muss der Typ eine Klasse mit einem entsprechend deklarierten Konstruktor (8.5, 12.1) sein, und der Ausdruck T (x1, x2, ...) entspricht in seiner Wirkung der Deklaration T t (x1, x2, ...); für einige erfundene temporäre Variable t, mit dem Ergebnis ist der Wert von t als Wert.

Optimierung oder nicht, sie entsprechen dem Standard. Beachten Sie, dass dies mit den anderen Antworten übereinstimmt. Ich zitiere nur, was der Standard aus Gründen der Korrektheit zu sagen hat.

bashrc
quelle
Keine der Ausdruckslisten Ihrer Beispiele gibt mehr als einen einzelnen Wert an. Wie ist irgendetwas davon relevant?
underscore_d
0

Viele dieser Fälle unterliegen der Implementierung eines Objekts, daher ist es schwierig, Ihnen eine konkrete Antwort zu geben.

Betrachten Sie den Fall

A a = 5;
A a(5);

In diesem Fall wirkt sich die Implementierung dieser Methoden auf das Verhalten jeder Zeile aus, wenn ein geeigneter Zuweisungsoperator und ein Initialisierungskonstruktor angenommen werden, die ein einzelnes ganzzahliges Argument akzeptieren. Es ist jedoch üblich, dass einer von ihnen den anderen in der Implementierung aufruft, um doppelten Code zu eliminieren (obwohl es in einem so einfachen Fall keinen wirklichen Zweck geben würde.)

Bearbeiten: Wie in anderen Antworten erwähnt, wird in der ersten Zeile tatsächlich der Kopierkonstruktor aufgerufen. Betrachten Sie die Kommentare zum Zuweisungsoperator als Verhalten im Zusammenhang mit einer eigenständigen Zuweisung.

Das heißt, wie der Compiler den Code optimiert, hat dann seine eigenen Auswirkungen. Wenn der initialisierende Konstruktor den Operator "=" aufruft - wenn der Compiler keine Optimierungen vornimmt, würde die oberste Zeile 2 Sprünge ausführen, im Gegensatz zu einem in der untersten Zeile.

In den häufigsten Situationen optimiert Ihr Compiler diese Fälle und beseitigt diese Art von Ineffizienzen. So effektiv werden alle verschiedenen Situationen, die Sie beschreiben, gleich ausfallen. Wenn Sie genau sehen möchten, was gerade getan wird, können Sie sich den Objektcode oder eine Assembly-Ausgabe Ihres Compilers ansehen.

dborba
quelle
Es ist keine Optimierung . Der Compiler muss den Konstruktor in beiden Fällen gleichermaßen aufrufen. Infolgedessen wird keiner von ihnen kompiliert, wenn Sie nur haben operator =(const int)und nein A(const int). Weitere Informationen finden Sie in der Antwort von @ jia3ep.
Mehrdad Afshari
Ich glaube, du hast tatsächlich Recht. Es wird jedoch problemlos mit einem Standardkopierkonstruktor kompiliert.
Dborba
Wie bereits erwähnt, ist es üblich, dass ein Kopierkonstruktor einen Zuweisungsoperator aufruft. Zu diesem Zeitpunkt kommen Compiler-Optimierungen ins Spiel.
Dborba
0

Dies ist aus der C ++ - Programmiersprache von Bjarne Stroustrup:

Eine Initialisierung mit einem = wird als Kopierinitialisierung betrachtet . Im Prinzip wird eine Kopie des Initialisierers (des Objekts, von dem wir kopieren) in das initialisierte Objekt eingefügt. Eine solche Kopie kann jedoch wegoptimiert (entfernt) werden, und eine Verschiebungsoperation (basierend auf der Verschiebungssemantik) kann verwendet werden, wenn der Initialisierer ein r-Wert ist. Wenn Sie das = weglassen, wird die Initialisierung explizit. Die explizite Initialisierung wird als direkte Initialisierung bezeichnet .

Bharat
quelle