Open Source im professionellen Einsatz
Linux-Magazin 02/2013

Modernes C++ in der Praxis – Folge 8

Räumkommando

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.

1528

"Zahle nicht für das, was du nicht nutzt", betont der C++-Erfinder Bjarne Stroustrup gerne [1]. Daher ist es nur konsequent, dass auch C++11 statt einer impliziten Garbage Collection eine explizite Speicherverwaltung mit den neuen Smart Pointern »unique_ptr« und »shared_ptr« anbietet. Dabei implementiert der Unique Pointer den exklusiven Zugriff auf seinen Speicherbereich, der Shared Pointer einen Referenzzähler auf diesen. Ihnen ist gemeinsam, dass sie ihre Ressource automatisch löschen, wenn das Programm sie nicht mehr benötigt. Während dieser Artikel »std::unique_ptr« vorstellt, wird sich der nächste in der Reihe mit »std::shared_ptr« beschäftigen.

RAII-Idiom

Die intelligenten Zeiger in C++11 verwalten automatisch den Speicherbereich der ihnen anvertrauten Ressource gemäß RAII-Idiom, die Abkürzung steht für Resource Acquisition Is Initalization. Dieses Idiom kommt in C++ gerne zum Einsatz. Die Idee ist einfach und doch mächtig zugleich: Sie kapselt die Lebenszeit einer zu überwachenden Ressource in einem lokalen Objekt. Im Konstruktor des Objekts wird die Ressource erzeugt und im Destruktor gelöscht.

Da die C++-Laufzeit die Lebenszeit des Objekts, das Objekt wiederum die Lebenszeit der kritischen Ressource verwaltet, wird die Ressource automatisch gelöscht. So überwachen die Smart Pointer die Lebenszeit ihres dynamisch allokierten Speicherbereichs und die Locks »std::lock_guard« und »std::unique_lock« [2] die Lebenszeit ihres Mutex, um Deadlocks zu vermeiden.

Listing 1 soll das deterministische Konstruktor- und Destruktor-Verhalten der Klasse »ResourceGuard« verdeutlichen. Der Konstruktor in Zeile 7 erzeugt das dynamische Array, der Destruktor (Zeile 11) gibt es wieder frei. Da das Beispiel Instanzen der Klasse »ResourceGuard« als lokale Objekte verwendet, rufen sie den Destruktor auf, sobald sie ihren Gültigkeitsbereich verlassen. Dies geschieht beim Objekt »resGuard(10000)« (Zeile 21) nach Verlassen der Main-Funktion, beim Objekt »resGuard2(200000)« am Ende des Bereichs in Zeile 25. In Abbildung 1 lässt sich das dynamische Verhalten schön nachvollziehen.

Listing 1

Speichermanagement mit RAII-Idiom

01 #include <iostream>
02 using std::endl;
03 using std::cout;
04
05 class ResourceGuard{
06 public:
07   ResourceGuard(int i){
08     myIntArray= new int[i];
09     cout << "constructing of myIntArray: " << myIntArray  <<  endl;
10   }
11   ~ResourceGuard(){
12     delete [] myIntArray;
13     cout << "destruction of myIntArray: " << myIntArray <<  endl;
14   }
15 private:
16   int* myIntArray;
17 };
18
19 int main(){
20   cout << endl;
21   ResourceGuard resGuard(10000);
22   cout << "\nbefore scope" << endl;
23   {
24     ResourceGuard resGuard2(200000);
25   }
26   cout << "after scope" << endl;
27   cout << endl;
28 }

Abbildung 1: Das RAII-Idiom angewandt: Beim Verlassen des Gültigkeitsbereichs gibt der Resource Guard die Ressourcen frei.

Insgesamt bietet C++11 vier intelligente Zeiger an. Einerseits rundet der »std::weak_ptr« die Funktionalität des »std::shared_ptr« ab, denn er hilft dabei, zyklische Referenzen des Shared Pointer aufzubrechen. Solche Referenzen verhindern, dass der Referenzzähler den Wert 0 erreichet. Dies wäre aber die Bedingung, um die Ressource zu löschen. Andererseits erbt C++11 von C++ den »std::auto_ptr« .

Im neuen Sprachstandard gilt dieser Auto Pointer allerdings als deprecated, denn er schleppt ein konzeptionelles Problem mit sich herum: Wenn er kopiert wird, wendet er heimlich Move-Semantik [3] an. Bevor sich der Artikel den Details rund um »std::auto_ptr« und »std::unique_ptr« widmet, stellt Tabelle 1 die vier Smart Pointer im Überblick dar.

Tabelle 1

Smart Pointer im Überblick

Name

Standard

Beschreibung

auto_ptr

C++98

Besitzt die Exklusiv-Ressource.Verschiebt beim Kopieren heimlich die Ressource.

unique_ptr

C++11

Besitzt die Exklusiv-Ressource.Lässt sich nicht kopieren.Verwaltet nicht kopierbare Objekte (Threads, Locks, Dateien …).

shared_ptr

C++11

Besitzt einen Referenzzähler auf eine gemeinsam genutzte Ressource.Verwaltet automatisch den Referenzzähler.Löscht die Ressource, sobald der Referenzzähler den Wert 0 besitzt.

weak_ptr

C++11

Hilft zyklische Referenzen von »shared_ptr« aufzulösen.

Zeiger-Kunde

Im neuen C++11 dient »std::unique_ptr« als Ersatz für den »std::auto_ptr« aus C++, der – wie oben beschrieben – gefährliche Nebenwirkungen besitzt. Diese lassen sich an dem einfachen Beispiel in Listing 2 demonstrieren, das dennoch Überraschungen birgt. Zeile 23 initialisiert einen Auto Pointer mit dessen allokierter Integer-Ressource:

Listing 2

Move-Semantik von auto_ptr

01 #include <iostream>
02 #include <memory>
03 #include <string>
04
05 using std::endl;
06 using std::cout;
07 using std::string;
08 using std::auto_ptr;
09
10 void printAutoPtr(const string& name, const auto_ptr<int>& autoPtr){
11   cout << name << ": " << autoPtr.get();
12   if (autoPtr.get()) cout << " value: " << *autoPtr;
13   cout << endl;
14 }
15
16 void catchResource(auto_ptr<int> localInt){
17   printAutoPtr("localInt.get()",localInt);
18 }
19
20 int main(){
21   cout << endl;
22   cout << "auto_ptr<int> autoPtr1{new int(2011)}" << endl;
23   auto_ptr<int> autoPtr1{new int(2011)};
24   printAutoPtr("autoPtr1.get()",autoPtr1);
25   cout << endl;
26
27   cout << "auto_ptr<int> autoPtr2(autoPtr1)" << endl;
28   auto_ptr<int> autoPtr2(autoPtr1);
29   printAutoPtr("autoPtr1.get()",autoPtr1);
30   printAutoPtr("autoPtr2.get()",autoPtr2);
31   cout << endl;
32
33   cout << "catchResource(autoPtr2)" << endl;
34   catchResource(autoPtr2);
35   printAutoPtr("autoPtr2.get()",autoPtr2);
36   cout << endl;
37 }
auto_ptr<int> autoPtr1{new int(2011)}

Abbildung 2 zeigt die Adresse des Speicherbereichs und seinen Wert.

Abbildung 2: Gefährlich: Der Auto Pointer verursacht unerwünscht einen Null Pointer.

Verwendet der Programmierer »autoPtr1« wie in Zeile 28, um »autoPtr2« zu initialisieren, nimmt das Unheil sein Lauf: Der Aufruf »auto_ptr<int> autoPtr2(autoPtr1)« verschiebt die Ressource von »autoPtr1« nach »autoPtr2« . Doch »autoPtr1.get()« verweist im Unterschied zu »autoPtr2.get()« auf keinen Wert mehr und lässt sich somit auch nicht ausgeben. Der Funktionsaufruf von »catchResource(autoPtr2)« in Zeile 34 veranlasst, dass »autoPtr2« kopiert wird. Das verschiebt die Ressource. Nun ist »localInt« in Zeile 17 der exklusive Besitzer von »newInt(2011)« . In der Abbildung 3 ist die heimliche Move-Semantik des »std::auto_ptr« dargestellt.

Abbildung 3: Das Kopieren eines Auto Pointer verschiebt dessen Ressource.

Natürlich kann der Entwickler das fehlerträchtige Kopieren von »std::auto_ptr« mit viel Disziplin vermeiden, doch besser ist es, wenn der Compiler das Kopieren gar nicht zulässt. Hier beginnt die Geschichte des neuen »std::unique_ptr« .

Der »std::unique_ptr« in C++11 ist der bessere Smart Pointer [4]. Er besitzt ein sehr ähnliches Interface wie »std::auto_ptr« [5]. Listing 3 zeigt, wie einfach sich der Unique Pointer einsetzen lässt, ohne die Risiken des alten Auto Pointer in Kauf zu nehmen. Der Wrapper »MyInt« macht das Löschen der Ressourcen auf dem Terminal sichtbar. Abbildung 5 zeigt die Ausgabe. Der Ausdruck

Listing 3

std::unique_ptr

01 #include <iostream>
02 #include <memory>
03
04 using std::ostream;
05 using std::cout;
06 using std::endl;
07 using std::unique_ptr;
08 using std::move;
09
10 struct MyInt{
11   MyInt(int i):i_(i){}
12
13   ~MyInt(){
14     cout << "Good bye from " << i_ << endl;
15   }
16
17   int i_;
18 };
19
20
21 int main(){
22   cout << endl;
23
24   std::unique_ptr<MyInt> uniquePtr1{ new MyInt(1998) };
25
26   cout << "uniquePtr1.get(): " << uniquePtr1.get() << endl;
27
28   unique_ptr<MyInt> uniquePtr2{ move(uniquePtr1) };
29   cout << "uniquePtr1.get(): " << uniquePtr1.get() << endl;
30   cout << "uniquePtr2.get(): " << uniquePtr2.get() << endl;
31   cout << endl;
32   {
33     unique_ptr<MyInt> localPtr{ new MyInt(2003) };
34   }
35   cout << endl;
36
37   uniquePtr2.reset(new MyInt(2011));
38   MyInt* myInt= uniquePtr2.release();
39   delete myInt;
40   cout << endl;
41
42   unique_ptr<MyInt> uniquePtr3{ new MyInt(2017) };
43   unique_ptr<MyInt> uniquePtr4{ new MyInt(2022) };
44
45   cout << "uniquePtr3.get(): " << uniquePtr3.get() << endl;
46   cout << "uniquePtr4.get(): " << uniquePtr4.get() << endl;
47
48   swap(uniquePtr3, uniquePtr4);
49
50   cout << "uniquePtr3.get(): " << uniquePtr3.get() << endl;
51   cout << "uniquePtr4.get(): " << uniquePtr4.get() << endl;
52   cout << endl;
53 }

Abbildung 5: Der Unique Pointer arbeitet ohne unliebsame Überraschungen.

unique_ptr<MyInt> uniquePtr2{move(uniquePtr1) }

in Zeile 28 verschiebt die Ressource von »uniquePtr1« nach »uniquePtr2« . Das ist exemplarisch in der Abbildung 4 dargestellt. Verpackt der Programmierer »unique_ptr« (Zeilen 32 bis 34) in einen Bereich, bewirkt dies, dass der »unique_ptr« beim Verlassen automatisch seine Gültigkeit verliert und seine Ressource »MyInt(2003)« löscht.

Abbildung 4: Der std::unique_ptr arbeitet mit expliziter Move-Semantik.

Das neue Setzen der Ressource »uniquePtr2.reset(new MyInt(2011))« , führt dazu, dass der Destruktor von »MyInt(1998)« automatisch ausgeführt wird. Selbstverständlich lässt sich der Destruktor in Zeile 39 auch explizit mit »delete myInt« aufrufen, nachdem Zeile 38 die Ressource freigegeben hat. Der Funktionsaufruf »swap(uniquePtr3, uniquePtr4)« (Zeile 48) vertauscht die Ressourcen. Mit dem Ende der Main-Funktion verlieren die letzten beiden »unique_ptr« ihre Gültigkeit, sodass sie ihre Ressourcen löschen.

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

    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.

  • 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++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++

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

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

comments powered by Disqus

Ausgabe 10/2017

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

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