Wann wird ein Copy/Move Constructor von C++ verwendet?
-
Lerne gerade C++ 11 und wollte mir den Unterschied zwischen Copy und Move Constructor klar machen. Dazu habe ich ein kleines C++ 11 "Hello, world!" geschrieben. Leider wird weder der Copy noch der Move Constructor aufgerufen:
Ich war der Meinung in der Funktion:
simplestring hello() { simplestring hello=simplestring("Hello, world!"); return hello; // IMHO this should call the move constructor, or if absent the copy constructor }
sollte bei der Anweisung "return hello;" der Copy (wenn es keinen Move Constructor gibt) oder der Move Constructor aufgerufen werden.
Immerhin wird die lokale (automatische) Variable auf dem Stack (Stackframe) angelegt und ist nach dem return weg (weil der Stackframe aufgeräumt wird).
Stackframe in Funktion hello IMHO:
STACK: // hier zeigt der Stackpointer hin (A7 bei 68000er-Prozessor)
// Aufruf von hello():
STACK: Rücksprungadresse
STACK + SIZE_OF_ADDRESS: hello // lokale (automatische Variable hello)Nach dem return in hello() sollte der Programmzähler auf die Rücksprungadresse gesetzt werden und der Stackpointer zeigt danach wieder auf "STACK:".
D. h. die lokale (automatische Variable hello steht nicht mehr zur Verfügung. Daher muss die IMHO beim beim return kopiert oder verschoben werden, was aber anscheinend nicht passiert, siehe folgenden Code:
#include <iostream> #include <string.h> #if __cplusplus <=199711L // if not a C++ 11 compiler #define nullptr 0 #endif using namespace std; // To show the new move operations (eg. move constructor) we need a simple class: class simplestring { int sz; char *s; public: friend ostream& operator<<(ostream& os,const simplestring &ss); // now we can use e. g. cout << simplestringVar simplestring() : sz(0), s(nullptr) {}; simplestring(const char *cstr); ~simplestring(); simplestring(const simplestring& ss); // copy constructor // C++ 11: simplestring(simplestring&& ss); // move constructor const char operator[](int i) const { // get char at index i (not C++ 11 specific) if (i < 0) return s[0]; if (i < sz) return s[i]; return s[sz-1]; } size_t address() {return reinterpret_cast<size_t>(s);} }; simplestring hello() { simplestring hello=simplestring("Hello, world!"); //cout << hello.address() << endl; return hello; // IMHO this should call the move constructor, or if absent the copy constructor } // We must overload the <<-operator for ostream, so that we can output a simplestring with <<: ostream& operator<<(ostream& os,const simplestring &ss) { for (int i=0; i < ss.sz; i++) os << ss[i]; return os; } int main(int argc, char *argv[]) { simplestring msg=hello(); //cout << msg.address() << endl; cout << msg << endl; return 0; } // methods for our simplestringclass: simplestring::simplestring(const char *cstr) { sz=strlen(cstr); s=new char[sz+1]; // demo: no error check strcpy(s,cstr); } simplestring::simplestring(const simplestring& ss) { cout << "In copy constructor" << endl; if (s != nullptr) { delete[] s; sz=0; } s=new char[ss.sz+1]; // demo: no error check sz=ss.sz; for (int i=0; i < sz; i++) s[i]=ss[i]; s[sz]=0; } // C++ 11 move constructor: simplestring::simplestring(simplestring&& ss) : s(ss.s), sz(ss.sz) { cout << "In move constructor" << endl; ss.s=nullptr; ss.sz=0; } simplestring::~simplestring() { if (s != nullptr) { delete[] s; sz=0; } }
Das Programm wurde mit "clang" (3.6.2) unter Windows mit:
clang++ -O0 -g -std=c++11 -o cpp11hello0.exe cpp11hello0.cpp
übersetzt und funktioniert auch, Ausgabe:
Hello, world!
aber ich dachte, die Ausgabe müsste:
In move constructor
Hello, world!bzw. (wenn Move Constructor auskommentiert wird):
In copy constructor
Hello, world!lauten.
-
-
Mit -O0 (Optimizer deaktiviert) macht das wenig Sinn. Ansonsten würde hier wohl eher RVO verwendet.
-
cutword schrieb:
Habe es noch nicht ganz durchgelesen, glaube es aber "geschnallt" zu haben. Obwohl ich alle Optimierungen ausgeschaltet habe, darf der Compiler in solch einem einfachen (offensichtlichen) Fall den Copy/Move-Constructor "wegoptimieren", selbst wenn er Seiteneffekte haben sollte.
Richtig?
-
Was anderes zu deinem Code:
s=new char[ss.sz+1]; // demo: no error check
Du musst da nichts checken, new schmeißt eine Exception, wenn was schief geht.
Außerdem implementierst du eher einen Assignment-Operator, keinen CopyCtor (da hat das Objekt nämlich keinen alten State!)
Wäre das ein Assignment-Operator, wäre der aber nicht Exception-safe: Wenn das new eine Exception schmeißt, ist der alte String bereits zerstört und das Objekt somit ungültig (der Pointer zeigt noch auf die alte Adresse)if (s != nullptr) { delete[] s; sz=0; }
Das if ist unnötig, delete[] kommt mit nullptr klar. Und im Destruktor musst du sz auch nicht mehr auf 0 setzen.
-
Der Standard definiert keinen Optimizer. Der Compiler darf sich immer Standradlonform verhalten.
-
Nathan schrieb:
Was anderes zu deinem Code:
Jau hast Recht, hatte das nicht so durchdacht, mir ging es in dem Beispiel (völlig sinnlos) nur darum, zu verstehen bzw. zu sehen, ob tatsächlich automatisch der Move Constructor aufgerufen wird (sofern vorhanden). Aber dazu ist mein Beispiel nach cutword (Danke!) wohl zu einfach gestrickt.
Nathan schrieb:
Das if ist unnötig, delete[] kommt mit nullptr klar. Und im Destruktor musst du sz auch nicht mehr auf 0 setzen.
Danke, werde ich mir merken.
-
So habe das Programm jetzt umgeschrieben und gebe in der "hello()-" Funktion jetzt ein verändertes Ergebnis zurück (dazu extra den operator+() für simplestring implementiert).
Leider wird der Verschiebe-Konstruktor nicht aufgerufen, sondern der Copy-Konstruktor. Ist mir nicht klar. Was muss ich ändern, damit der Copy-Konstruktor aufgerufen wird. Eigentlich sollte der Compiler das doch automatisch machen, fall er definiert ist. Scheint aber doch nicht so einfach zu sein.
// C++ 11 "Hello, world!" program to demonstrate the move constructor // Compiles on Windows with clang version 3.6.2 with: // clang++ -O0 -g -std=c++11 -o cpp11hello2.exe cpp11hello2.cpp #include <iostream> #include <string.h> #if __cplusplus <=199711L // if not a C++ 11 compiler #define nullptr 0 #endif using namespace std; // To show the new move operations (eg. move constructor) we need a simple class: class simplestring { int sz; char *s; public: friend ostream& operator<<(ostream& os,const simplestring &ss); // now we can use e. g. cout << simplestringVar simplestring() : sz(0), s(nullptr) {}; simplestring(const char *cstr); ~simplestring(); simplestring& operator+(const char *cstr); // We need string catenation for hello() (see footnote 1) simplestring(const simplestring& ss); // copy constructor simplestring& operator=(const simplestring &ss); // Required, move constructor deletes assignement(!) // (albeit [currently] not used in this program) // C++ 11: simplestring(simplestring&& ss); // move constructor const char operator[](int i) const { // get char at index i (not C++ 11 specific) if (i < 0) return s[0]; if (i < sz) return s[i]; return s[sz-1]; } }; simplestring hello(const char *cstr) { simplestring ss=simplestring("Hello, "); return ss+"world!"; // IMHO this should call the move constructor (only if absent the copy constructor) } // . . . B u t i t d o e s n ' t - it calls the copy constructor - why? // We must overload the <<-operator for ostream, so that we can output a simplestring with <<: ostream& operator<<(ostream& os,const simplestring &ss) { for (int i=0; i < ss.sz; i++) os << ss[i]; return os; } int main(int argc, char *argv[]) { simplestring msg=hello("world!\n"); cout << msg << endl; return 0; } // methods for our simplestringclass: simplestring::simplestring(const char *cstr) { sz=strlen(cstr); s=new char[sz+1]; strcpy(s,cstr); } simplestring::simplestring(const simplestring& ss) { // copy constructor cout << "In copy constructor" << endl; s=new char[ss.sz+1]; sz=ss.sz; for (int i=0; i < sz; i++) s[i]=ss[i]; s[sz]=0; } // C++ 11 move constructor: simplestring::simplestring(simplestring&& ss) : s(ss.s), sz(ss.sz) { cout << "In move constructor" << endl; ss.s=nullptr; ss.sz=0; } // We need a chain operator for the hello() function (see footnote 1 below): simplestring& simplestring::operator+(const char *cstr) { int newsz=sz+strlen(cstr); char *tmp=new char[newsz+1]; for (int i=0; i < sz; i++) tmp[i]=s[i]; strcpy (tmp+sz,cstr); delete[] s; s=tmp; sz=newsz; return *this; } simplestring& simplestring::operator=(const simplestring &ss) { delete[] s; s=new char[ss.sz+1]; sz=ss.sz; for (int i=0; i < sz; i++) s[i]=ss[i]; s[sz]=0; return *this; } simplestring::~simplestring() { if (s != nullptr) { delete[] s; sz=0; } } /* footnote 1: Please note, that just using the following function does neither call the copy nor the move constructor. This is because of the copy elision (http://http://en.cppreference.com/w/cpp/language/copy_elision and see also here: https://www.c-plusplus.net/forum/334016 [German]): simplestring hello() { simplestring ss=simplestring("Hello, world!"); return ss; // This doesn't call the move constructor, nor (if absent) } // the copy constructor(!). At least not with clang 3.6.2. */
Ausgabe des Programms:
F:\home\hps\cpp\clang\test>cpp11hello2 In copy constructor Hello, world!
Schreibt man die hello()-Funktion um:
...
ss+"world!";
return ss;
...dann wird weder der Copy noch der Move-Konstruktor aufgerufen sondern es schlägt anscheinend wieder die "copy elision" zu.
-
Dein Operator+ hat eine merkwürdige Semantik. Wie ein Operator +=. Wenn wir deinen Operator so aufrufen, wie man ihn korrekterweise aufrufen würde, also:
simplestring ss("Hello, "); ss += "world!"; // Oder bei dir eben ss + "world!"; return ss;
Dann sollten hier sogar sämtliche Konstruktoren der copy-elision zum Opfer fallen.
Wenn wir hingegen einen "richtigen" Operator+ definieren (
simplestring operator+(simplestring const& ss, const char *cstr)
) und dann so wie bei dir aufrufen:simplestring ss=simplestring("Hello, "); return ss+"world!";
Dann sollte ebenfalls alles wegoptimiert werden.
Dein komisches Zwischending ist bei der Überladungsauflösung kein gültiger Kandidat für eine rvalue-Referenz. Wenn es unbedingt sein muss, kann man natürlich mit Gewalt eine daraus machen:
simplestring ss=simplestring("Hello, "); return std::move(ss+"world!");
Dies sollte zu einem einzelnen move führen. Natürlich sind die beiden anderen Lösungen besser, denn da wird überhaupt keine unnötige Aktion durchgeführt.
-
So mittlerweile habe ich es immerhin geschafft, dass die Verschiebezuweisung (move assignement) aufgerufen wird. Ist sie auskommentiert (vor C++ 11 gab es die ja nicht), wird ohne jede Änderung im Sourcecode die Kopierzuweisung (copy assignement) aufgerufen. Aber es will mir einfach nicht gelingen, einen Code zu schreiben, der den Verschiebekonstruktor (move constructor) aufruft. Entweder er wird aufgrund der Copy Elosion weg optimiert oder es wird trotz Move Constructor der Copy Constructor aufgerufen.
BTW: In meinem aktuellen Stroustrup (deutsch) kann ich zu Copy Elosion bzw. Return Value Optimization (RVO) nichts finden - sollte der das echt vergessen haben!
// C++ 11 "Hello, world!" program to demonstrate the move assignement // Compiles on Windows with clang version 3.6.2 with: // clang++ -O0 -g -std=c++11 -o cpp11hello5.exe cpp11hello5.cpp #include <iostream> #include <string.h> #if __cplusplus <=199711L // if not a C++ 11 compiler #define nullptr 0 #endif using namespace std; // To show the new move operations (eg. move constructor) we need a simple class: class simplestring { int sz; char *s; void appendzero() {s[sz]=0;} // So that s member is always compatible with a traditional C-String public: friend ostream& operator<<(ostream& os,const simplestring &ss); // now we can use e. g. cout << simplestringVar simplestring() : sz(0), s(nullptr) {}; simplestring(const char *cstr); ~simplestring(); friend simplestring operator+(const simplestring &s1, const simplestring &s2); // Needed see footnote 1 simplestring(const simplestring& ss); // copy constructor // copy assignement: simplestring& operator=(const simplestring &ss); // This or assignement operator below is required // C++ 11 (if you comment the following move assignement, copy assignement will be used): simplestring& operator=(simplestring &&ss) { // move assignement - will be used without any cout << "In move assignement"; // other change to source code if available. If cout << ", address ss.s=" << ss.address() << endl; s=ss.s; ss.s=nullptr; sz=ss.sz; ss.sz=0; // not available, copy assignement is used. return *this; // Comment it out (this move assignement) to see it } // END move assignement simplestring(simplestring&& ss); // move constructor (C++ 11) char& operator[](const int i) const { // get lreference to char at index i (not C++ 11 specific) if (i < 0) return s[0]; if (i < sz) return s[i]; return s[sz-1]; } size_t address() const {return reinterpret_cast<size_t>(s);} }; simplestring hello(const simplestring& h2) { simplestring h1=simplestring("Hello, "); simplestring hello; cout << "In hello(...) after line \"simplestring hello;\"" << endl; hello=h1+h2; // This calls the move assignement or if absent the copy assignement cout << "In hello(...) after \"hello=h1+h2;\", address of hello.s=" << hello.address() << endl; return hello; } // The following (instead of above function) will neither call the move constructor nor // the copy constructor, because of Return Value Optimiziation (RVO) aka "copy elision": /* simplestring hello(const simplestring& h2) { simplestring h1=simplestring("Hello, "); cout << "In hello(...) after line \"simplestring h1=simplestring(\"Hello, );\"" << endl; simplestring hello=h1+h2; return hello; } */ int main(int argc, char *argv[]) { simplestring msg=hello(simplestring("world!")); //cout << msg.address() << endl; cout << msg << endl; cout << endl << "Note: If copy assignement is used the both addresses are\n" "different, while if move assignement is used, they are equal" << endl; return 0; } // methods for our simplestringclass: // C++ 11 move constructor: simplestring::simplestring(simplestring&& ss) : s(ss.s), sz(ss.sz) { cout << "In move constructor" << endl; ss.s=nullptr; ss.sz=0; } // Rest not C++ 11 specific: simplestring::simplestring(const simplestring& ss) { // copy constructor cout << "In copy constructor" << endl; // if (s != nullptr) { // delete[] s; // sz=0; // } s=new char[ss.sz+1]; sz=ss.sz; for (int i=0; i < sz; i++) s[i]=ss[i]; s[sz]=0; } simplestring& simplestring::operator=(const simplestring &ss) { // copy assignement cout << "In copy assignement"; cout << ", address of member ss.s=" << ss.address() << endl; delete[] s; s=new char[ss.sz+1]; sz=ss.sz; for (int i=0; i < sz; i++) s[i]=ss[i]; s[sz]=0; return *this; } simplestring::simplestring(const char *cstr) { sz=strlen(cstr); s=new char[sz+1]; // demo: no error check strcpy(s,cstr); } simplestring operator+(const simplestring &s1, const simplestring &s2) { cout << "In Function operator+(const simplestring &s1, const simplestring &s2)" << endl; int tsz=s1.sz+s2.sz; simplestring tmp; tmp.s=new char[tsz+1]; tmp.sz=tsz; int i; for (i=0; i < s1.sz; i++) tmp[i]=s1[i]; for (int j=0; i < tsz; i++, j++) tmp[i]=s2[j]; // tmp[tsz]=0; // This doesn't work because the index operator cannot write outside the simplestring tmp.appendzero(); // So we have implemented a member function to terminate the s member return tmp; } // We must overload the <<-operator for ostream, so that we can output a simplestring with <<: ostream& operator<<(ostream& os,const simplestring &ss) { for (int i=0; i < ss.sz; i++) os << ss[i]; return os; } simplestring::~simplestring() { if (s != nullptr) { delete[] s; sz=0; } } /* footnote 1: Please note, that just using the following function does neither call the copy nor the move constructor/assignement. This is because of the copy elision (http://http://en.cppreference.com/w/cpp/language/copy_elision and see also here: https://www.c-plusplus.net/forum/334016): simplestring hello() { simplestring hello=simplestring("Hello, world!"); return hello; // This does neither call the move , nor (if absent) the copy } // assignement/constructor(!). At least not with clang 3.6.2. */
Erzeugt folgende Ausgaben (je nachdem, ob move assignement verfügbar):
F:\home\hps\cpp\clang\test>REM move assignement commented: F:\home\hps\cpp\clang\test>clang++ -O0 -g -std=c++11 -o cpp11hello5.exe cpp11hel lo5.cpp F:\home\hps\cpp\clang\test>cpp11hello5 In hello(...) after line "simplestring hello;" In Function operator+(const simplestring &s1, const simplestring &s2) In copy assignement, address of member ss.s=212720 In hello(...) after "hello=h1+h2;", address of hello.s=212744 Hello, world! Note: If copy assignement is used the both addresses are different, while if move assignement is used, they are equal
F:\home\hps\cpp\clang\test>REM move assignement not commented: F:\home\hps\cpp\clang\test>clang++ -O0 -g -std=c++11 -o cpp11hello5.exe cpp11hel lo5.cpp F:\home\hps\cpp\clang\test>cpp11hello5 In hello(...) after line "simplestring hello;" In Function operator+(const simplestring &s1, const simplestring &s2) In move assignement, address ss.s=212720 In hello(...) after "hello=h1+h2;", address of hello.s=212720 Hello, world! Note: If copy assignement is used the both addresses are different, while if move assignement is used, they are equal
-
johan schrieb:
Aber es will mir einfach nicht gelingen, einen Code zu schreiben, der den Verschiebekonstruktor (move constructor) aufruft. Entweder er wird aufgrund der Copy Elosion weg optimiert oder es wird trotz Move Constructor der Copy Constructor aufgerufen.
Was ja auch gut ist! Ich versteh nicht so ganz, was du hier überhaupt erreichen möchtest. Aber wenn wir mit Gewalt einen Fall erzwingen, wo die Kopie nicht komplett wegoptimiert werden kann aber gleichzeitig die Überladungsauflösung noch den Move-Constructor finden kann, dann wird dieser auch genommen. Ein Beispiel, wenn man deinen Code noch ein Stück ekeliger macht:
friend simplestring operator+(simplestring s1, const simplestring &s2); simplestring operator+(simplestring s1, const simplestring &s2) { cout << "In Function operator+(simplestring s1, const simplestring &s2)" << endl; int tsz=s1.sz+s2.sz; char *tmp=new char[tsz+1]; int i; for (i=0; i < s1.sz; i++) tmp[i]=s1[i]; for (int j=0; i < tsz; i++, j++) tmp[i]=s2[j]; tmp[tsz]=0; delete[] s1.s; s1.sz=tsz; s1.s=tmp; return s1; } // Und dann simplestring hello(const simplestring &h2) { return "Hello, " + h2; }
Das sollte einen einzelnen Move-Konstruktoraufruf ergeben. Bei deinem Code konnte einfach das tmp-Objekt weiter benutzt werden und somit RVO greifen. Dank meiner "Verbesserung" greift das nicht mehr. Das Ergebnis ist aber ein temporäres Objekt, daher kann ein move genutzt werden, um es aus der hello-Funktion heraus zu bringen.
-
Ich empfehle dir, alle deine Programme mit
-fno-elide-constructors
zu kompilieren.
-
@SeppJ: Ich wollte eben einfach mal sehen, ob/wie so ein C++ 11 Compiler (bisher hatte ich nur Visual Studio 10 zur Verfügung) den Verschiebekonstruktor (statt des Kopierkonstruktors) aufruft.
Dabei habe ich aber ja jetzt gesehen, dass Kopier-/Verschiebekonstruktor häufig durch RVO einfach wegoptimiert wird. Aber immerhin weiß ich jetzt auch, dass man für einen ersten Entwurf Verschiebkonstruktor (-zuweisung) einfach weglassen kann, der Compiler nimmt dann einfach den (langsameren) Kopierkonstruktor (Kopierzuweisung).
Allerdings will ich meinen Verschiebkonstruktor auch testen, aber das ist ja dank des Hinweis auf -fno-elide-constructors von rip performance (danke) auch kein Problem mehr.