Warum C++ std::weak_ptr gar nicht “weak” sind
In C++ 17 sind drei verschiedene Smart Pointer verfügbar: std::unique_ptr, std::shared_ptr und std::weak_ptr. Das Einsatzgebiet der beiden ersten Smart Pointers ist recht intuitiv: verwende std::unique_ptr für das Managen exklusiver Ressourcen, std::shared_ptr für geteilte Ressourcen. Was aber ist mit std::weak_ptr?
Kleine Einführung in Smart Pointer
Um std::weak_ptr zu verstehen werfen wir zuerst einen allgemeinen Blick auf die Smart Pointer. Smart Pointer sollen unser Leben vereinfachen, indem sie die bekannten “Raw Pointer” ersetzen und das Freigeben von Ressourcen selbst managen. Ich muss mich also nicht mehr selbst um das Freigeben von Ressourcen via “delete” kümmern, sondern überlasse das dem Smart Pointer. Hier ein Code-Snippet, das einen Raw Pointer einem std::unique_ptr Smart Pointer gegenüber stellt:
void rawPointer() { // Create a raw pointer Cat* p_kitty = new Cat("Nala"); // Use p_kitty... // Delete the pointer when you don't need it anymore delete p_kitty; } void smartPointer() { // Create a smart pointer std::unique_ptr<Cat> p_other_kitty(std::make_unique<Cat>("Simba")); // Use p_other_kitty... int average_weight = p_other_kitty->weight; } // p_other_kitty is deleted automatically here
Es gibt auch die Möglichkeit, den Smart Pointer schon vor Ende des Scopes zu löschen:
void deleteSmartPointer() { // Create a smart pointer std::unique_ptr<MyClass> p_toObject(std::make_unique<MyClass>()); // Do something with the smart pointer p_toObject->doSomething(); // Free the memory before exiting the code block p_toObject.reset(); // Do something else... }
Die verschiedenen Smart Pointer im Überblick
Der std::unique_ptr ist für exklusive Ressourcen zuständig. Der Pointer hat somit nur einen “Besitzer” und gibt diese Ressource frei, wenn er zerstört oder an einen anderen Besitzer gemoved wird. Ein std::unique_ptr kann nicht kopiert werden, da dann der Pointer mehrere Besitzer hätte und die Zuständigkeit beim Freigeben der Ressourcen nicht klar wäre.
Der std::shared_ptr ist für geteilte Ressourcen zuständig. So können mehrere std::shared_ptr auf dieselbe Ressource zeigen. Pro Pointer wird der Referenzzähler der Ressource erhöht, sodass jeder std::shared_ptr weiss, wie viele Pointer noch auf die Ressource zeigen. Analog wird der Referenzzähler verringert, wenn ein Pointer nicht mehr auf die Ressource zeigt. Der letzte Pointer, der auf die Ressource zeigt übernimmt deren Freigabe.
Der std::weak_ptr entsteht in den meisten Fällen durch Kopieren eines std::shared_ptr. Allerdings hat er keinen Einfluss auf den Referenzzähler der Ressource, auf die er zeigt. Er besitzt darum nur eine schwache, also “weake” Referenz auf die Ressource. std::weak_ptr können nicht dereferenziert werden. Über die Funktion expired() kann aber überprüft werden, ob der std::weak_ptr hängt, also ob das Objekt, auf das er zeigt, nicht mehr existiert. Will man allerdings prüfen, ob das Objekt noch immer existiert und dann darauf zugreifen, muss der std::weak_ptr in einen std::shared_ptr umgewandelt werden. Das geht zum Beispiel über die Funktion lock(). Sie gibt einen std::shared_ptr zurück, der null ist, wenn das zugrunde liegende Objekt nicht mehr existiert.
std::weak_ptr in Aktion
Dass der std::weak_ptr gar nicht weak ist, sondern durch seine Funktionalitäten sehr nützlich sein kann, werden die nachfolgenden drei Beispiele zeigen.
Beispiel 1 – Observer Pattern
Beim Observer Pattern informiert ein Subject seine Beobachter, sobald es sich verändert hat. Dabei kennt das Subject seine Beobachter häufig über Pointer. Da es nicht Aufgabe des Subjects ist, die Lebensdauer seiner Beobachter zu verwalten, ist die Verwendung eines std::shared_ptr nicht zu empfehlen. Das Subject muss einzig und allein wissen, ob die Beobachter noch existieren. Hierzu eignet sich der std::weak_ptr perfekt!
Beispiel 2 – Aufbrechen von std::shared_ptr Cycles
Besitzt ein Objekt A einen std::shared_ptr auf ein Objekt B und besitzt ein Objekt B einen std::shared_ptr auf A, so kann weder Objekt A noch Objekt B jemals von aussen gelöscht werden, da die Referenzzähler der beiden Objekte nie auf 0 gesetzt werden. Der Einsatz eines std::weak_ptr löst solch einen Cycle aus std::shared_ptr auf. Ersetzen wir nämlich den Pointer von A nach B durch einen std::weak_ptr, so kann B einfach gelöscht werden, da der std::weak_ptr den Referenzzähler von B nicht beeinflusst und dieser somit noch immer auf 0 ist. Das Löschen von B führt schliesslich dazu, dass auch A gelöscht werden kann.
Beispiel 3 – Caching
Wird eine Ressource in einem Cache erstellt, so macht es Sinn, dem anfordernden Objekt die Cache-Ressource via Pointer zugänglich zu machen. Der Pointer an das anfordernde Objekt muss ein Smart Pointer sein. So wird sicher gestellt, dass die Ressource gelöscht wird, wenn sie nicht mehr gebraucht wird. Auf der anderen Seite muss aber auch der Cache selbst einen Pointer auf seine Ressourcen haben. Der Pointer des Caches selbst muss allerdings ein std::weak_ptr sein. So kann der Cache herausfinden, ob die Ressource noch benutzt wird, oder ob er sie löschen kann.
Das Code-Snippet zeigt, wie eine Cache-Ressource einmalig erstellt und übergeben werden kann und wie sie gelöscht wird, sobald der Smart Pointer den Code Block von main() verlässt.
#include <iostream> #include <memory> class ResourceA{ public: ResourceA() { std::cout << "Resource created" << "\n"; } ~ResourceA() { std::cout << "Resource deleted" << "\n"; } void doSomething() { std::cout << "working..." << "\n"; } }; class MyCache { private: std::weak_ptr<ResourceA> cachedResourceA; public: std::shared_ptr<ResourceA> getResourceA() { if (!cachedResourceA.expired()) { return cachedResourceA.lock(); } // Create shared pointer to cache-resource std::shared_ptr<ResourceA> s_ptr(std::make_shared<ResourceA>()); // Save pointer to resource cachedResourceA = s_ptr; return s_ptr; } }; int main() { MyCache testObj = MyCache(); std::shared_ptr<ResourceA> sptr1 = testObj.getResourceA(); sptr1->doSomething(); std::shared_ptr<ResourceA> sptr2 = testObj.getResourceA(); sptr2->doSomething(); std::shared_ptr<ResourceA> sptr3 = testObj.getResourceA(); sptr3->doSomething(); } // sptr1, sptr2, sptr3 are deleted Output: Resource created working... working... working... Resource deleted
Quellenangaben:
- https://en.cppreference.com/w/cpp/memory/weak_ptr
- https://docs.microsoft.com/en-us/cpp/cpp/smart-pointers-modern-cpp?view=vs-2019
- Scott Meyers : Effectives Modernes C++, 1. Auflage 2015