std::tr1::shared_ptr

Drucke diesen Post This page as PDF Posted by Michael | Posted in Artikel, C++ | Posted on 04-03-2009

Tags: , , , , , , ,

1 Star2 Stars3 Stars4 Stars5 Stars (No Ratings Yet)
Loading ... Loading ...

2

Index

  1. Einführung
  2. Nachteile von Rohzeigern
  3. Vorteile des std::tr1::shared_ptr
  4. Zugriff auf Rohzeiger
  5. Schlusswort

Einführung
C++ ist eine sehr facettenreiche Sprache und als solche bietet sie eine ganze Reihe Möglichkeiten für eine Lösung der Problematik. Dabei kommt man früher oder später unausweichlich mit Zeigern in Kontakt. Für die einen ist es ein alltägliches Gebrauchswerkzeug, das Tag für Tag seinen Dienst tut, für die anderen ist diese Erfindung schlimmer als die Hölle.

Dieser Artikel wird sich damit beschäftigen, wie man Zeiger auf eine sicherere Weise nutzen kann. Die Möglichkeit die ich hier beleuchten möchte, ist der std::tr1::shared_ptr, ein sog. intelligenter Zeiger. Dabei wendet sich diese Ausarbeitung an Programmierer, die schon eine minimale Erfahrung haben, sodass ich auf Nebenthemen wie die Standardbibliothek oder den syntaktischen Gebrauch von Zeigern verzichten möchte.

Der std::tr1::shared_ptr ist dabei ein Werkzeug aus dem Arsenal der sehr nützlichen boost-Bibliothek. Diese ist unter folgendem Link zu finden: http://www.boost.org/.
Nachteile von Rohzeigern
Bevor wir uns auf die Jagt nach Vorteilen des neuen Werkzeugs machen, müssen wir uns bewusst machen, warum diese überhaupt nötig sein sollen. Was sind also die Nachteile von Zeigern, die aus dem Weg geräumt werden wollen?

Erstens muss man im Hinterkopf behalten, dass man auf dem Heap reservierten Speicher wieder freigeben muss. In manchen Fällen klingt es einfacher, als es ist. Beispielsweise könnten Schleifen oder bedingte Funktionsabbrüche im Spiel sein. Zusätzlich muss man bedenken, dass sich Code im Zuge der Wartungsarbeiten verändern und das Freigeben des Zeigers so in Gefahr bringen könnte.

Zusätzlich erschweren sie das Kopierverhalten und die Ressourcenverwaltung. Dazu mal ein anschauliches Beispiel, das in jedem Anfängerbuch Platz findet.

Code: C++ | Plain Text
  1. class Memory
  2. {
  3. public:
  4.     // ...
  5.     // Kopierverhalten?
  6.     // ...
  7.  
  8. private:
  9.     void* m_pSomeMemory;
  10. };

Im Zuge des Erstellens einer neuen Klasse muss man sich unter anderem die Frage stellen, wie das Kopierverhalten dieser Klasse sein soll. Bei einer Klasse wie dieser kann die Möglichkeit günstig sein, eine Referenzzählung vorzunehmen oder mit dem Zeiger auch den Speicherbereich, auf den das Original zeigt, zu kopieren. Falls ein Objekt gelöscht wird, bleibt der kopierte Zeiger immer noch gültig. Der Nachteil ist, dass sich durch beide Instanzen nicht derselbe Speicherbereich verwalten lässt, was die Ressourcenverwaltung erschwert.

Auf den ersten Blick scheint die Referenzzählung gar keine schlechte Alternative zu sein. Muss man dabei aber nicht viel zusätzlichen Code in Kauf nehmen?
Vorteile des std::tr1::shared_ptr
An dieser Stelle greifen wir zu unserem neuen Werkzeug und schauen es uns genauer an. Um den im vorigen Kapitel genannten Problemsituationen zu entgehen, hat der std::tr1::shared_ptr einen kompletten Zählermechanismus unter der Haube und nicht nur das. Zusätzlich kümmert er sich um Zähleraktualisierungen, das automatische Freigeben des Speicherplatzes und einiges mehr. Sinkt der Zählerwert auf null Referenzen, gibt er den Speicherbereich, auf den er zeigt, automatisch frei.

Code: C++ | Plain Text
  1. std::tr1::shared_ptr(new Instance);


Diese intelligenten Zeiger befreien uns von einer Reihe von Aufgaben und geben uns zusätzliche Freiheiten. Diese möchte ich an einem Beispiel verdeutlichen.

In diesem prägnanten Beispiel entwickeln wir eine Klasse, die den Zeiger auf einen beliebigen Speicherbereich analog zum vorherigen Beispiel beinhalten soll. Diesmal verwenden wir allerdings einen std::tr1::shared_ptr anstatt auf den Rohzeiger zurückzugreifen.

Code: C++ | Plain Text
  1. #include
  2.  
  3. class Save_Memory
  4. {
  5. public:
  6.     Save_Memory()   {}
  7.  
  8.     Save_Memory(std::tr1::shared_ptr pMemory)
  9.         : m_pSomeMemory(pMemory)
  10.     {
  11.     }
  12.  
  13.     // Standard-Kopierverhalten sollte hier ausreichen
  14.  
  15. private:
  16.     std::tr1::shared_ptr m_pSomeMemory;
  17. };

Um die Fähigkeiten dieses intelligenten Zeigers zu demonstrieren, habe ich ein kurzes Beispielprogramm geschrieben, das die einfache Verwendung dieser Klasse präsentiert.

Code: C++ | Plain Text
  1. #include
  2. #include ”Save_Memory.h”
  3.  
  4. class Debug_Obj
  5. {
  6. public:
  7.     Debug_Obj()
  8.     { std::cout << "Test-Obj: Konstruktor" << std::endl; }
  9.  
  10.     ~Debug_Obj()
  11.     { std::cout << "Test-Obj: Destruktor" << std::endl; }
  12. };
  13.  
  14. int main()
  15. {
  16.     Save_Memory M1(std::tr1::shared_ptr(new Debug_Obj));
  17.     Save_Memory M2;
  18.  
  19.     M2 = M1;
  20. }

Die Debug_Obj-Klasse soll uns dabei lediglich Informationen liefern, wann und ob sie instantiiert und eine Instanz von ihr wieder zerstört wurde. Das Programm erstellt dabei zwei Save_Memory-Instanzen, wobei nur einer der Zeiger auf den Heap übergeben wird.

Als erstes können wir sehen, wie erstaunlich leicht die Instanzen kopiert werden können, ohne dass wir die Klasse Save_Memory mit zusätzlichen Fähigkeiten ausstatten mussten. Nach dem Kopiervorgang enthält jede Instanz einen Zeiger auf denselben Speicherbereich.

Dieser wird wieder freigegeben, sobald kein Zeiger mehr darauf verweist – und das völlig automatisch. Das bedeutet, dass wir nach der Erstellung der Debug_Obj-Instanz auf dem Heap keine Sorge tragen müssen, den reservierten Speicherplatz wieder freigeben zu müssen.

Kombination mit einer selbst definierten Löschfunktion

Dass uns der hier vorgestellte, intelligente Zeiger so viel Arbeit abnimmt, ist meistens sehr schön, doch es gibt Fälle, in denen wir das überhaupt nicht wollen. Ein solcher Fall würde eintreten, wenn wir eine Ressource sperren wollen, solange wir auf sie zugreifen. Sobald kein Zugriff mehr erfolgt, soll die Ressource aber wieder entsperrt und nicht gelöscht werden.

Doch auch für solche Fälle bietet std::tr1::shared_ptr eine passende Lösung. Diese sieht so aus, dass wir optional eine Funktion angeben können, die statt dem Freigeben des Speichers aufgerufen wird, sobald der Referenzzähler bei null ankommt. Dafür ist der zweite Parameter des Konstruktors vorgesehen, den wir bisher unverwendet gelassen haben.

An dieser Stelle möchte ich wieder ein einfaches Beispiel einfügen, das die Benutzung dieser Methode demonstrieren soll.

Code: C++ | Plain Text
  1. void onDelete(Debug_Obj* ptr)
  2. {
  3.     delete ptr;
  4.     std::cout << "Pointer deleted" << std::endl;
  5. }
  6.  
  7. int main()
  8. {
  9.     std::tr1::shared_ptr Ptr(new Debug_Obj, onDelete);
  10. }

Zusätzlich gewinnt diese Methode des Ressourcenmanagements an Attraktivität, wenn man sich bewusst macht, dass sie auch auf Device-Instanzen bestimmter Engines und Bibliotheken anwendbar ist, die nicht einfach durchs Löschen freigegeben werden.

Alles, was dann nötig ist, ist eine einfache Anpassung der Löschfunktion.

Code: C++ | Plain Text
  1. void onDelete(Engine_Device* dev)
  2. {
  3.     dev->Release();
  4. }

Auf diese Weise wird der Einsatzbereich dieses intelligenten Zeigers erweitert, so dass er zu einem sehr leistungsfähigen Werkzeug für Ressourcenverwaltung avanciert. Gründe dafür ist seine Flexibilität und Sicherheit, die uns Freiheiten schenken und Sorgen abnehmen.
Zugriff auf Rohzeiger
Obwohl std::tr1::shared_ptr einen kompletten Zählermechanismus beinhaltet, bietet er gleichzeitig sehr einfachen Zugriff auf den Zeiger und somit den Speicherplatz, über den er wacht. Das hat mehrere gute Gründe. Erstens sind Zeiger dazu da, um durch sie Speicherplatz zu manipulieren. Das bedeutet, dass wir schlichtweg Zugriff auf ihn brauchen, um ihn nutzen zu können.

Ein weiterer Grund ist der, dass viele Funktionen als Parameter den nativen Zeiger erwarten, so dass sie mit einem std::tr1::shared_ptr nichts anfangen könnten.

Die Syntax ist denkbar einfach.

Code: C++ | Plain Text
  1. void trace(Debug_Obj* obj);
  2.  
  3. int main()
  4. {
  5.     std::tr1::shared_ptr Ptr(new Debug_Obj);
  6.     // ...
  7.     trace(Ptr.get());   // den nativen Zeiger übergeben
  8.  
  9.     Ptr->method();
  10.     (*Ptr).method();
  11. }

Die Methode get() liefert den nativen Zeiger, so wie wir ihn kennen. Alternativ dazu kann man den Operator -> benutzen, um Zugriff auf den Zeiger zu erlangen. Eine Dereferenzierung mit dem Operator * ist ebenfalls möglich.

Alle diese Möglichkeiten sollten eine doch recht intuitive Handhabung des std::tr1::shared_ptr ermöglichen.
Schlusswort
Nachdem so viel über die Vorteile von dem hier vorgestellten, intelligenten Zeiger geschrieben wurde, sollten wir uns zum Schluss auch über den Nachteil bewusst werden. Tatsache ist nämlich, dass der std::tr1::shared_ptr doppelt so groß als ein in C++ üblicher Zeiger ist. Grund dafür sind selbstverständlich die versteckten Mechanismen, die uns die Arbeit abnehmen und der damit verbundene Verbrauch an zusätzlichen Speicherressourcen. Zusätzlich nimmt der Programmablauf durch den Zählermechanismus an Reibung zu.

Außerdem kann dieser intelligente Zeiger keine Rohzeiger auf Arrays ersetzen, da er diese auf die falsche Weise freigeben würde. Dafür sollte std::tr1::shared_array verwendet werden.

In diesem Artikel habe ich alles in einem aufgezeigt, wie man einen std::tr1::shared_ptr erstellt und diesen mit seinen durchschlagskräftigen Fähigkeiten nutzen kann, um sicherer programmieren zu können. Zusätzlich hab ich gezeigt, wie man den nativen Zeiger erreicht, sobald es die Situation erfordert.

Zum Schluss sollte noch geschrieben werden, dass das mein erster Artikel überhaupt war. Falls ihr also der Meinung seid, dass es zu viel Text für so ein einfaches Thema ist, kann ich das sehr gut nachvollziehen. Mit der Steigerung des Niveaus soll das Verhältnis aber auch optimaler werden. Für den Anfang hab ich mir aus nahe liegenden Gründen ein sehr einfaches und kurzes Thema ausgesucht. Gerne erfreue ich mich auch an konstruktiver Kritik. :>

Ich hoffe, dass ich euch mit dieser Ausarbeitung Wissen vermittelt habe, das den Weg bei euren Projekten und Vorhaben etwas ebnen kann.
Take care,

Michael

Comments (2)

Gut zu wissen das man das Rad nicht selber neu erfinden muss , genau sowas habe ich gesucht.Danke für diesen Artikel!

Hey, habe deine Seite gerade bei Bing entdeckt. Hast wirklich ein prima Blog, werde bestimmt noch

Write a comment