Ein std::unique_ptr verwaltet exklusiv den Lebenszyklus seiner Ressource nach dem RAII-Idiom. Er ist die erste Wahl für einen Smart Pointer, da er seine Arbeit in der Regel ohne Speicher- oder Performanz-Overhead vollzieht.
Bevor ich einen std:unique_ptr in Anwendung vorstelle, will ich erst die Charakteristiken eines std::unique_ptr im Schnelldurchlauf vorstellen.
std::unque_ptr kann
- mit oder ohne Ressource instanziiert werden.
- den Lebenszyklus eines Objektes oder eines Array von Objekten verwalten.
- das Interface seiner zugrundeliegenden Ressource transparent anbieten.
- mit einer eigenen Löschfunktion angepasst werden.
- nur verschoben werden (Move-Semantik).
- einfach mit der Hilfsfunktion std::make_unique erzeugt werden.
Die Anwendung
Die entscheidende Frage für einen std::unque_ptr ist es, wann er seine zugrundeliegende Ressource löscht. Dies tritt genau dann auf, wenn der std::unique_ptr seinen Gültigkeitsbereich verlässt oder eine neue Ressource zugewiesen bekommt. Beide Anwendungsfälle zeigt das Beispiel.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
// uniquePtr.cpp
#include <iostream>
#include <memory>
#include <utility>
struct MyInt{
MyInt(int i):i_(i){}
~MyInt(){
std::cout << "Good bye from " << i_ << std::endl;
}
int i_;
};
int main(){
std::cout << std::endl;
std::unique_ptr<MyInt> uniquePtr1{ new MyInt(1998) };
std::cout << "uniquePtr1.get(): " << uniquePtr1.get() << std::endl;
std::unique_ptr<MyInt> uniquePtr2;
uniquePtr2= std::move(uniquePtr1);
std::cout << "uniquePtr1.get(): " << uniquePtr1.get() << std::endl;
std::cout << "uniquePtr2.get(): " << uniquePtr2.get() << std::endl;
std::cout << std::endl;
{
std::unique_ptr<MyInt> localPtr{ new MyInt(2003) };
}
std::cout << std::endl;
uniquePtr2.reset(new MyInt(2011));
MyInt* myInt= uniquePtr2.release();
delete myInt;
std::cout << std::endl;
std::unique_ptr<MyInt> uniquePtr3{ new MyInt(2017) };
std::unique_ptr<MyInt> uniquePtr4{ new MyInt(2022) };
std::cout << "uniquePtr3.get(): " << uniquePtr3.get() << std::endl;
std::cout << "uniquePtr4.get(): " << uniquePtr4.get() << std::endl;
std::swap(uniquePtr3, uniquePtr4);
std::cout << "uniquePtr3.get(): " << uniquePtr3.get() << std::endl;
std::cout << "uniquePtr4.get(): " << uniquePtr4.get() << std::endl;
std::cout << std::endl;
}
|
Die Klasse MyInt (Zeile 7 - 17) ist ein einfacher Wrapper für eine ganze Zahl. Dabei habe ich den Destruktor in Zeile 11 - 13 instrumentalisiert, damit sich der Lebenszyklus von Instanzen vom Typ MyInt leichter nachvollziehen lassen.
In Zeile 24 lege ich einen std::unique_ptr an und gebe in der Zeile 26 die Adresse seiner Ressource (new MyInt(1998)) aus. Anschließend verschiebe ich den uniquePtr1 in uniquePtr2 (Zeile 29). Damit ist uniquePtr2 der Besitzer der Ressource. Das zeigen schön die Ausgaben in Zeile 30 und 31. Der lokale std::unique_ptr in Zeile 37 verlässt mit dem Ende des Bereiches (Zeile 38) seinen Gültigkeitsbereich. Damit wird der Destruktor des localPtr und damit der Destruktor der Ressource (new MyInt(2003)) ausgeführt. Die ganze Erläuterung des Programms bringt die Ausgabe auf den Punkt.
Besonders interessant sind die Zeilen 42 - 44. Zuerst weise ich dem uniquePtr2 eine neue Ressource zu. Das führt dazu, dass der Destruktor von MyInt(1998) ausgeführt wird. Gebe ich die Ressource in Zeile 43 explizit frei, so kann ich auf der Ressource direkt den Destruktor aufrufen.
Der Rest des Programms ist einfache Hausmannskost. In den Zeilen 48 - 57 erzeuge ich zwei neue std::unique_ptr und tausche deren Ressourcen. std::swap in Zeile 54 wendet unter der Decke Move-Semantik an, da std::unique_ptr keine Copy-Semantik untersützt. Mit dem Ende der main-Funktion verlassen uniquePtr3 und uniquePtr4 ihren Gültigkeitsbereich und ihre Destruktoren werden erwartungsgemäß aufgerufen.
Auf dieses große Bild, folgen jetzt die weiteren Details zu std::unique_ptr.
Lebenszyklus von Arrays transparent verwalten
std::unique_ptr besitzt eine Spezialisierung für Arrays. Dabei ist der Zugriff auf ihn vollkommen transparent. Das heißt, verwaltet der std::unique_ptr ein einzelnes Objekt, sind die Operatoren für den Zeigerzugriff (operator* und operator->) überladen, verwaltet er ein Arrary, ist der Indexoperator operator[] überladen. Die entsprechenden Operatoren werden damit vollkommen transparent an die zugrundeliegende Ressource delegiert.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
// uniquePtrArray.cpp
#include <iomanip>
#include <iostream>
#include <memory>
class MyStruct{
public:
MyStruct(){
std::cout << std::setw(15) << std::left << (void*) this << " Hello " << std::endl;
}
~MyStruct(){
std::cout << std::setw(15) << std::left << (void*)this << " Good Bye " << std::endl;
}
};
int main(){
std::cout << std::endl;
std::unique_ptr<int> uniqInt(new int(2011));
std::cout << "*uniqInt: " << *uniqInt << std::endl;
std::cout << std::endl;
{
std::unique_ptr<MyStruct[]> myUniqueArray{new MyStruct[5]};
}
std::cout << std::endl;
{
std::unique_ptr<MyStruct[]> myUniqueArray{new MyStruct[1]};
MyStruct myStruct;
myUniqueArray[0]=myStruct;
}
std::cout << std::endl;
{
std::unique_ptr<MyStruct[]> myUniqueArray{new MyStruct[1]};
MyStruct myStruct;
myStruct= myUniqueArray[0];
}
std::cout << std::endl;
}
|
In der Zeile 22 dereferenziere ich den std::unique_ptr uniqInt und erhalten den Wert seiner Ressource.
MyStruct in Zeile 7 - 15 ist die Grundlage für das Array von std::unique_ptr. Wird ein MyStruct Objekt instanziiert, gibt es seine Adresse aus. Die entsprechende Ausgabe produziert der Destruktor. Damit lässt sich der Lebenszyklus der Objekte gut nachvollziehen.
In den Zeilen 26 - 28 werden fünf Instanzen vom Typ MyStruct erzeugt und destruiert. Deutlich interessanter ist da schon der Bereich in den Zeilen 32 - 36. In ihm wird eine MyStruct Instanz auf dem Heap (Zeile 33) und auf dem Stack (Zeile 34) erzeugt. Entsprechend besitzen beide Objekte verschiedene Adresse. Anschließend weise ich das lokale Objekt dem std::unique_ptr zu (Zeile 35). Ähnlich gehe ich in den Zeilen 40 - 54 vor. In diesem Fall weise ich der lokalen Instanz myStruct das erste Element von myUniqueArray zu. Der Indexzugriff der Elemente des std::unique_ptr-Arrays in den Zeilen 35 und 43 fühlt sie wie ein gewöhnlicher Indexzugriff auf ein Array an.
Eigene Löschfunktion
std::unique_ptr können mit einer eigenen Löschfunktion parametrisiert werden: std::unique_ptr<int,MyDeleter> uniqPtr(new int(2011), intDeleter). Die Löschfunktion ist Teil des Typs. Als Löschfunktion lassen sich aufrufbare Einheiten wie Funktionen, Funktionsobjekte oder auch Lambda Funktionen verwenden. Falls die Löschfunktion intDeleter keinen Zustand besitzt, ändert sie nichts an der Größe des std::unique_ptr. Ist die Löschfunktion ein Funktionsobjekt und besitzt einen eigenen Zustand oder eine Lambda-Funktion, die ein Teil des Aufrufkontextes bindet, dann gilt das Null-Overhead Prinzip nicht mehr. Auf die Details zu der Löschfunktion gehe ich beim std::shared_ptr noch genauer ein.
Ersatz für std::auto_ptr
Klassisches C++ kennt bereits den std::auto_ptr. Dieser hat ein ähnliches Aufgabengebiet wie der std::unique_ptr. Er soll den Lebenszyklus seiner Ressource automatisch verwalten. std::auto_ptr besitzt aber ein sehr eigentümliches Verhalten. Wird ein std::auto_ptr kopiert, wird die Ressource verschoben. Das heißt, eine Operation mit Copy-Semantik verhält sich unter der Decke wie Move-Semantik. Aufgrund dieses Verhaltens ist der std::auto_ptr deprecated und statt ihm sollte der std::unique_ptr verwendet werden. std::unique_ptr kann nur verschoben, aber nicht kopiert werden. Dazu muss explizit std::move auf ihn angewandt werden.
Gut lässt sich das Verhalten von std::auto_ptr und std::unique_ptr in einem Bild vergleichen.
Wird der kleine Codeschnipsel ausgeführt,
std::auto_ptr<int> auto1(new int(5));
std::auto_ptr<int> auto2(auto1);
führt dies dazu, das der std::auto_ptr auto1 danach keine Ressource mehr besitzt.
Da verhält sich std::unique_ptr deutlich berechenbarer. Für ihn muss explizit die Move-Semantik angefordert werden.
std::unique_ptr<int> uniqueo1(new int(5));
std::unique_ptr<int> unique2(std::move(unique1));
std::unique_ptr kann in den Algorithmen oder Container der STL verwendet werden, wenn diese unter der Decke keine Copy-Semantik verwenden.
Die Hilfsfunktion std::make_unique
Bei der Standardisierung von C++11 wurde die Hilfsfunktion std::make_unique leider vergessen, so dass sie erst mit C++14 zur Verfügung steht. Zu meiner Verwunderung hat sie aber bereits MIscrosoft Visual 2015 umgesetzt, obwohl der Compiler erst C++11 unterstützt. Dank std::make_unique muss new nicht mehr direkt angefasst werden, denn die Funktion erledigt alles selber.
std::unique_ptr<int> uniqPtr1= std::make_unique<int>(2011);
auto uniqPtr2= std::make_unique<int>(2014);
Wird std::make_unique in Kombination mit automatischer Typableitung verwendet, reduziert sich der Schreibaufwand auf das nötigste. Das zeigt der std::unique_ptr uniqPtr2.
Verwende immer std::make_unique
Es gibt einen weiteren, subtilen Grund std::make_unique zu verwenden. std::make_unique räumt immer seine Ressource auf.
Falls du
func(std::make_unique<int>(2014), functionMayThrow());
func(std::unique_ptr<int>(new int(2011)), functionMayThrow());
aufrufst und functionMayThrow eine Ausnahme wirft, hast du ein Speicherleck für new int(2011), falls die folgenden Sequenz von Befehlen ausgeführt wird.
new int(2011)
functionMayThrow()
std::unique_ptr<int>(...)
Wie geht's weiter?
Weiter geht es mit dem Smart Pointer std::shared_ptr. Auf die exklusiven Besitzverhältnisse mit std::unique_ptr folgen im nächsten Artikel der geteilten Besitzverhältnisse mit std::shared_ptr.
Go to Leanpub/cpplibrary "What every professional C++ programmer should know about the C++ standard library". Hole dir dein E-Book. Unterstütze meinen Blog.
Weiterlesen...