Schwache Zeiger
Wenn man versucht, nur mit std::shared_ptr zu arbeiten, dass stößt man früher
oder später auf ein Problem: kreisförmige Objektbeziehungen machen unsere
schöne, neue, automatisch aufräumende Welt wieder kaputt. Referenzieren sich
zwei oder mehr Objekte im Kreis, dann können die über ihre shared_ptr selbst
dann nicht gelöscht werden, wenn sich außerhalb des Kreises keiner mehr dafür
interessiert. Um diese Kreise zu unterbrechen und trotzdem die Annehmlichkeiten
eines Smart Pointers zu haben, bietet die Standardbibliothek std::weak_ptr.
Video
Code
#include <iostream>
#include <memory>
class Test
{
Test(Test const &) {}
Test &operator=(Test const &) {}
public:
Test() = default;
~Test()
{
std::cerr << "Test wurde zerstört\n";
}
void sayHello() const
{
std::cout << "Hallo\n";
}
};
int main()
{
std::weak_ptr<Test> w;
{
std::shared_ptr<Test> t(new Test);
std::cerr << "Anzahl shared_ptr: " << t.use_count() << std::endl;
w = t;
std::cerr << "weak_ptr abgelaufen: " << w.expired() << std::endl;
std::shared_ptr<Test> tempT = w.lock();
std::cerr << "Anzahl shared_ptr: " << t.use_count() << std::endl;
tempT->sayHello();
}
std::cerr << "Scope verlassen\n";
std::cerr << "weak_ptr abgelaufen: " << w.expired() << std::endl;
}
Erklärung
Der anonyme Block in Zeile 28 bis 41 dient hier der Verdeutlichung des Prinzips
std::weak_ptr. Der eigentliche weak_ptr w lebt länger als alle shared_ptr
innerhalb dieses Blocks. Wäre w ein klassischer shared_ptr, dann würde die
Zuweisung in Zeile 33 den Referenzzähler des Pointer um eins erhöhen und das
Test-Objekt würde den Block überleben. Das wollen wir aber nicht. w soll uns
zwar die Möglichkeit geben, auf das entsprechende Objekt zuzugreifen, aber nur
dann, wenn der eigentliche "Aufhängepunkt", der zugehörige shared_ptr noch
nicht vernichtet wurde. Zu diesem Zweck können wir w in Zeile 35 mittels
w.expired() fragen, ob der dahinter stehende shared_ptr noch gültig ist. Ist
das der Fall, so können wir eine neue Kopie dieses Pointer mittels w.lock()
erhalten (Zeile 37), normal benutzen und dann wieder vernichten.
Fällt nun am Ende des Blocks der shared_ptr t aus dem Scope und damit dessen
Referenzzähler auf 0, so wird das Test-Objekt freigegeben (Zeile 41). Wenn wir
danach den weak_ptr fragen, ob er noch gültig ist, dann wird der das
verneinen.
Wie kann man das nun nutzen, um kreisförmige Beziehungen zu bauen? Nehmen wir
an, wir haben ein Objekt A, welches eine Referenz auf ein anderes Objekt B hat.
B hat ebenso eine Rückreferenz zu A. Sind beide Referenzen mittels shared_ptr
implementiert, dann entsteht das bekannte Problem. Implementieren wir hingegen
B->A als weak_ptr und A->B als shared_ptr, so bleiben beide Objekte nur dann
erhalten, solange es noch eine Referenz von außen auf A gibt. Ist das nicht mehr
der Fall, dann wird A vernichtet, damit auch A->B, weswegen B ebenfalls
freigegeben wird. Dadurch, das B->A ein weak_ptr ist, zählt er nicht für die
Lebenszeit des Objektes.
Durch diesen Entwurf entsteht natürlich eine gewisse Hierarchie zwischen den
Objekten. B wird als abhängiges Objekt in A geführt, umgekehrt aber nicht. In
den allermeisten Fällen ist das auch genau das richtige Design. Sollten beide
Objekte gleichberechtigt sein, dann wird's komplizierter. Entweder nimmt man nur
weak_ptr zwischen beiden Objekten und macht sie damit faktisch unabhängig
voneinander oder man arbeitet mit zwei shared_ptr und muss dann selbst für das
Auftrennen des Kreises sorgen (bspw. indem man einen der shared_ptr mittels
.reset() zurücksetzt). In dem Fall wäre es aber durchaus nochmal
empfehlenswert, das eigene Design zu überdenken.