Advanced Services | Sharedpointer |
Im Gegensatz zu Autopointer können mehrere shared_ptr auf dasselbe Objekt zeigen. Alle diese Exemplare verfügen über einen internen Zähler, der die Anzahl der Referenzierungen speichert. Immer wenn ein Exemplar von shared_ptr einen Block verläßt wird dieser heruntergezählt. Der letzte verbleibende shared_ptr ruft dann den Destruktor.
Das folgende Beispiel erläutert das Vorgehen. Zuerst brauchen wir jedoch eine einfche Testklasse, um die Destruktoraufrufe verfolgen zui können.
class Test { public: int i; Test(int j = 0) : i(j) {} ~Test() { cout < "destructor test\n" < endl; } };
shared_ptr erlaubt (wie auch unique_ptr) die Verwendung von selbst geschriebenen Deletern bzw. Allocatern. Im folgenden werden diese passend zur Testklasse vorgestellt.
void testdeleterfree(Test* t) { std::cout << "testdeleterfree called" << std::endl; free(t); };
void testdeleternew(Test* t) { std::cout << "testdeleternew called" << std::endl; delete t; };
// void operator() ist notwendig, in welcher Form auch immer struct CustomDeleterClass { void operator()(Test* pt) { cout << "custom deleter operator() in CustomDeleterClass" << endl; delete pt; } };
struct CustomMemoryManager { Test* alloc() { cout << "custom allocator in CustomMemoryManager" << endl; return new Test(); } void operator()(Test* pt) { cout << "custom deleter operator() in CustomMemoryManager" << endl; delete pt; } };
Benötigte includes.
#include <memory> // und std:: falls der Compiler C++11 unterstützt // ansonsten mit der boost library #include <boost/tr1/memory.hpp>
Das Beispiel legt drei shared_ptr in zwei geschachtelten Blöcken an.
void std_shared_ptr_1() { cout << "std::shared_ptr 1\n" << endl; std::shared_ptr<Test> sptr1( new Test(17) ); cout << "sptr1.use_count = " << sptr1.use_count() << endl; // // dereferenzieren möglich, da * und -> überladen cout << "sptr1->i = " << sptr1->i << endl; { cout << " begin block" << endl; std::shared_ptr<Test> sptr2 = sptr1; cout << " sptr2->i = " << sptr2->i << endl; std::shared_ptr<Test> sptr3 = sptr1; sptr3->i = 42; // cout << " sptr1->i = " << sptr1->i << endl; cout << " sptr1.use_count = " << sptr1.use_count() << endl; // cout << " sptr2.use_count = " << sptr2.use_count() << endl; // cout << " sptr3.use_count = " << sptr3.use_count() << endl; // cout << " end block" << endl; } cout << "sptr1.use_count = " << sptr1.use_count() << endl; // cout << "end std::shared_ptr 1" << endl; }
Die Ausgabe
Wie man im oberen Beispiel sieht, sind operator* und operator-> überladen, auch gibt es eine Methode T * get() const die den originalen raw-Pointer zurückliefert. Diese drei Funktionen finden sich bei allen Smartpointern außer bei weak_ptr.
swap() vertauscht den Inhalt zweier shared_ptr. Das folgende Beispiel zeigt dies.
void std_shared_ptr_2() { cout << "std::shared_ptr 2\n" << endl; std::shared_ptr<Test> ptr1( new Test(17) ); std::shared_ptr<Test> ptr2( new Test(42) ); std::shared_ptr<Test> ptr3(ptr2); cout << "ptr1->i = " << ptr1->i << " : use_count = " << ptr1.use_count() << endl; cout << "ptr2->i = " << ptr2->i << " : use_count = " << ptr2.use_count() << endl; cout << "ptr3->i = " << ptr3->i << " : use_count = " << ptr3.use_count() << endl; ptr2.swap(ptr1); cout << "\nswap: ptr2 <--> ptr1\n" << endl; cout << "ptr1->i = " << ptr1->i << " : use_count = " << ptr1.use_count() << endl; cout << "ptr2->i = " << ptr2->i << " : use_count = " << ptr2.use_count() << endl; cout << "ptr3->i = " << ptr3->i << " : use_count = " << ptr3.use_count() << endl << endl; }
Die Ausgabe
Die Wirkung von reset() erkennt man am nächsten Beispiel.
void std_shared_ptr_3() { cout << "std::shared_ptr 3\n" << endl; std::shared_ptr<Test> ptr( new Test(17) ); cout << "ptr->i = " << ptr->i << " : use_count = " << ptr.use_count(); cout << " address = " << ptr.get() << endl; cout << "reset intrusive_ptr" << endl; ptr.reset(); cout << "address = " << ptr.get() << endl; }
Wie man der API entnehmen kann gibt es reset() in vier Varianten, da es möglich ist (wie bei unique_ptr) eigene Deleter und Allocator zu schreiben.
void std_shared_ptr_4() { cout << "custom-deleters\n" << endl; // Bekanntgabe eines custom-deleters, hier std::free falls speicher mit malloc angeschafft wird std::shared_ptr<Test> sp1( (Test*)malloc(sizeof(Test)) , std::free ); // custom-deleter der free() verwendet std::shared_ptr<Test> sp2( (Test*)malloc(sizeof(Test)) , testdeleterfree ); // custom deleter der delete verwendet, delete ruft den Destruktor! std::shared_ptr<Test> sp3( new Test() , testdeleternew ); }
Die Ausgabe
Die Ausgabe
void std_shared_ptr_5() { cout << "custom-deleters 2\n" << endl; // variante 1 CustomDeleterClass cdc; std::shared_ptr<Test> up1( new Test(), cdc); // variante 2 CustomMemoryManager cmm; std::shared_ptr<Test> up2( cmm.alloc(), cmm ); }
Die erste Falle ist leicht zu erkennen. Hier wurde eine Grundregel nicht befolgt und der Rawpointer noch einmal für eine zweite Initialisierung verwendet.
/*
Was man nicht machen darf:
*/
void std_shared_ptr_6()
{
cout << "std_shared_ptr 6\n" << endl;
Test* ptest = new Test(17); // gefährlich
std::shared_ptr<Test> sptr1(ptest);
cout << "sptr1.use_count = " << sptr1.use_count() << endl; //
// dereferenzieren möglich, da * und -> überladen
cout << "sptr1->i = " << sptr1->i << endl;
{
cout << " begin block" << endl;
//lokal im Block
std::shared_ptr<Test> sptr2(ptest); // falsch
//std::shared_ptr<Test> sptr2 = sptr1; // OK
cout << " sptr1.use_count = " << sptr1.use_count() << endl; //
cout << " sptr2.use_count = " << sptr2.use_count() << endl; //
cout << " end block" << endl;
}
cout << "sptr1.use_count = " << sptr1.use_count() << endl; //
cout << "end std_shared_ptr 6" << endl;
}
Der Screenshot zeigt deutlich, daß hier der Destruktor zweimal aufgerufen wird.
Dieser zweite Fall ist deutlich raffinierter. Da der Fehler grundsätzlicher Natur kann er nur durch die Einführung eines anderen Konzepts behoben werden. weak-Pointer führen hier weiter. Das korrigierte Beispiel (Aufbrechen einer cyclic reference) findet man deshalb im Kapitel zu weak-Pointern. Wir entwerfen zwei Klassen, die jeweils einen shared-Pointer halten, der auf ein Element der anderen Klasse zeigen kann. Dadurch entsteht eine zyklische Struktur, mit der ein shared-Pointer nicht umgehen kann.
Zuerst brauchen wir zwei Klassen, der Einfachheit werden Strukturen verwendet.
//cyclic reference struct B; // forward declaration struct A { shared_ptr<B> b; ~A() {cout << "~A()" << endl; } }; struct B { shared_ptr<A> a; ~B() {cout << "~B()" << endl;} };
Es reichen nun ein paar Zeilen um das Problem auszulösen.
/*
Speicher wird nicht freigegeben.
*/
void cyclicsharedpointer()
{
cout << "cyclicsharedpointer" << endl; //
shared_ptr<A> shptrA(new A); // shptrA->b share_ptr ist mit nullptr initialisiert, use_count ist 0
cout << "shptrA.use_count = " << shptrA.use_count() << endl; // 1
cout << "shptrA->b.use_count = " << shptrA->b.use_count() << endl; // 0
shptrA->b = make_shared<B>(); //
cout << "shptrA->b.use_count = " << shptrA->b.use_count() << endl; // 1
shptrA->b->a = shptrA;
cout << "shptrA.use_count = " << shptrA.use_count() << endl; // 2
}
Der Screenshot zeigt deutlich, daß der Destruktor nicht aufgerufen wird.
Durch die Anweisung in der Zeile mit roter Schrift wird der Kreis geschlossen. Fehlt diese Zeile, so wird der Speicher korrekt freigegeben.