Der Smart Pointer gilt als der klügste seiner Art in C++11. Sein Fachgebiet ist die elegante Garbage Collection, manchmal muss aber sein kleiner Bruder Weak Pointer mithelfen.
Ein unrühmliches Alleinstellungsmerkmal der Sprache C++ war lange, dass sie belegten Speicher nicht selbst aufräumte, also keine Garbage Collection anbot. Mit C++11 hat sich das geändert, denn der neue Standard bietet explizite, automatische Speicherverwaltung in Form von Reference Counting. Das vermeidet Effekte wie Programmabbrüche infolge doppelt gelöschter, dynamisch allokierter Variablen oder das ungebremste Vergrößern der Anwendung infolge vergessener Speicherbereinigung. Dieser Artikel zeigt, wie der C++11-Programmier Reference Counting mit Hilfe der neuen Shared Pointer »std::shared_ptr« einsetzt.
Der einfache Anwendungsfall
Der Shared Pointer ist der klügste aller Smart Pointer in C++11. Im Gegensatz zum Unique Pointer »std::unique_ptr« [1] teilt er sich den Besitz an einer Ressource, aber im Unterschied zum Weak Pointer »std::weak_ptr« besitzt er die Ressource. Die Idee hinter dem Shared Pointer ist einfach und mächtig zugleich: Jeder Shared Pointer hält zwei Verweise, einen auf die Ressource, die er kapselt, einen anderen auf den Referenzzähler, wie Abbildung 1 zeigt.

Abbildung 1: Der Shared Pointer hält einen Verweis auf seine Ressource, der Referenzzähler zählt mit.
Zähl mich!
Der Einfachheit halber spricht dieser Artikel im Weiteren von der Ressource als dynamischer Variablen, obwohl dies nicht notwendigerweise so sein muss. Denn mit einem Smart Pointer lässt sich der Lebenszyklus einer beliebigen Ressource, beispielsweise eines Datei-Objekts, verwalten. Wird ein Shared Pointer kopiert, inkrementiert er seinen Referenzzähler. Verliert er seine Gültigkeit, dekrementiert er diesen. Erreicht der Referenzzähler den Wert 0, löscht C++11 automatisch die dynamische Variable.
Kurz gesagt: Der Smart Pointer ist ein Pointer, da er sich wie ein Pointer verhält, und smart, da er zusätzliche Intelligenz besitzt: das Lebenszeitmanagement seiner Ressource.
Listing 1 zeigt den einfachen Umgang mit dem Shared Pointer. »MyInt« (Zeilen 9 bis 17) stellt einen einfachen Wrapper um eine natürliche Zahl dar. Instanzen dieses Datentyps teilen durch die Ausgabe von »Hello« und »Good Bye« mit, wann das Programm sie konstruiert und destruiert, wie in Abbildung 2 zu sehen.
Listing 1
std::shared_ptr im Einsatz
01 #include <iostream>
02 #include <memory>
03
04 using std::cout;
05 using std::endl;
06
07 using std::shared_ptr;
08
09 struct MyInt{
10 MyInt(int v):val(v){
11 cout << " Hello: " << val << endl;
12 }
13 ~MyInt(){
14 cout << " Good Bye: " << val << endl;
15 }
16 int val;
17 };
18
19 int main(){
20
21 cout << endl;
22
23 shared_ptr<MyInt> sharPtr(new MyInt(1998));
24 cout << " My value: " << sharPtr->val << endl;
25 cout << "sharedPtr.use_count(): " << sharPtr.use_count() << endl;
26 {
27 shared_ptr<MyInt> locSharPtr(sharPtr);
28 cout << "locSharPtr.use_count(): " << locSharPtr.use_count() << endl;
29 }
30 cout << "sharPtr.use_count(): "<< sharPtr.use_count() << endl;
31
32 shared_ptr<MyInt> globSharPtr= sharPtr;
33 cout << "sharPtr.use_count(): "<< sharPtr.use_count() << endl;
34 globSharPtr.reset();
35 cout << "sharPtr.use_count(): "<< sharPtr.use_count() << endl;
36
37 sharPtr= shared_ptr<MyInt>(new MyInt(2011));
38
39 cout << endl;
40 }
Der Code instanziiert zunächst den Shared Pointer »sharPtr« in Zeile 23 mit der dynamisch allokierten Variablen »new MyInt(1998)« . Da er alleiniger Besitzer der Ressource ist, ergibt der Aufruf »sharedPt.use_count« in Zeile 25 den Wert 1. Dies ändert sich im lokalen Bereich der Zeilen 26 bis 29, denn die Copy-Initialisierung des Shared Pointer »locSharPtr« in Zeile 27 führt dazu, dass die Ressource nun zwei Herren besitzt.
Mit dem Ende des Blocks in Zeile 29 verliert der Shared Pointer »locSharPtr« seine Gültigkeit und wird destruiert, sodass »sharPtr.use_count« wieder den Wert 1 liefert. Der Aufruf »globShrPtr.reset()« in Zeile 34 gibt die Ressource explizit frei. Implizit hingegen geschieht dies in Zeile 37, denn durch den Ausdruck
sharPtr=shared_ptr<MyInt>(new MyInt(2011))
gibt der Shared Pointer »sharPtr« seine Ressource frei und wird Besitzer der neuen Ressource »new MyInt(2011)« . Da er der letzte Besitzer der Ressource »new MyInt(1998)« war, erreicht der Referenzzähler den Wert 0, die C++11-Laufzeit bereinigt den Speicher automatisch.
Hier hört aber die Funktionalität des Shared Pointer bei Weitem noch nicht auf. Dem Programmierer steht es beispielsweise frei, das Verhalten der Löschfunktion anzupassen. Einen Anwendungsfall zeigt das Listing 2, denn der »Deleter« (Zeilen 18 bis 25) zählt durch seine statische Variable »count« mit, wie viele Objekte er mittels seiner Löschfunktion destruiert hat.
Listing 2
Shared Pointer mit eigener Löschfunktion
01 #include <iostream>
02 #include <memory>
03
04 using std::cout;
05 using std::endl;
06 using std::shared_ptr;
07
08 struct MyInt{
09 MyInt(int v):val(v){
10 cout << " Hello: " << val << endl;
11 }
12 ~MyInt(){
13 cout << " Good Bye: " << val << endl;
14 }
15 int val;
16 };
17
18 template <typename T>
19 struct Deleter{
20 void operator()(T *ptr){
21 ++Deleter::count;
22 delete ptr;
23 }
24 static int count;
25 };
26
27 template <typename T>
28 int Deleter<T>::count=0;
29
30 typedef Deleter<int> IntDeleter;
31 typedef Deleter<double> DoubleDeleter;
32 typedef Deleter<MyInt> MyIntDeleter;
33
34 int main(){
35 cout << endl;
36 {
37 shared_ptr<int> sharedPtr1(new int(1998),IntDeleter());
38 shared_ptr<int> sharedPtr2(new int(2011),IntDeleter());
39 shared_ptr<double> sharedPtr3(new double(3.17),DoubleDeleter());
40 shared_ptr<MyInt> sharedPtr4(new MyInt(2017),MyIntDeleter());
41 }
42
43 cout << "Deleted " << IntDeleter().count << " int values." << endl;
44 cout << "Deleted " << DoubleDeleter().count << " double value." << endl;
45 cout << "Deleted " << MyIntDeleter().count << " MyInt value." << endl;
46
47 cout << endl;
48 }
Dazu braucht er lediglich die Shared Pointer in den Zeilen 37 bis 40 über die Funktionsobjekte zu parametrisieren. Die eigentliche Funktionalität steckt in deren überladenem Klammeroperator in Zeile 20, der zuerst den Zähler erhöht und dann die Ressource destruiert.
Prinzipiell lässt sich zum Löschen auch eine Funktion oder eine Lambdafunktion verwenden. In diesem konkreten Fall bietet sich aber ein Funktionsobjekt an, da es einen Zustand – den Wert seiner statischen Zählvariablen »count« – aufbauen kann. Abbildung 3 zeigt das Programm im Einsatz.
Während der Shared Pointer ein mächtiges Interface anbietet [2], ist sein kleiner Bruder Weak Pointer »std::weak_ptr« [3] genau genommen gar kein Smart Pointer. Er erlaubt schließlich keinen transparenten Zugriff auf die Ressource. Zwar lässt sich mit ihm eine Ressource teilen, besitzen kann sie der Weak Pointer aber nicht, denn tatsächlich leiht er sich die Ressource nur von einem Shared Pointer aus. Dabei verändert er den Referenzzähler nicht. Listing 3 demonstriert die eingeschränkte Funktionalität des Weak Pointer.
Listing 3
Das einfache Interface des Weak Pointer
01 #include <iostream>
02 #include <memory>
03
04 using std::cout;
05 using std::endl;
06 using std::boolalpha;
07 using std::weak_ptr;
08 using std::shared_ptr;
09
10 int main(){
11 cout << boolalpha<< endl;
12
13 auto sharedPtr=std::make_shared<int>(2011);
14 weak_ptr<int> weakPtr(sharedPtr);
15 cout << "weakPtr.use_count(): " << weakPtr.use_count() << endl;
16 cout << "weakPtr.expired(): " << weakPtr.expired() << endl;
17
18 if(shared_ptr<int> sharedPtr1 = weakPtr.lock()) {
19 cout << "*sharedPtr: " << *sharedPtr << endl;
20 }
21 else{
22 std::cout << "Don't get the resource!" << std::endl;
23 }
24
25 weakPtr.reset();
26 if(shared_ptr<int> sharedPtr1 = weakPtr.lock()) {
27 cout << "*sharedPtr: " << *sharedPtr << endl;
28 }
29 else{
30 cout << "Don't get the resource!" << endl;
31 }
32
33 cout << endl;
34 }
Kleiner Bruder »weak_ptr«
Zeile 13 erzeugt mit der Templatefunktion »std::make_shared()« den Shared Pointer »sharedPtr« , der eine dynamisch allokierte »int« -Variable mit dem Wert 2011 besitzt. Dieser Shared Pointer kommt in der nächsten Zeile zum Einsatz, um den Weak Pointer »weakPtr« zu initialisieren. Schön ist in Abbildung 4 zu sehen, dass der Weak Pointer den Referenzzähler nicht erhöht hat, aber sich trotzdem die Ressource mit einem Shared Pointer teilt. Der Aufruf »weakPtr.lock()« in Zeile 18 erzeugt einen neuen Shared Pointer, mit dem der Programmierer auf die Ressource in Zeile 19 direkt zugreifen kann. Die Anweisung »weak_ptr.reset()« in Zeile 25 gibt die Ressource schließlich wieder frei.
Zyklische Referenzen aufbrechen
Zugeben, der Umgang mit dem Weak Pointer ist relativ umständlich, denn der Zugriff auf die Ressource ist nur über einen Shared Pointer möglich. Für welchen Einsatzbereich ist der Weak Pointer also konzipiert?
Das klassische Problem von Garbage Collection via Reference Counting besteht darin, dass so genannte zyklische Referenzen entstehen, wenn Smart Pointer wie die Shared Pointer wechselseitig aufeinander verweisen. So erreicht der Referenzzähler niemals den Wert 0 und die Ressource lässt sich nicht automatisch löschen. Hier hilft der Kunstgriff, einen Weak Pointer statt eines Shared Pointer in den Zyklus aufzunehmen. Da dieser den Referenzzähler nicht erhöht, lässt sich der Teufelskreis brechen.
Mutter und Kinder
In Abbildung 5 ist ein einfacher Anwendungsfall dargestellt. Während der Sohn und die Tochter mit einem Shared Pointer auf ihre Mutter verweisen, zeigt die Mutter zwar mit einem Shared Pointer auf ihren Sohn, hingegen mit einem Weak Pointer auf ihre Tochter. Die Grafik macht auf einen Blick anschaulich, was im Listing 4 weniger leicht zu erkennen ist: Die Mutter besitzt mit ihrem Sohn eine zyklische Referenz. In der Main-Funktion kommt in den Zeilen 38 bis 44 ein künstlicher Block zum Einsatz, um einfach darstellen zu können, dass die Tochter-Ressource automatisch freigegeben wird.
Listing 4
Zyklische Referenz
01 #include <iostream>
02 #include <memory>
03
04 using std::cout;
05 using std::endl;
06 using std::weak_ptr;
07 using std::shared_ptr;
08
09 struct Son;
10 struct Daughter;
11
12 struct Mother{
13 ~Mother(){cout << "Mother gone" << endl;}
14 void setSon(const shared_ptr<Son> s ){
15 mySon=s;
16 }
17 void setDaughter(const shared_ptr<Daughter> d ){
18 myDaughter=d;
19 }
20 shared_ptr<const Son> mySon;
21 weak_ptr<const Daughter> myDaughter;
22 };
23
24 struct Son{
25 Son(shared_ptr<Mother> m):myMother(m){}
26 ~Son(){cout << "Son gone" << endl;}
27 shared_ptr<const Mother> myMother;
28 };
29
30 struct Daughter{
31 Daughter(shared_ptr<Mother> m):myMother(m){}
32 ~Daughter(){cout << "Daughter gone" << endl;}
33 shared_ptr<const Mother> myMother;
34 };
35
36 int main(){
37 cout << endl;
38 {
39 shared_ptr<Mother> mother= shared_ptr<Mother>( new Mother);
40 shared_ptr<Son> son= shared_ptr<Son>( new Son(mother) );
41 shared_ptr<Daughter> daugher= shared_ptr<Daughter>( new Daughter(mother) );
42 mother->setSon(son);
43 mother->setDaughter(daugher);
44 }
45 cout << endl;
46 }

Abbildung 5: Die Beziehung der Mutter zu den Kindern: Mit zwei Shared Pointers (rechts) ist sie zyklisch, links bricht ein Weak Pointer die zyklische Referenz auf.
Der entscheidende Unterschied findet sich aber in der Implementierung der Klasse »Mother« (Zeilen 12 bis 22), denn in ihr verweist die Mutter auf ihren Sohn mit einem Shared Pointer (Zeile 20), hingegen auf die Tochter mit einem Weak Pointer (Zeile 21). Abbildung 6 zeigt, dass das Programm tatsächlich nur die Tochter automatisch freigibt.
Die Entwarnung
Gemeinsam genutzte Variablen bedürfen besonderer Aufmerksamkeit, wenn mehrere Threads gleichzeitig darauf zugreifen. Die Idee eines Shared Pointer ist es aber gerade, dass sich diese eine Ressource teilen. Wie passt das zusammen? Ganz einfach: Der neue C++-Standard garantiert dem Programmierer zum einen, dass das Ändern des Referenzzählers eine atomare Operation ist, zum anderen, dass der Destruktor des Shared Pointer nur einmal aufgerufen wird. Der Zugriff auf die Ressource ist nicht Thread-safe und muss daher im Zweifelsfall synchronisiert werden.
Wie geht es weiter?
Das Arbeiten mit Strings war keine besondere Stärke von C++. Da nimmt es nicht wunder, dass die Bibliothek für reguläre Ausdrücke in C++11 den Entwicklern ähnlich willkommen ist wie die neuen Smart Pointer. Schließlich erlauben es reguläre Ausdrücke, Muster in Texten zu identifizieren oder zu modifizieren. Der nächste Artikel wird zeigen, dass dies bei Weitem noch nicht alles ist, was die regulären Ausdrücke in C++11 zu bieten haben. (mhu)
Infos
- Rainer Grimm, “Räumkommando”: Linux-Magazin 02/13, S. 90
- »shared_ptr« : http://en.cppreference.com/w/cpp/memory/shared_ptr
- »weak_ptr:« http://en.cppreference.com/w/cpp/memory/weak_ptr
- Listings zu diesem Artikel: https://www.linux-magazin.de/static/listings/magazin/2013/04/cpp/










