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.
“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.
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.
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 }
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.
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.
Vielseitige Zeiger
Die »unique_ptr« decken darüber hinaus noch drei weitere Anwendungsfälle ab. Sie lassen sich in den Containern und insbesondere Algorithmen der Standard Template Library verwenden. Zwar setzt der Container voraus, dass alle Elemente in ihn hineinkopiert werden. Dies lässt sich aber durch das Verschieben der Elemente in ihn hinein umgehen. In [3] ist zu sehen, wie ein Vektor vom Typ
std::vector<std::unique_ptr<int>> myInt
seinen Unique Pointer »unique1« durch »myInt.push_back(std::move(unique1))« erhält. Anschließend lässt sich der Vektor mit »std::sort« sortieren.
Daneben lassen sich Unique Pointer mit einer Löschfunktion parametrisieren. Gibt der Programmier keine an, gelangt statt ihrer der Destruktor der Ressource zum Aufruf. Beispielsweise ist in den folgenden Zeilen die Integer-Ressource mit »malloc()« allokiert und muss daher wieder mit »free« freigegeben werden:
int* ptr=(int*)malloc(sizeof(int)); unique_ptr<ptr, free> intptr;
Unique Pointer besitzen zudem eine Template-Spezialisierung für Arrays. Damit lässt sich einfach ein »std::unique_ptr« von Integern definieren. Als Default-Löschfunktion kommt dabei »delete []« zum Einsatz:
unique_ptr <int[]> intArray (new int[4]); intArray[0]=1998; intArray[1]=2005; intArray[2]=2011; intArray[3]=2017;
Während der »std::unique_ptr« den Lebenszyklus genau einer Ressource überwacht, stellt der »std::shared_ptr« den typischen Anwendungsfall für einen Smart Pointer dar. Er bietet einen Referenzzähler auf eine gemeinsam genutzte Variable an, die automatisch mitzählt, wie viele »std::shared_ptr« eine Ressource referenzieren. Erreicht der Referenzzähler den Wert 0, löscht die Laufzeit die Ressource automatisch.
Der Artikel über »std::shared_ptr« wird klären, wie sie sich anwenden lassen, wie sie zyklische Referenzen vermeiden helfen und welche Rolle der »std::weak_ptr« spielt.
Infos
- Mike Toms, “An E-Mail Conversation with Bjarne Stroustrup”: http://accu.org/index.php/journals/1356
- Rainer Grimm, “Gemeinsam ins Ziel”: Linux-Magazin 06/12, S. 90
- Rainer Grimm, “Rasch verschoben”: Linux-Magazin 10/12, S. 90
- Unique Pointer: http://en.cppreference.com/w/cpp/memory/unique_ptr
- Auto Pointer: http://en.cppreference.com/w/cpp/memory/auto_ptr










