Open Source im professionellen Einsatz
Linux-Magazin 04/2013

Modernes C++ in der Praxis – Folge 9

Klug aufgeräumt

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.

1337

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 }

Abbildung 2: Automatische Speicherverwaltung: Die Instanzen von MyInt melden sich an und ab.

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.

Abbildung 3: Automatische Speicherverwaltung mit einer eigenen Löschfunktion.

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 }

Diesen Artikel als PDF kaufen

Express-Kauf als PDF

Umfang: 4 Heftseiten

Preis € 0,99
(inkl. 19% MwSt.)

Linux-Magazin kaufen

Einzelne Ausgabe
 
Abonnements
 
TABLET & SMARTPHONE APPS
Bald erhältlich
Get it on Google Play

Deutschland

Ähnliche Artikel

  • C++11

    Die neuen Smart Pointer in C++11 machen den Zugriff auf Ressourcen transparent und räumen hinter sich auf. Dieser Artikel stellt als ersten Vertreter den Unique Pointer vor.

  • C++11

    Klein, aber oho: Platziert ein Entwickler drei Punkte ("…") geschickt an der richtigen Stelle im C++-Code, entpacken die so genannten Variadic Templates ihre Argumente an Ort und Stelle.

  • C++11

    Diese Folge geht ans Eingemachte von C++11: Sie zeigt, wozu Move-Semantik nützlich ist, erklärt Rvalues und Lvalues und deckt auf, was es mit dem doppelten &-Zeichen auf sich hat.

  • Reichhaltiges Angebot

    C++0x bringt nicht nur Veränderungen in der Kernsprache. Die Standardbibliothek der C++-Neuausgabe hat Multithreading, asynchrone Funktionsaufrufe, reguläre Ausdrücke und vieles mehr im Angebot.

  • C++

    Verantwortung abgeben fällt C++-Entwicklern nicht immer leicht. Dabei trifft der Compiler fast immer die besseren Entscheidungen, wie der vorliegende Artikel zeigt.

comments powered by Disqus

Ausgabe 08/2017

Digitale Ausgabe: Preis € 6,40
(inkl. 19% MwSt.)

Artikelserien und interessante Workshops aus dem Magazin können Sie hier als Bundle erwerben.