Martin Sulzmann
Objekte mit Zeigerelementen (dynamisch allokierten Komponenten)
Zu wenige “deletes” (Speicherleck)
Zu viele “deletes (es kracht)
Abhilfen:
Resource Acquisition Is Initialization (RAII)
Anlegen der Resource im Konstruktor (“new”)
Freigabe via Destruktor (“delete”)
RAII hat Problem falls Zeiger kopiert werden (möglicherweise zu viele deletes)
“copy” (Rule of Three)
Kopiere keine Zeiger (sonst kann es krachen)
Kopiere den Inhalt
C++11 rvalue references und “move” Semantik (Rule of Five)
Kopiere den Inhalt (ineffizient da in der Regel zeitintensiv)
Bewege (“move”) den Zeiger (effizient)
Wann und wie werden Objekte kopiert?
Wann:
d
ist neues Objekt basierend auf einer Kopie von
c
Überschreibe f
mit c
. Daher ist
f
eine Kopie von c
.
Parameter e
gebunden an f
. Daher ist
e
eine Kopie von f
.
Wie:
Jede Klasse hat
einen Kopierkonstruktor
einen Zuweisungsoperator (methode)
class MyInt {
int x;
public:
MyInt(int y=0) { x=y;}
// Kopierkonstruktor.
// Folgende Standard Definition implizit vorhanden.
// Elementweises kopieren.
MyInt(MyInt& src) { this->x = src.x; }
// Zuweisungsoperator (methode).
// Folgende Standard Definition implizit vorhanden.
// Elementweises kopieren.
// 'const' weil die Quelle (src) nicht veraendert werden soll.
MyInt& operator=(const MyInt& src) {
this-> x = src.x;
return *this;
}
};
Beachte:
Referenzübergabe im Fall des Kopierkonstruktors (sonst zyklische Abhängigkeit).
Zuweisung
intern dargestellt als
Methode (=)
aufgerufen auf f
(linke Seite)
mit Argument c
(rechte Seite).
#include <stdio.h>
class MyInt {
int x;
public:
MyInt(int y=0) { x=y;}
// Kopierkonstruktor.
// Folgende Standard Definition implizit vorhanden.
// Elementweises kopieren.
// 'const' weil die Quelle (src) nicht veraendert werden soll.
MyInt(const MyInt& src) { this->x = src.x; }
// Wiederholung.
// Notation: this->x
// this ist der Zeiger auf das aktuelle Objekt.
// this->x entspricht (*this).x
// Zuweisungsoperator (methode).
// Folgende Standard Definition implizit vorhanden.
// Elementweises kopieren.
MyInt& operator=(const MyInt& src) {
this-> x = src.x;
return *this;
}
void print() {
printf("\n %d", x);
}
};
void g(MyInt e) {
e.print();
}
int main() {
MyInt c(1);
MyInt d(c);
MyInt e = c;
MyInt f;
f = c;
g(f);
}
Wir betrachten kopieren von Objekte welche als Elemente Pointer haben.
Was passiert im folgenden Fall?
class MyInt {
public:
MyInt(int y=0) { x = new int(y); }
~MyInt() { delete x; }
int* x; // POINTER
};
void f(MyInt d) {
*d.x = 0;
}
int main() {
MyInt c = MyInt(1);
f(c);
}
Beachte. Resource Acquisition Is Initialization (RAII) wird befolgt.
Aber. Bei Ausführung stürzt Programm ab. Ein delete zu viel!
int
Objektes.Objekt c
befindet sich auf dem Stack und so auch der
Zeiger c.x
.
c.x
verweist auf dynamisch (auf dem Heap) angelegtes
int
Objekt.
d.x
und c.x
verweisen auf das gleiche
(Heap) Objekt.
Objekt d
angelegt auf Stackbereich der Funktion
f
.
d
bei
FunktionsaustrittAufruf des Destruktors der Klasse MyInt
auf
d
.
Dadurch Freigabe des belegten Speichers des dynamischen
int
Objektes (delete d.x
).
Zeiger d.x
des lokalen Objekts d
und
c.x
des in main deklarierten Objekts c
verweisen auf den gleichen Speicherbereich!
main
Freigabe von c
bei Programmende (Funktionsaustritt
main
).
Destruktoraufruf auf d
. Dadurch Aufruf
delete c.x
.
Es kracht weil d.x
und c.x
auf den gleichen
Speicherbereich verweisen, und ein Speicherbereich nur einmal
freigegeben werden kann.
Das beobachtete Problem tritt ein weil C++ bei Zuweisung und Kopieren von Objekten immer eine “shallow copy” durchführt.
Shallow = Flach. Bedeutet im Fall von Zeiger werden die Speicheradressen kopiert.
D.h. die Standarddefinitionen von Kopierkonstruktor und Zuweisungsoperator wenden immer eine “shallow copy” an.
Deshalb:
Original und Kopie zeigen auf die gleiche Speicherstelle
In unserem Fall, die Zeiger d.x
und c.x
zeigen auf den gleichen Speicherbereich
Dadurch gibt es ein “delete” zu viel.
Abhilfe: “deep copy”
Anlegen von neuem Speicher. Kopiere Inhalt und nicht nur die Speicheradresse (“clone” in Java)
Muss “manuell” von Programmierer angelegt werden
Standarddefinitionen von Kopierkonstruktor und Zuweisungsoperator können geeignet definiert werden
class MyInt {
public:
MyInt(int y=0) { x = new int(y); }
~MyInt() { delete x; }
// Kopierkonstruktor ("deep copy")
MyInt(const MyInt& src) {
x = new int();
*x = *src.x;
// Geht auch in einer Zeile.
// x = new int(*src.x);
}
// Zum Vergleich "shallow copy"
// MyInt(const MyInt& src) { x = src.x; }
// Zuweisung ("deep copy")
MyInt& operator=(const MyInt& src) {
if(this != &src) {
delete x;
x = new int(*src.x);
}
return *this;
}
int* x;
};
Bedeutung von const MyInt& src
:
Objekt bleibt unverändert/konstant
Übergabe als Referenz (wichtig!)
Falls Übergabe per Kopie befinden wir uns in einer unendlichen Schleife
Der Kopierkonstruktor versucht sich quasi selbst aufzurufen
Definition von
Destruktor
Kopierkonstruktor (“deep copy”)
Zuweisung via Kopie (“deep copy”)
auch bekannt als “Rule of Three” in C++.
In der Regel, Konstruktor legt Resource an (“new”) und Destruktor handhabt Resource Freigabe (“delete”). Kopierkonstrutor und Zuweisung handhaben Weitergabe der Resource.
Durch eine “shallow” copy entsteht das sogenannte “aliasing” Problem.
Zwei Zeiger verweisen auf den gleichen Speicherbereich.
Weitere Abhilfen:
Bisher.
“shallow copy” ist zu unsicher.
“deep copy” ist nicht sehr performant.
Im folgenden, copy = deep copy.
Wie wäre ein “move”?
Kopiere Pointer
Invalidiere “alten” Pointer (wird auf nullptr
)
gesetzt.
delete nullptr
ohne Effekt
#include <iostream>
#include <string>
using namespace std;
class MyInt {
public:
int* x;
MyInt(int y=0) { x = new int(y); }
~MyInt() { delete x; }
MyInt(MyInt& src) {
this->x = src.x;
src.x = nullptr;
}
MyInt& operator=(MyInt& src) {
if(this != &src) {
delete this->x;
this->x = src.x;
src.x = nullptr;
}
return *this;
}
bool isNull() {
return x == nullptr;
}
};
void foo(MyInt z) {
*z.x = 3;
}
int main() {
MyInt x(5);
MyInt y;
x = y;
foo(x);
cout << x.isNull() << " " << y.isNull() ;
}
Beachte “shallow copy” !=
“move”.
Im Fall von “move” wird Pointer auf nullptr
gesetzt.
Beachte: delete nullptr
hat keinen Effekt.
Beobachtungen:
Nach x = y
ist ein Zugriff auf y
nicht
mehr möglich.
Ist so gewollt, da ein “move” stattfindet.
Aber im Fall von foo(x)
findet auch ein “move”
statt!
Danach ist ein Zugriff auf x
nicht mehr
möglich.
Weitere Einschränkungen:
foo(MyInt(4))
x = MyInt(4)
nicht erlaubt, da Argumente von Kopierkonstruktor/Zuweisung lvalues
sein müssen. Aber MyInt(4)
ist ein rvalue!
“copy” für lvalues (“Werte mit fester Speicheradresse”)
“move” für rvalues (“flüchtige/temporäre Werte”)
An bestimmten Programmpunkt (manuell), transformiere lvalue in ein rvalue, um einen “move” anstatt “copy” auszuführen
Weiteres Vorgehen.
lvalues versus rvalues
C++11 rvalue references und “move” Semantik
Jeder Ausdruck (expression) hat
einen Typ, kann statisch (= Compilezeit) bestimmt werden, und
und einen Wert der erst dynamisch (= Laufzeit) feststeht.
Was bedeutet lvalue und rvalue?
lvalue steht links von Zuweisung?
rvalue steht rechts von Zuweisung?
Ein paar Beispiele.
int inc (int x) {
return x+1;
}
...
int x;
int* p;
int const y = 5;
x = 5;
p = &x;
x = inc(x);
*p = 6;
y = 7; // Compiler meckert, const kann nicht links stehen
“return values” können rvalues sein.
#include <iostream>
#include <string>
using namespace std;
int cnt = 0;
int inc() {
cnt++;
return cnt;
}
int get() {
return cnt;
}
int& access() {
return cnt;
}
/*
// Won't compile, ref to stack object.
int& access2() {
int cnt = get();
return cnt;
}
*/
int inc2(int& x) {
return x+1;
}
int inc3(const int& x) {
return x+1;
}
void testInc2() {
int y;
inc2(y);
// inc2(10); // Compiler meckert, lvalue erwartet
inc3(10); // OK weil const
// Compiler baut
// int _x10 = 10;
// inc3(_x10);
}
class Foo {
int x;
public:
Foo(int x) { this->x = x; }
};
int main() {
int x;
x =inc();
inc();
access() = get(); // OK
cout << x << " " << get();
Foo(2) = f; // OK, temporary object
return 1;
}
lvalues sind lokalisierbar (Localizable), d.h. erreichbar wie Speicheradresse.
rvalues sind lesbar (Readable).
Recht grobe Charakterisierung. Details siehe C++ value categories
lvalue reference: int &ref1 = x;
(C++)
rvalue reference: int &&ref1 = 5;
(C++11)
lvalue/rvalue references müssen initialisiert werden (es sei denn Parameter)
lvalue reference verlangt initialen lvalue
rvalue reference verlangt initialen rvalue
Idee:
copy für lvalue references (lokalisierbare Werte)
move für rvalue references (temporäre Werte)
Ausnahmen mit Hilfe von
std::move
T
in ein “moveable” Objekt
vom Type &&T
Verlangt C++11 compiler, auf dem mac z.B. via
g++ -std=c++11
.
#include <iostream>
#include <string>
using namespace std;
class MyInt {
public:
int* x;
MyInt(int y=0) { cout << "init \n"; x = new int(y); }
~MyInt() {
if (x == nullptr) {
cout << "destr nope \n";
} else {
cout << "destr \n";
}
delete x;
}
// copy
MyInt(const MyInt& src) {
x = new int();
*x = *src.x;
// x = new int(*src.x);
cout << "copy Cons\n";
}
// copy
MyInt& operator=(const MyInt& src) {
if(this != &src) {
delete x;
x = new int(*src.x);
}
cout << "copy =\n";
return *this;
}
// move
MyInt(MyInt&& src) {
this->x = src.x;
src.x = nullptr;
cout << "move Cons\n";
}
// move
MyInt& operator=(MyInt&& src) {
if(this != &src) {
delete this->x;
this->x = src.x;
src.x = nullptr;
}
cout << "move =\n";
return *this;
}
bool isNull() {
return x == nullptr;
}
};
void foo(MyInt z) {
*z.x = 3;
}
int main() {
MyInt x(5);
MyInt y;
x = y;
x = MyInt(3);
foo(MyInt(4));
x = move(y);
x = MyInt(3);
cout << x.isNull() << " " << y.isNull() ;
}
C++ Rule of Three
C++11 Rule of Five
Definiere (geeignet)
Destruktor
“copy” Konstruktor (für lvalue refs)
“copy” Zuweisung (für lvalue refs)
“move” Konstruktor (für rvalue refs)
“move” Zuweisung (für rvalue refs)
Move semantics and rvalue references in C++11
C++ Rvalue References Explained
C++11 “smart” pointers
Wrapper Klasse, um
dynamische angelegte Resourcen zu verwalten (z.B. ein “raw” C++ pointer),
die Resource wird freigegeben, sobald Lebenszeit des Objektes zu Ende (via Destruktor),
kein “copy” nur “move”, deshalb “unique”.
Es gibt einen (“unique”) Besitzer (“owner”).
Mehrere “owner”
Verwendet “reference counting”
Letzte “owner” gibt Resource frei oder Zuweisung an anderen Pointer (“ownership transfer”)