Kopier-Konstruktor
-
Ich frage mich einfach: Wie weit darf der Compiler das erwartete Verhalten zugunsten einer Optimierung verändern.
Grundsätzlich: Überhaupt nicht. Die Auslassung von Kopien ist die einzige Ausnahme von dieser Regel.
-
Ulrike_1992 schrieb:
class Foo ... Foo f; Foo g(f);sollte doch das Gleiche bewirken wie
Foo f; Foo g; g = f;Eher genau umgekehrt:
Foo f; Foo g = f; // oder Foo g; g = f;Wird zu:
Foo f; Foo g(f);
-
Skym0sh0 schrieb:
Foo f; Foo g = f; // oder Foo g; g = f;Wird zu:
Foo f; Foo g(f);Dass ein Ausdruck wie
Foo g = feine andere Schreibweise ist fürFoo g(f)ist mir schon bewusst. Worauf ich hinaus wollte ist, dass eine Initialisierung der FormFoo g(f)ein Objekt erzeugen sollte, das inhaltlich gleich ist mit einem Objekt, das zunächst mit dem Default-KonstruktorFoo gerzeugt wurde und das man anschließend in einer Zuweisungg = fändert.Aus dem Grund sollte es dem Compiler eigentlich möglich sein, einen Kopierkonstruktor zu erzeugen, wenn er den Zuweisungsoperator kennt. Er würde dann einfach den Funktionsrumpf des Kopierkonstruktors ersetzen durch
this->operator=(foo_objekt_oder_wie_immer_die_Klasse_lautet).Ein Programmierer könnte aber den Kopierkonstruktor und den Zuweisungsoperator so definieren, dass überraschenderweise ein Dreizeiler wie
Foo f; Foo g; g = f;ein Objekt
gerzeugt, das sich inhaltlich unterscheidet von dem Objektg, das erzeugt wird durch folgenden Zweizeiler:Foo f; Foo g(f);Wie ich ja jetzt gelernt habe, geht der Compiler (Copy Elision) davon aus, dass das Objekt
fdas durch einen Kopier-Konstruktor (mit temporärem Objekt) erzeugt wird:Foo f(Foo(p1, p2, p3));inhaltlich identisch ist mit dem Objekt
fdas durch den Aufruf eines Konstruktors mit Parameterliste erzeugt wird:Foo f(p1, p2, p3);Auch wenn der Kopierkonstruktor (unsinnigerweise) so definiert sein könnte, dass er eigentlich ein ganz anderes Objekt erzeugen sollte als über einen Konstruktor mit Parameterliste.
-
Ulrike_1992 schrieb:
Aus dem Grund sollte es dem Compiler eigentlich möglich sein, einen Kopierkonstruktor zu erzeugen, wenn er den Zuweisungsoperator kennt. Er würde dann einfach den Funktionsrumpf des Kopierkonstruktors ersetzen durch
this->operator=(foo_objekt_oder_wie_immer_die_Klasse_lautet).Nein, das geht eben nicht. Ein Gegenbeispiel wurde dir schon genannt.
-
SeppJ schrieb:
Ein Gegenbeispiel wurde dir schon genannt.
Ich muss leider zugeben, dass ich den Punkt noch nicht verstanden habe. Ich nehme an, du meinst das Beispiel mit dem
vector. Aber erzeugen nicht folgende zwei Codefragmente den inhaltlich gleichenvector v2?Version 1:
std::vector<int> v1; v1.push_back(3); std::vector<int> v2; v2 = v1;Version 2:
std::vector<int> v1; v1.push_back(3); std::vector<int> v2(v1);Könnte der Compiler nicht Version 1 durch Version 2 ersetzen?
- Version 1: 2 x Default-Konstruktor + 1 x Zuweisung
- Version 2: 1 x Default-Konstruktor + 1 x Kopierkonstruktor
Ich weiß ja nicht wie der Kopierkonstruktor für
vectorimplementiert ist, aberVersion 2könnte schneller sein...
-
Ulrike_1992 schrieb:
Könnte der Compiler nicht Version 1 durch Version 2 ersetzen?
- Version 1: 2 x Default-Konstruktor + 1 x Zuweisung
- Version 2: 1 x Default-Konstruktor + 1 x Kopierkonstruktor
Nö, das ist doch syntaktisch und semantisch total was anderes. Hab den Thread jetzt nicht gelesen, aber kennst du den Unterschied zwischen Initialisierung und Zuweisung?
-
Version 1 durch Version 2 zu ersetzen ist doch der umgekehrte Vorgang zu dem was du gefragt hast:Aus dem Grund sollte es dem Compiler eigentlich möglich sein, einen Kopierkonstruktor zu erzeugen, wenn er den Zuweisungsoperator kennt. Er würde dann einfach den Funktionsrumpf des Kopierkonstruktors ersetzen durch this->operator=(foo_objekt_oder_wie_immer_die_Klasse_lautet).
Das entspräche aus
#include <iostream> #include <vector> #include <cstdlib> using namespace std; typedef vector<int> Vector; int main() { Vector original = {1,2,3,4,5}; Vector *kopie = new Vector(original); // Kopierkonstruktor for (auto a : *kopie) cout << a << '\n'; delete kopie; }folgendes zu machen:
#include <iostream> #include <vector> #include <cstdlib> using namespace std; typedef vector<int> Vector; int main() { Vector original = {1,2,3,4,5}; Vector* kopie = static_cast<Vector*>(malloc(sizeof(Vector))); // Vollkommen uninitialisierter Vector *kopie = original; // Zuweisung statt Kopierkonstruktor for (auto a : *kopie) cout << a << '\n'; kopie->~Vector(); free(kopie); }Und das ist hochgradig undefiniertes Verhalten. Ironischerweise wird das sogar zu 99% laufen (da man Nullpointer deleten kann), jedoch wird dir ein automatischer leak detector bei dem Code durchdrehen. valgrind sagt zum Beispiel:
==15044== Conditional jump or move depends on uninitialised value(s) ==15044== at 0x400FB1: std::vector<int, std::allocator<int> >::operator=(std::vector<int, std::allocator<int> > const&) (vector.tcc:183) ==15044== by 0x400C05: main (test.cc:12) ==15044== Uninitialised value was created by a heap allocation ==15044== at 0x4C27302: malloc (vg_replace_malloc.c:263) ==15044== by 0x400BEE: main (test.cc:11) ==15044== ==15044== Conditional jump or move depends on uninitialised value(s) ==15044== at 0x401435: std::_Vector_base<int, std::allocator<int> >::_M_deallocate(int*, unsigned long) (stl_vector.h:174) ==15044== by 0x40104C: std::vector<int, std::allocator<int> >::operator=(std::vector<int, std::allocator<int> > const&) (vector.tcc:189) ==15044== by 0x400C05: main (test.cc:12) ==15044== Uninitialised value was created by a heap allocation ==15044== at 0x4C27302: malloc (vg_replace_malloc.c:263) ==15044== by 0x400BEE: main (test.cc:11)P.S.: Entschuldige den Schmuddelcode mit new, malloc & Co, aber anders bekommt man eben keinen "leeren" vector hin.
-
SeppJ schrieb:
Version 1 durch Version 2 zu ersetzen ist doch der umgekehrte Vorgang zu dem was du gefragt hast:Entschuldigung, der Thread ist ein bisschen unübersichtlich geworden. Ich hatte tatsächlich zunächst gedacht, dass man ein leeres Objekt erzeugen könnte. Aber klar, leuchtet mir ein: geht nicht. Wenigstens über einen Default-Konstruktor muss das Objekt erst einmal ordnungsgemäß initialisiert werden.
Daher kam mir der Gedanke, ob der umgekehrte Vorgang zu einer Optimierung des Codes führen könnte. Das heißt, ob der Compiler versuchen könnte, in speziellen Fällen, eine Kombination aus Default-Konstruktor und Zuweisung durch eine einzelne Initialisierung durch den Kopier-Konstruktor zu ersetzen. Meine Frage lautet also nur noch: Kann es passieren, dass der Compiler Version 1 (siehe oben, Beitrag 19:16:21 15.03.2013) durch Version 2 ersetzt? Der Compiler könnte doch auf die Idee kommen: Warum soll ich erst ein Objekt mit dem Default-Konstruktor erzeugen und anschließend mit einer Zuweisung ändern, wenn ich das gleich in einem Schritt mit dem Kopier-Konstruktor erledigen könnte?
-
Ulrike_1992 schrieb:
Warum soll ich erst ein Objekt mit dem Default-Konstruktor erzeugen und anschließend mit einer Zuweisung ändern, wenn ich das gleich in einem Schritt mit dem Kopier-Konstruktor erledigen könnte?
Weil dies das beobachtbare Verhalten des Programmes ändern könnte (wenn die Funktionen Nebeneffekte haben). Das darf eine Optimierung nicht, außer eben in den genau definierten Fällen, die oben schon genannt/verlinkt wurden und zur Ursprungsfrage führten. Wenn die Funktionen keine beobachtbaren Effekte haben, dann kann und wird das natürlich optimiert, wahrscheinlich sogar mehr noch als du dir überhaupt vorstellen kannst. Denn Optimierung findet übrigens meistens auf Assemblerebene statt, da ist vom Code selber sowieso nichts mehr übrig und es braucht auf keinerlei Strukturen Rücksicht genommen werden. Wenn ein aggressiver Optimierer über den Code geht und man sich hinterher (anhand von Debugsymbolen) mal ansieht, was wo landet, dann erkennt man kaum mehr etwas wieder.
-
Alles klar. Danke
