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

Quellcode

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