Einführung in C++ - Teil 2

Martin Sulzmann

Inhalt

Objekte mit Zeigerelementen (dynamisch allokierten Komponenten)

Abhilfen:

Kopieren von Objekten

Wann und wie werden Objekte kopiert?

Wann:

  MyInt c(1);
  MyInt d(c);

d ist neues Objekt basierend auf einer Kopie von c

  MyInt f;
  f = c;

Überschreibe f mit c. Daher ist f eine Kopie von c.

void g(MyInt e);


 g(f);  // C/C++ call-by-value Parameteruebgabe

Parameter e gebunden an f. Daher ist e eine Kopie von f.

Wie:

Jede Klasse hat

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:

 f = c

intern dargestellt als

 f.(=)(c)

Methode (=) aufgerufen auf f (linke Seite) mit Argument c (rechte Seite).

Beispiel komplett

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

Kopieren mit Pointern

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!

Im Detail

  1. Anlegen eines dynamisch int Objektes.
  MyInt c = MyInt(1);

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.

  1. Funktionsaufruf, bilde lokale Kopie
void f(MyInt d);

  f(c);

d.x und c.x verweisen auf das gleiche (Heap) Objekt.

Objekt d angelegt auf Stackbereich der Funktion f.

  1. Speicherfreigabe von lokalem Objekt d bei Funktionsaustritt

Aufruf des Destruktors der Klasse MyInt auf d.

Dadurch Freigabe des belegten Speichers des dynamischen int Objektes (delete d.x).

void f(MyInt d) {
  *d.x = 0;
} // Destruktor Aufruf auf d => delete d.x

Zeiger d.x des lokalen Objekts d und c.x des in main deklarierten Objekts c verweisen auf den gleichen Speicherbereich!

  1. Rücksprung nach main
int main() {
  MyInt c = MyInt(1);
  f(c);
} // Destruktor Aufruf auf d => delete c.x => CRASH

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.

Zusammengefasst: Kopieren mit Pointern

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:

Abhilfe: “deep copy”

“deep copy” am Beispiel

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:

Definition von

  1. Destruktor

  2. Kopierkonstruktor (“deep copy”)

  3. 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.

“shallow copy” noch einmal

Durch eine “shallow” copy entsteht das sogenannte “aliasing” Problem.

Zwei Zeiger verweisen auf den gleichen Speicherbereich.

Weitere Abhilfen:

void f(MyInt& d) { // call-by reference
  *d.x = 0;
}
class MyInt {
  private:
   MyInt(MyInt& src){}
   MyInt&operator=(MyInt& src){}
  ...
};

“copy” versus “move”

Bisher.

“shallow copy” ist zu unsicher.

“deep copy” ist nicht sehr performant.

Im folgenden, copy = deep copy.

Wie wäre ein “move”?

Versuch der “move” Semantik am laufenden Beispiel


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

Diskussion

Beachte “shallow copy” != “move”.

Im Fall von “move” wird Pointer auf nullptr gesetzt.

Beachte: delete nullptr hat keinen Effekt.

Beobachtungen:

Weitere Einschränkungen:

nicht erlaubt, da Argumente von Kopierkonstruktor/Zuweisung lvalues sein müssen. Aber MyInt(4) ist ein rvalue!

Wunschliste

Weiteres Vorgehen.

L-Value (lvalue) versus R-Value (rvalue)

Jeder Ausdruck (expression) hat

Was bedeutet lvalue und rvalue?

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

C++11 - rvalue references und move Semantik

rvalue and lvalue references

lvalue reference: int &ref1 = x; (C++)

rvalue reference: int &&ref1 = 5; (C++11)

Idee:

Ausnahmen mit Hilfe von

std::move

Beispiel

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

Zusammenfassung

C++ Rule of Three

C++11 Rule of Five

Definiere (geeignet)

  1. Destruktor

  2. “copy” Konstruktor (für lvalue refs)

  3. “copy” Zuweisung (für lvalue refs)

  4. “move” Konstruktor (für rvalue refs)

  5. “move” Zuweisung (für rvalue refs)

Weitere Referenzen

Move semantics and rvalue references in C++11

C++ Rvalue References Explained

Anwendung

C++11 “smart” pointers

std::unique_ptr

std:unique_ptr

Wrapper Klasse, um

Es gibt einen (“unique”) Besitzer (“owner”).

std:shared_ptr

std:shared_ptr