Smart Pointer

Einführung/Überblick

In neuen Versionen von C++ findet man eine Alternative zu new/delete Funktionen, die die Effizienz dynamischer Objekte mit dem Besitzverhalten eines Wertes oder Containers kombinieren. Dadurch kann das korrekte Löschen der Objekte automatisert werden, wenn keine Referenzen auf das Objekt mehr bestehen:

Dereferenzieren funktioniert, außer bei weak_ptr, genauso wie bei einfachen Pointern, get() gibt einen einfachen Pointer auf das Objekt zurück. Bei weak_ptr erfolgt der Zugriff auf das Objekt durch Aufruf der Funktion lock() , um zu testen, ob der zugehörige shared_ptr noch gültig ist. Für die Nutzung der Smart Pointer muss der Header <memory> eingebunden werden. Die Smart Pointer sind Teil der Standartbibliothek std. Wenn der Smart Pointer erstellt wird, wird Speicherplatz reserviert und das Objekt konstruiert. Smart Pointer selber werden wie andere Daten automatisch gelöscht, wenn sie die Gültigkeit verlieren. Gegenüber klassischer Speicherverwaltung mit new und delete haben Smart Pointer mehrere Vorteile:

Smart Pointer können auch in Feldern gespeichert werden, etwa zur Verwaltung mehrerer polymorpher Objekte. Während bei klassischen Pointern die Objekte des Feldes explizit gelöscht werden müssen, werden bei Smart Pointern die Destruktoren korrekt aufgerufen.

unique-pointer

unique-pointer sind sehr ressourcenschonend: Sie brauchen genauso viel Speicherplatz wie ein normaler Pointer. Wie der Name schon verrät, hat das Objekt, auf den der unique-ptr zeigt, genau einen Besitzer. Das heißt, dass es genau eine Instanz eines Objekt gibt. Dementsprechend kann dieser Pointer auch nicht einfach in einen anderen Pointer kopiert werden. Eine Alternative ist der move()-Befehl. Dieser überträgt den Wert des einen Pointers in den gewünschten neuen Pointer oder die gewünschte Funktion. Der ursprüngliche Pointer ist danach ein nullptr und wird nach Beendigung der Funktion/des Programms gelöscht. Genauso kann man den Wert des Pointers (also das Objekt, auf das gezeigt wird) nicht per call by value übergeben, sondern muss per call by reference übergeben werden. Es ist zu empfehlen, den Befehl make_unique() beim Erzeugen eines Objektes zu benutzen. Wird der unique_ptr gelöscht, so wird auch das Objekt zerstört.

Beispiel:

#include <memory>
#include <iostream>
int squareIt(std::unique_ptr<int> s) {
    int squared = *s;
    return squared*squared;
}
{
    std::unique_ptr<int> number = std::make_unique<int>(15);
    
    if(number){
        std::cout << "Der Pointer `number` existiert noch und hat den Inhalt: " << *number << std::endl;
    } else {
        std::cout << "Der Pointer `number` existiert nicht mehr." << std::endl;
    }
    
    int numberSquared = squareIt(move(number));
    std::cout << "Der Wert des Pointers wurde zur Berechnung übergeben: " << numberSquared << std::endl;
    
    if(number){
        std::cout << "Der Pointer `number` existiert noch und hat den Inhalt: " << *number << std::endl;
    } else {
        std::cout << "Der Pointer `number` existiert nicht mehr." << std::endl;
    }
}

Man kann die Pointer auch in Containerklassen (siehe Abschnitt zu Containerklassen für mehr Informationen darüber) speichern.
Ein Beispiel dafür:

#include <memory>
#include <string>
#include <iostream>
#include <vector>
class Animal{
    private:
        std::string species;
        int age;
        double weight;
    public:
        std::string getSpecies(){return species;}
        int getAge(){return age;}
        double getWeight() {return weight;}
        Animal(){};
        Animal(std::string species, int age, double weight);
};
Animal::Animal(std::string species, int age, double weight):
    species(species), age(age), weight(weight)
{
    
}
{
    std::vector<std::unique_ptr<Animal>> animalVector;
    std::unique_ptr<Animal> cat = std::make_unique<Animal>("Cat", 7, 3.9);
    std::unique_ptr<Animal> dog = std::make_unique<Animal>("Dog", 11, 36.4);
    std::unique_ptr<Animal> turtle = std::make_unique<Animal>("Turtle", 52, 1.5);
    animalVector.push_back(move(cat));
    animalVector.push_back(move(dog));
    animalVector.push_back(move(turtle));
    
    if(cat){
        std::cout << "Der Pointer `cat` existiert noch mit Inhalt: " << cat->getSpecies() << ", age: " << cat->getAge() << ", weight: " << cat->getWeight() << std::endl;
    } else {
        std::cout << "Der ursprüngliche Unique_Ptr `cat` ist jetzt nullpointer." << std::endl;
    }
    
    if(dog){
        std::cout << "Der Pointer `dog` existiert noch mit Inhalt: " << dog->getSpecies() << ", age: " << dog->getAge() << ", weight: " << dog->getWeight() << std::endl;
    } else {
        std::cout << "Der ursprüngliche Unique_Ptr `dog` ist jetzt nullpointer." << std::endl;
    }
    if(turtle){
        std::cout << "Der Pointer `turtle` existiert noch mit Inhalt: " << turtle->getSpecies() << ", age: " << turtle->getAge() << ", weight: " << turtle->getWeight() << std::endl;
    } else {
        std::cout << "Der ursprüngliche Unique_Ptr `turtle` ist jetzt nullpointer." << std::endl;
    }
    
    
    for(auto& i : animalVector){
        std::cout << "Species: " << i->getSpecies() << ", Age: " << i->getAge() << " years,  Weight: " << i->getWeight() << " kg" << std::endl;
    }
}

shared-pointer

shared_ptr sind ähnlich zu unique_ptr, haben aber den großen Unterschied, dass ein Objekt mehr als einen Besitzer haben kann. Das heißt allerdings im Umkehrschluss, dass shared_ptr einen höheren Speicherbedarf und Verwaltungsaufwand haben als unique_ptr (Es ist also empfehlenswert, unique_ptr überall zu nutzen, wo es möglich ist). Die Anzahl der Referenzen/Besitzer, die auf das Objekt zeigen, werden entsprechend hoch- und runtergezählt. Wenn dieser Counter auf 0 gefallen ist (es zeigt kein shared_ptr mehr auf dieses Objekt), wird das Objekt gelöscht. Daduruch können shared_ptr auch kopiert und ohne den move()-Befehl an Funktionen oder Container übergeben werden. Das heißt, dass shared_ptr ihren Wert auch per call by value übergeben können (bspw. an Funktionen) und nicht nur per pass by reference. Es ist zu empfehlen, den Befehl make_shared() zum Erzeugen eines Shared-Pointers zu benutzen.

Das obige Beispiel sieht nun so aus:

#include <iostream>
#include <memory>
int squareIt(std::shared_ptr<int>& s) {
    return (*s)*(*s);
}
{
    std::shared_ptr<int> i_number = std::make_shared<int>(15);
    
    if(i_number){
        std::cout << "Der Pointer `i_number` existiert noch und hat den Inhalt: " << *i_number << std::endl;
    } else {
        std::cout << "Der Pointer `i_number` existiert nicht mehr." << std::endl;
    }
    
    int numberSquared = squareIt(i_number);
    std::cout << "Der Wert des Pointers wurde zur Berechnung übergeben: " << numberSquared << std::endl;
    
    if(i_number){
        std::cout << "Der Pointer `i_number` existiert noch und hat den Inhalt: " << *i_number << std::endl;
    } else {
        std::cout << "Der Pointer `i_number` existiert nicht mehr." << std::endl;
    }
}

Genauso lassen sich shared_ptr ebenfalls in Containerklassen (siehe Abschnitt zu Containerklassen für mehr Informationen darüber) speichern.
Das obige Beispiel sieht mit shared_ptr so aus:

#include <memory>
#include <string>
#include <iostream>
#include <vector>
class Animal{
    private:
        std::string species;
        int age;
        double weight;
    public:
        std::string getSpecies(){return species;}
        int getAge(){return age;}
        double getWeight() {return weight;}
        Animal(){};
        Animal(std::string species, int age, double weight);
};
Animal::Animal(std::string species, int age, double weight):
    species(species), age(age), weight(weight)
{
    
}
{
    std::vector<std::shared_ptr<Animal>> animalVector;
    std::shared_ptr<Animal> cat = std::make_shared<Animal>("Cat", 7, 3.9);
    std::shared_ptr<Animal> dog = std::make_shared<Animal>("Dog", 11, 36.4);
    std::shared_ptr<Animal> turtle = std::make_shared<Animal>("Turtle", 52, 1.5);
    animalVector.push_back(cat);
    animalVector.push_back(dog);
    animalVector.push_back(turtle);
    
    if(cat){
        std::cout << "Der Pointer `cat` existiert noch mit Inhalt: " << cat->getSpecies() << ", age: " << cat->getAge() << ", weight: " << cat->getWeight() << std::endl;
    } else {
        std::cout << "Der ursprüngliche Pointer `cat` ist jetzt nullpointer." << std::endl;
    }
    
    if(dog){
        std::cout << "Der Pointer `dog` existiert noch mit Inhalt: "  << dog->getSpecies() << ", age: " << dog->getAge() << ", weight: " << dog->getWeight() << std::endl;
    } else {
        std::cout << "Der ursprüngliche Pointer `dog` ist jetzt nullpointer." << std::endl;
    }
    if(turtle){
        std::cout << "Der Pointer `turtle` existiert noch mit Inhalt: " << turtle->getSpecies() << ", age: " << turtle->getAge() << ", weight: " << turtle->getWeight() << std::endl;
    } else {
        std::cout << "Der ursprüngliche Pointer `turtle` ist jetzt nullpointer." << std::endl;
    }
    
    
    for(auto& i : animalVector){
        std::cout << "Species: " << i->getSpecies() << ", Age: " << i->getAge() << " years,  Weight: " << i->getWeight() << " kg" << std::endl;
    }
}

weak-pointer

weak_ptr sind eine spezielle Art shared_ptr: Sie existieren nur, wenn sie mit einem zugehörigen shared_ptr verbunden sind. Sie ermöglichen Zugriff auf ein Objekt, erhöhen aber nicht den Referenzzähler des shared_ptr; sie haben also keine Besitzrechte an dem Objekt. Man benutzt weak_ptr, um sogenannte Besitzzyklen in Datenstrukturen zu vermeiden. Wenn keine shared_ptr mehr auf das Objekt zeigen, wird es gelöscht, selbst wenn noch weak_ptr darauf zeigen.
Das folgende Beispiel zeigt den sinnvollen Einsatz der weak_ptr:

#include <memory>
#include <string>
#include <list>
#include <vector>
#include <iostream>
class Person
{
    public :
        Person() = delete ;
        Person(const std::string& name , std::shared_ptr<Person> father = nullptr , std::shared_ptr<Person> mother = nullptr) ;
        virtual ~Person() ;
        void setSibling(std::weak_ptr<Person> sibling);
    private :
        std::shared_ptr<Person> p_father;
        std::shared_ptr<Person> p_mother;
        const std::string p_sName;
        std::vector<std::weak_ptr<Person>> p_siblings;
};
Person::Person(const std::string& name, std::shared_ptr<Person> father, std::shared_ptr<Person> mother) :
p_sName(name), p_father(father), p_mother(mother) 
{
    
}
void Person::setSibling(std::weak_ptr<Person> brosis)
{
    p_siblings.push_back(brosis);
}
Person::~Person()
{
    p_siblings.clear();
    std::cout << "Geloescht: " << p_sName << std::endl;
}
void test1 ()
{
    auto m1 = std::make_shared<Person>("Josef");
    auto f1 = std::make_shared<Person>("Maria");
    auto m2 = std::make_shared<Person>("Peter", m1 , f1 );
    auto f2 = std::make_shared<Person>("Birgit", m1 , f1 );
    m2->setSibling(f2);
    f2->setSibling(m2);
}
{
    std::cout << "Anfang" << std::endl;
    test1();
    std::cout << "Ende" << std::endl;
}

Wenn man statt weak_ptr im Vektor p_siblings shared_ptr speichert, wird der Speicher im Programm nicht automatisch freigegeben, da Peter und Birgit gegenseitig aufeinander zeigen. Sie können gerne an entsprechenden Stellen im Beispiel (in der Funktion setSibling() und im Vektor p_siblings) die weak_ptr durch shared_ptr ersetzen und schauen, was passiert. Erst wenn man durch weak_ptr den zyklischen Besitz auflöst, werden die Destruktoren wie erwartet aufgerufen.

Beim weak_ptr kann man abfragen, ob der zugehörige shared_ptr noch definiert ist. Dies geschieht durch die Funktion lock(). Sie liefert einen Nullzeiger, wenn das Objekt nicht mehr existiert. Diese Funktion muss eingesetzt werden, wenn man auf den Inhalt des weak_ptr zugreifen möchte. Ein weak_ptr in der Parameterliste (wie bei setSibling) macht deutlich, dass man beim Aufruf shared_ptr dort nicht mit move() einen Besitzwechsel initiieren sollte.
Ein Beispiel mit der Funktion lock():

#include <iostream>
#include <memory>
void doesWeakExistProperly(std::weak_ptr<int> weak){
    if(weak.lock()){
        std::cout << "Der Pointer wurde erfolgreich gelocked und hat den Inhalt: " << *weak.lock() << std::endl;
    } else {
        std::cout << "Der Pointer konnte nicht gelocked werden." << std::endl;
    }
}
{
    std::weak_ptr<int> weak;
    doesWeakExistProperly(weak);
    {
        auto shared = std::make_shared<int>(42);
        weak = shared;
        std::cout << "Der Pointer `weak` ist jetzt mit `shared` gepaired." << std::endl;
        doesWeakExistProperly(weak);
    }
    std::cout << "Der Pointer `shared` ist jetzt out of scope." << std::endl;
    doesWeakExistProperly(weak);
}

Übungsaufgaben zu diesem Kapitel finden Sie hier.