std::shared_ptr teilen sich eine gemeinsame Ressource. Dabei zählt der gemeinsame Referenzzähler mit, wie viele Besitzer die Ressource hat. Wird der Smart Pointer std::shared_ptr kopiert, erhöht sich automatisch der Referenzzähler. Beim Löschen eines std::shared_prt wird sein Referenzzähler hingegen automatisch erniedrigt. Erreicht der Referenzzähler den Wert 0, wird die Ressource freigegeben.
Bevor ich auf die Details rund um den std::shared_ptr genauer eingehe, möchte ich erst mal seine Basics vorstellen.
Die Basics
Wird eine std::shared_ptr kopiert, erhöht sich sein Referenzzähler um 1. Beide Smart Pointer verweisen auf die gleiche Ressource. Dies Szenario stellt die folgende Graphik schematisch dar.
Mit Hilfe von shared1 wird shared2 initialisiert. Danach besitzt der Referenzzähler den Wert 2 und beide Smart Pointer verweisen auf die gleiche Ressource.
Die Anwendung
Dieses Programm zeigt den typischen Umgang mit dem Smart Pointer. Damit sich der Lebenszyklus seiner Ressource besser nachvollziehen lässt, habe ich den Konstruktor und Destruktor von MyInt (Zeile 8 - 16) mit Ausgaben versehen.
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
|
// sharedPtr.cpp
#include <iostream>
#include <memory>
using std::shared_ptr;
struct MyInt{
MyInt(int v):val(v){
std::cout << " Hello: " << val << std::endl;
}
~MyInt(){
std::cout << " Good Bye: " << val << std::endl;
}
int val;
};
int main(){
std::cout << std::endl;
shared_ptr<MyInt> sharPtr(new MyInt(1998));
std::cout << " My value: " << sharPtr->val << std::endl;
std::cout << "sharedPtr.use_count(): " << sharPtr.use_count() << std::endl;
{
shared_ptr<MyInt> locSharPtr(sharPtr);
std::cout << "locSharPtr.use_count(): " << locSharPtr.use_count() << std::endl;
}
std::cout << "sharPtr.use_count(): "<< sharPtr.use_count() << std::endl;
shared_ptr<MyInt> globSharPtr= sharPtr;
std::cout << "sharPtr.use_count(): "<< sharPtr.use_count() << std::endl;
globSharPtr.reset();
std::cout << "sharPtr.use_count(): "<< sharPtr.use_count() << std::endl;
sharPtr= shared_ptr<MyInt>(new MyInt(2011));
std::cout << std::endl;
}
|
Die Ausgabe des Programms zeigt der Screenshot.
In Zeile 22 wird MyInt(1998) erzeugt. Dies ist die Ressource, um dessen Lebenszyklus sich der Smart Pointer shardPtr kümmert. sharPtr->val erlaubt es in Zeile 23, direkt auf die Ressource zuzugreifen. Die Ausgaben der Konsole zeigen schön, wie der Referenzzähler in Zeile 24 mit 1 startet, durch die lokale Kopie von sharPtr in Zeile 28 erhöht wird und nach dem Bereich in Zeile 27 - 30 wieder den Wert 1 besitzt. Sowohl die Kopier-Zuweisung in Zeile 33 als auch das Zurücksetzen der Ressource in Zeile 35 modifizieren den Referenzzähler. Besonders interessant ist der Ausdruck sharPtr= shared_ptr<MyInt>(new MyInt(2011)) in Zeile 38. Im ersten Schritt wird die Ressource MyInt(2011) erzeugt und diese dem sharPtr zugewiesen. Das führt dazu, dass der Destruktor von sharPtr aufgerufen wird, den dieser war der alleinige Besitzer der Ressource new MyInt(1998) aus Zeile 22. Beim Verlassen der main-Funktion wird die letzte Ressource new MyInt(2011) aufgeräumt.
Das vorgestellte Programm hatte wenig Überraschungspotential. Auf die Suche in die Breite folgt nun die in die Tiefe.
Der Kontrollblock
Die std::shared_ptr teilen sich nicht nur eine Ressource und einen Referenzzähler. Sie teilen sich eine Ressource und einen ganzen Kontrollblock. Dieser Kontrollblock enthält zwei Zähler und gegebenenfalls zusätzliche Daten. Zwei Zähler? Der Kontrollblock besitzt einen Zähler für die std::shared_ptr und die std::weak_ptr, die auf ihn verweisen. Jetzt kommen zum ersten Mal die std::weak_ptr ins Spiel. Sie dienen dazu, zyklische Referenzen von std::shared_ptr aufzulösen. Über sie und das Problem mit zyklischen Referenzen werde ich in einem weiteren Artikel schreiben. Nun nochmals alles im Überblick.
Der Kontrollblock enthält
- einen Zähler für std::shared_ptr.
- einen Zähler für std::weak_ptr.
- gegebenfalls weitere Daten wie eine eigene Löschfunktion oder einen Allocator.
Wird ein std::shared_ptr zusammen mit seiner Ressource erzeugt, sind zwei teure Speicherallokationen notwendig. Eine Speicherallokation für die Ressource und eine für den Kontrollblock. Das kann std::make_shared in einem Schritt. Dies ist der Grund dafür, dass std::make_shared deutlich schneller ist als das explizite Erzeugen eines std::shared_ptr. Wer den Zahlen mehr als meinen Worten traut, kann die Fakten in dem Artikel Speicher und Performanz Overhead von Smart Pointern nachlesen.
Der std::shared_ptr kann mit einer speziellen Löschfunktion parametrisiert werden. Genau das passiert im nächsten Abschnitt.
Die Löschfunktion
Im Gegensatz zum Smart Pointer std::unique_ptr ist die Löschfunktion beim std::shared_ptr nicht Bestandteil des Typs. Daher lassen sich zum Beispiel std::shared_ptr mit verschiedenen Löschfunktionen auf einen std::vector<std::shared_ptr<int>> schieben. Wird beim std::shared_ptr eine eigene Löschfunktion verwendet, wird diese im Kontrollblock gespeichert.
Im folgenden Beispiel verwende ich einen std::shared_ptr, der für jeden Typ mitprotokolliert, wie viel Speicher er wieder freigegeben hat.
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
62
63
64
65
66
67
68
69
70
71
72
|
// sharedPtrDeleter.cpp
#include <iostream>
#include <memory>
#include <random>
#include <typeinfo>
template <typename T>
class Deleter{
public:
void operator()(T *ptr){
++Deleter::count;
delete ptr;
}
void getInfo(){
std::string typeId{typeid(T).name()};
size_t sz= Deleter::count * sizeof(T);
std::cout << "Deleted " << Deleter::count << " objects of type: " << typeId << std::endl;
std::cout <<"Freed size in bytes: " << sz << "." << std::endl;
std::cout << std::endl;
}
private:
static int count;
};
template <typename T>
int Deleter<T>::count=0;
typedef Deleter<int> IntDeleter;
typedef Deleter<double> DoubleDeleter;
void createRandomNumbers(){
std::random_device seed;
std::mt19937 engine(seed());
std::uniform_int_distribution<int> thousand(1,1000);
int ranNumber= thousand(engine);
for ( int i=0 ; i <= ranNumber; ++i) std::shared_ptr<int>(new int(i),IntDeleter());
}
int main(){
std::cout << std::endl;
{
std::shared_ptr<int> sharedPtr1( new int,IntDeleter() );
std::shared_ptr<int> sharedPtr2( new int,IntDeleter() );
auto intDeleter= std::get_deleter<IntDeleter>(sharedPtr1);
intDeleter->getInfo();
sharedPtr2.reset();
intDeleter->getInfo();
}
createRandomNumbers();
IntDeleter().getInfo();
{
std::unique_ptr<double,DoubleDeleter > uniquePtr( new double, DoubleDeleter() );
std::unique_ptr<double,DoubleDeleter > uniquePtr1( new double, DoubleDeleter() );
std::shared_ptr<double> sharedPtr( new double, DoubleDeleter() );
std::shared_ptr<double> sharedPtr4(std::move(uniquePtr));
std::shared_ptr<double> sharedPtr5= std::move(uniquePtr1);
DoubleDeleter().getInfo();
}
DoubleDeleter().getInfo();
}
|
Deleter in Zeile 8 - 27 ist die spezielle Löschfunktion. Diese ist über ihren Typ T parametrisiert. Mit Hilfe der statischen Variable count (Zeile 23) zählt sie mit, wie oft ihr Klammeroperator (Zeile 11 - 14) aufgerufen wurde. Mit der Methode getInfo (Zeile 15 - 21) gibt sie ihre ganze Statistik aus. Weiter geht es in dem Programm mit der Funktion createRandomNumbers (Zeile 32 - 42). Sie erzeugt zwischen 1 bis 1000 std::shared_ptr in der Zeile 40, die mit der speziellen Löschfunktion IntDeleter() parametrisiert sind.
Die Ausgabe zeigt, dass beim ersten Aufruf von intDeleter->getInfo() noch keine Ressourcen freigegeben wurden. Das ändert sich mit dem Aufruf von sharedPtr2.reset() in Zeile 53. Eine int-Variable mit 4 Bytes ist freigegeben worden. Der Aufruf von createRandomNumbers() in Zeile 57 erzeugt 74 std::shared_ptr<int> Smart Pointer. Die Löschfunktion lässt sich natürlich auch für einen std::unique_ptr (Zeile 60 - 68) in dem lokalen Bereich verwenden. Der Speicher für die double Objekte wird aber erst nach dem Block in Zeile 68 freigegeben.
Wie geht's weiter?
std::shared_ptr hat noch einiges zu bieten. So lässt sich ein std::shared_ptr auf ein existierendes Objekt erzeugen und bietet minimale Multithreading Garantien. Eine Frage steht aber vor allem im Raum. Soll ein std::shared_ptr von einer Funktion per Copy oder per Referenz angenommen werden? Die Auflösung folgt im nächsten Artikel.
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...