Einleitung
Im Einführungskapitel wird zunächst die Notwendigkeit von Shared Pointern verdeutlicht und zu deren
Verwendung motiviert. Nicht ohne Grund sind Shared Pointer bei einigen höheren Programmiersprachen wie z.B.
das ARC (Automatic Reference Counting) bei
Objective-C
ein Standard Sprach-Feature der Programmiersprache.
Lesern, die bereits Grundkenntnisse besitzen und sich für ein lauffähiges Code Snippet interessieren,
empfehlen wir, direkt bei Kapitel 2 Grundlagen der shared_ptr einzusteigen. Darin wird anhand von Beispielen
die Verwendung der Shared Pointer erklärt.
Einführung - Das Mysterium der Shared Pointer in C++
“Warum kann man nicht einfach Shared Pointer verwenden? Shared Pointer sind eine sehr schlaue
Methode zur Verwaltung von Pointern und des Speichers.”
Diese Aussage hört man sehr häufig. Aber werden Shared Pointer bereits wirklich im größeren Stil und
konsequent in Projekten eingesetzt? Im Austausch mit Entwicklern gewinnt man schnell den Eindruck, dass
Shared Pointer nicht besonders weit verbreitet oder populär sind. Woran liegt das? Gibt es Probleme, die bei
der Verwendung häufiger auftreten? Sind Shared Pointer zu kompliziert zu verstehen oder ist es zu aufwendig,
diese zu benutzen?
Wir hoffen, dass dieser Beitrag dabei helfen wird, einige der Fragen um das Mysterium Shared Pointer zu
klären und ein gutes Beispiel dafür geben zu können, wie man diese korrekt verwendet.
Einführung - Eine Analogie zu Shared Pointern
Anstatt direkt in technische Details zu springen, wird die Verwendung von Shared Pointern zunächst anhand
eines Beispiels aus dem echten Leben erläutert:
Stellen Sie sich vor, Sie arbeiten in einer Firma und es gibt eine einfache Regelung für das Bürogebäude: der
letzte Mitarbeiter, welcher das Gebäude verlässt, muss die Eingangstür zum Gebäude verriegeln und die
Alarmanlage aktivieren. Falls jemand das Gebäude verlässt und die Alarmanlage ist bereits aktiviert, dann
wird der Alarm ausgelöst. Deswegen sollten Sie hier genau prüfen, ob sich noch jemand anderes im Gebäude
befindet, bevor Sie die Alarmanlage aktiv schalten.
Angenommen, das Bürogebäude besteht nur aus einem kleinen Zimmer und es gibt nur fünf Mitarbeiter, dann ist
es für Sie sehr einfach festzustellen, ob Sie der letzte Mitarbeiter sind, der sich noch im Gebäude
befindet. Stellen Sie sich jedoch vor, Sie arbeiten in einem riesigen Bürogebäude mit mehreren Stockwerken
und vielen hunderten Mitarbeitern. Um hier noch zu wissen, ob Sie die letzte Person im Gebäude sind, müssten
Sie alle Stockwerke und alle Zimmer absuchen. Nun überlegen Sie sich, jemand anderes sucht zur gleichen Zeit
das Gebäude ab, um nachzusehen, ob noch jemand anderes im Gebäude ist und sie beide verfehlen sich dadurch.
Am Ende schließen Sie aus Versehen die andere Person im Gebäude ein, mit aktivierter Alarmanlage. Was für
ein Chaos!
Aber was wäre, wenn es eine Art “Smart Door”, also eine Art “schlaue Eingangstür” zum Gebäude gäbe, welche +1
für jede Person addiert, die das Gebäude betritt und -1 subtrahiert für jede Person, die das Gebäude
verlässt. Sobald der Zählerstand also den Wert 0 erreicht, wird die Alarmanlage automatisch aktiviert.
Wäre das nicht eine großartige Lösung für dieses Problem? Die Idee, welche sich hinter den Shared Pointern
verbirgt, ähnelt der “Smart Door” aus dem erläuterten Beispiel.
Einführung - Die Verwendung von C++ shared_ptr
Bei der Programmierung in C++ ist es oft nicht einfach, den Überblick über die Speicherverwaltung des
gesamten Source Codes eines Projektes zu behalten. Sobald ein C++ Pointer erzeugt wurde, kommen häufig
folgende Fragen auf:
- Wann soll der Pointer wieder gelöscht werden?
- Was, wenn ein anderer den Pointer noch benötigt?
- Und wenn der Pointer nicht gelöscht wird, wissen andere Beteiligte dann, ob und wann der Pointer
gelöscht werden soll?
Als simple Antwort auf diese Fragen folgt dann meist folgende Aussage:
“Der Pointer wird an der Stelle im Programm gelöscht, in der er als letztes verwendet wurde.”
Aber wie in dem Beispiel aus 1.2, in dem nicht eindeutig bestimmt werden kann, wieviele Leute sich in einem
großen Gebäude befinden, und wo genau diese sich aufhalten, so ist es keineswegs einfach, den Überblick
darüber
zu behalten, wohin alle Pointer in einem Programm weitergereicht wurden.
"Woher weiß man, welches die letzte Stelle ist, an der ein Pointer verwendet wurde?"
Ein Pointer könnte unter Umständen durch mehrere Konstruktoren oder Methoden Parameter weitergereicht worden
sein. Außerdem könnte es sein, dass man mit mehreren Teamkollegen zusammen am selben Source Code
arbeitet.
Vergleicht man die Behandlung von Pointern in C++ mit dem Beispiel aus 1.2, dann wäre ein Aufruf des delete
Befehls auf einen Pointer, für welchen der Speicher bereits freigegeben wurde, gleichbedeutend mit dem
Versuch, das Gebäude zu verlassen, in welchem bereits die Alarmanlage aktiviert wurde. Sobald man versucht,
die Tür von innen zu öffnen, würde die Alarmanlage ausgelöst und bei den Pointern das Programm
abstürzen.
Welches die letzte Stelle ist, an der ein Pointer verwendet wurde und zu wissen zu welchem Zeitpunkt man
delete aufrufen sollte, ist keine Frage die sich so einfach beantworten lässt, sofern es eine dritte Instanz
gibt, welche sich um diese Aufgabe kümmert. Und mit dieser dritten Instanz kommen Shared
Pointer, wie der std::shared_ptr
ins Spiel.
Der Shared Pointer weiß genau, welches die letzte Stelle ist, an der ein Pointer verwendet wurde und sobald
ein Pointer zum letzten Mal im gesamten Programm verwendet wurde, wird er automatisch deleted. Hierfür
addiert er +1. Dies geschieht jedes Mal, wenn jemand eine Kopie erzeugt, also z.B. wenn der Shared Pointer
in einer Methode oder in einem Konstruktor als Parameter übergeben wird. Des Weiteren wird immer -1
subtrahiert, wenn das Objekt, welches den Shared Pointer verwendet hat, gelöscht oder die Methode, welche
den Shared Pointer verwendet hat, beendet wurde. Sobald der Wert 0 erreicht wird, wird der Pointer
automatisch gelöscht und der belegte Speicher wieder freigegeben.
Grundlagen der shared_ptr
Nachfolgend werden nun einige Code Beispiele präsentiert. Aufgrund der Anschaulichkeit wird auch hier nochmal
das Büro Beispiel aus Kapitel 1.2 aufgegriffen:
Es wird zunächst eine Klasse OfficeBuilding definiert, also ein Gebäude, welches von Mitarbeitern betreten
werden kann. Das bedeutet, ein OfficeBuilding kann mehrere Instanzen eines Mitarbeiter Objektes (Employee)
referenzieren. Und als kleines, zusätzliches Feature gibt es in dem Gebäude ein WC (Washroom), welches von
genau einem Mitarbeiter betreten werden kann.
Grundlagen der shared_ptr - Instanziierung eines shared_ptr
Möchte man z.B. eine Instanz eines Objektes Employee erzeugen, dann würde man normalerweise einen Pointer mit
Hilfe des Schlüsselworts new instanziieren, um Heap-Speicher zu allokieren. Das Schlüsselwort delete würde
man dazu verwenden, um den Speicher wieder freizugeben:
Employee *employee1 = new Employee( 1, “Peter Parker”);
delete employee1;
Diese beiden Zeilen Code werden nun ersetzt durch die Folgende:
shared_ptr<Employee> employee1 = make_shared<Employee>(1, "Peter Parker");
Die linke Seite der Zuweisung shared_ptr<Employee> employee1
hält
sich eine Instanz der
Klasse Employee und anstatt das Schlüsselwort new zu verwenden, verwendet man das Schlüsselwort make_shared,
um diese Instanz zu erzeugen.
Die Parameter, welche man normalerweise dem Konstruktor der Klasse Employee übergeben würde, werden
stattdessen dem Ausdruck shared_ptr<Employee> (parameter1, parameter2,
...)
übergeben.
Sobald der aktuelle Kontext verlassen wird, also z.B. der Kontext der momentan aufgerufenen Methode, wird der
Speicher, welcher von der employee1
Instanz belegt wurde, automatisch
freigegeben. Der Aufruf
des delete-Befehls ist somit nicht notwendig.
Grundlagen der shared_ptr - Zugriff auf Class Members eines Shared Pointers
Angenommen, man möchte unter Verwendung der Getter Methode auf den Namen der Variablen employee1
zugreifen, dann könnte man dies tun, indem man die Variable wie einen ganz normalen C++ Pointer behandelt
und den Pfeil Operator “->” verwendet:
string employeeName = employee1->getName();
Die Verwendung des Punkt Operators “.” gewährt im Gegensatz dazu den Zugriff auf hilfreiche Informationen
über die shared reference wie zum Beispiel die Anzahl an Reference Counts:
long employee1UseCount = employee1.use_count();
Theoretisch betrachtet, ist es möglich, über den Shared Pointer auch noch auf den Raw Pointer zuzugreifen,
welcher vom Shared Pointer verwaltet wird:
Employee *pointer = employee1.get();
Jedoch sollte ein Zugriff auf den Raw Pointer, wie in diesem Beispiel um jeden Preis vermieden werden, weil
dies wieder nur zu altbekannten Problemen führt: jemand versucht, Speicher freizugeben, welcher bereits
freigegeben wurde. Diese Möglichkeit soll hier nur aus Gründen der Vollständigkeit erwähnt werden.
Grundlagen der shared_ptr - Kopieren eines shared_ptr
Wie kann man eine Kopie eines shared_ptr machen, welcher auf allokierten Speicher verweist? Vergleicht man es
mit normalen Pointern, dann würde man normalerweise Folgendes schreiben:
Employee *employee1 = new Employee( 1, “Peter Parker”);
Employee *employee1Copy = employee1;
Die erste Zeile des Codes allokiert ein Employee Objekt und lässt den Pointer employee1
auf den
neu allokierten Speicher zeigen. Anschließend wird in der zweiten Zeile eine Kopie dieser Variable gemacht
und die neue Variable employee1Copy
verweist dann auf denselben
Speicherbereich wie employee1
.
Wenn man etwas vergleichbares mit einem shared_ptr machen möchte, dann gibt es zwei Möglichkeiten:
shared_ptr<Employee> employee1 = make_shared<Employee>(1, "Peter Parker");
1) shared_ptr<Employee> employee1Copy = employee1;
2) auto employee1Copy2 = employee1;
Wie verhält sich das Reference Counting?
Nachdem nun die Grundlagen über shared_ptr vermittelt wurden, wäre es als nächstes interessant, zu
betrachten, wie sich das Reference Counting in einem richtigen Code Beispiel verhält.
Zunächst soll die Klasse Employee betrachtet werden. Ein Mitarbeiter (Employee) hat ein Attribut
Name
und eine eindeutige Id
, welche dem
Konstruktor übergeben werden kann. Diese
Klasse enthält, um das Beispiel möglichst einfach zu halten, keine Pointer oder Shared Pointer.
#ifndef SHAREDPOINTEREXAMPLEAPPLICATION_EMPLOYEE_H
#define SHAREDPOINTEREXAMPLEAPPLICATION_EMPLOYEE_H
#import <vector>
#import <string>
#import <iostream>
using namespace std;
class Employee {
private:
int id;
string name;
public:
Employee(int id, const string &name) : id(id), name(name){
}
int getId() const{
return id;
}
const string &getName() const{
return name;
}
void printCurrentUseCount(shared_ptr<Employee> employee){
if(employee.use_count() > 0){
printf("%s, use count: %li n", getName().c_str(), (employee.use_count() -1));
} else{
printf("use count: %li n", employee.use_count());
}
}
};
#endif //SHAREDPOINTEREXAMPLEAPPLICATION_EMPLOYEE_H
Um einen besseren Überblick über die Reference Counts zu haben, gibt es noch eine Methode printCurrentUseCount
.
Da bei jedem Aufruf der Methode ein neuer Kontext betreten wird und damit das Ergebnis um den Wert +1
“verfälscht” würde, wird hier wieder das auszugebende Ergebnis immer um -1 heruntergesetzt.
Das OfficeBuilding, in welchem die Mitarbeiter arbeiten, kann durch den Aufruf der Method enterBuilding und
leaveBuilding von Mitarbeitern betreten bzw. verlassen werden. Bei genauerem Hinsehen fällt auf, dass die
Klasse OfficeBuilding eine Klassenvariable vector<shared_ptr<Employee>>
smartDoorPeopleCounter
hat, welche sich merkt, welche Mitarbeiter sich zur Zeit im Gebäude
befinden.
Über die beiden Methoden useWashRoom und leaveWashRoom kann immer genau ein Mitarbeiter das WC betreten bzw.
wieder verlassen.
#ifndef SHAREDPOINTEREXAMPLEAPPLICATION_OFFICEBUILDING_H
#define SHAREDPOINTEREXAMPLEAPPLICATION_OFFICEBUILDING_H
#include "Employee.h"
#include "WashRoom.h"
#import <vector>
#import <string>
#include <thread>
#include <cstdlib>
class OfficeBuilding {
private:
vector<shared_ptr<Employee>> smartDoorPeopleCounter;
vector<WashRoom> washRoom;
public:
void enterBuilding(shared_ptr<Employee> employeeWhoWantsToEnter) {
smartDoorPeopleCounter.push_back(employeeWhoWantsToEnter);
employeeWhoWantsToEnter->printCurrentUseCount(employeeWhoWantsToEnter);
}
void leaveBuilding(shared_ptr<Employee> employeeWhoWantsToLeave) {
int indexToRemove = -1;
for (int i = 0; i < smartDoorPeopleCounter.size(); i++) {
auto currentEmployee = smartDoorPeopleCounter[i];
if (currentEmployee->getId() == employeeWhoWantsToLeave->getId()) {
indexToRemove = i;
}
}
if (indexToRemove != -1) {
smartDoorPeopleCounter.erase(smartDoorPeopleCounter.begin() + indexToRemove);
}
if (smartDoorPeopleCounter.size() == 0) {
// activate alarm system
}
}
void useWashRoom(shared_ptr<Employee> employee) {
if (washRoom.size() == 0) {
WashRoom emptyWashRoom;
emptyWashRoom.setEmployee(employee);
washRoom.push_back(emptyWashRoom);
} else {
//washroom is currently in use, please wait!
}
}
void leaveWashRoom() {
if (washRoom.size() == 1) {
washRoom.clear();
}
}
};
#endif //SHAREDPOINTEREXAMPLEAPPLICATION_OFFICEBUILDING_H
Das Objekt Washroom hält sich immer einen shared_ptr auf den aktuellen Mitarbeiter, welcher gerade das WC
benutzt.
#ifndef SHAREDPOINTEREXAMPLEAPPLICATION_OFFICEROOM_H
#define SHAREDPOINTEREXAMPLEAPPLICATION_OFFICEROOM_H
#include "Employee.h"
class WashRoom {
shared_ptr<Employee> employee;
public:
void setEmployee(const shared_ptr<Employee> &employee) {
WashRoom::employee = employee;
}
const shared_ptr<Employee> &getEmployee() const {
return employee;
}
};
#endif //SHAREDPOINTEREXAMPLEAPPLICATION_OFFICEROOM_H
Der folgende Beispielcode allokiert einige Mitarbeiter Objekte (Employee) und lässt diese das OfficeBuilding
betreten und wieder verlassen.
OfficeBuilding officeBuilding;
/* create employee 1 */
shared_ptr<Employee> employee1 = make_shared<Employee>(1, "Peter Parker");
employee1->printCurrentUseCount(employee1); //Peter Parker, use count: 1
string employeeName = employee1->getName(); // "Peter Parker"
Employee *pointer = employee1.get(); // attention: raw pointer should not be used!!
shared_ptr<Employee> employee1Copy = employee1;
employee1->printCurrentUseCount(employee1); //Peter Parker, use count: 2
auto employee1Copy2 = employee1;
employee1->printCurrentUseCount(employee1); //Peter Parker, use count: 3
/* create employee 2 */
shared_ptr<Employee> employee2;
employee2->printCurrentUseCount(employee2); //use count: 0
employee2 = make_shared<Employee>(2, "Bruce Wayne");
employee2->printCurrentUseCount(employee2); //Bruce Wayne, use count: 1
/* create employee 3 */
auto employee3 = make_shared<Employee>(3, "Clark Kent");
employee3->printCurrentUseCount(employee3); //Clark Kent, use count: 1
/* employees entering building */
officeBuilding.enterBuilding(employee1); //Peter Parker, use count: 5
employee1->printCurrentUseCount(employee1); //Peter Parker, use count: 4
officeBuilding.enterBuilding(employee2); //Bruce Wayne, use count: 3
employee2->printCurrentUseCount(employee2); //Bruce Wayne, use count: 2
officeBuilding.enterBuilding(employee3);//Clark Kent, use count: 3
employee3->printCurrentUseCount(employee3);//Clark Kent, use count: 2
/* employee 1 entering using washroom */
officeBuilding.useWashRoom(employee1);
employee1->printCurrentUseCount(employee1); //Peter Parker, use count: 5
officeBuilding.leaveWashRoom();
employee1->printCurrentUseCount(employee1);//Peter Parker, use count: 4
/* employee 1 leaving building */
officeBuilding.leaveBuilding(employee1);
employee1->printCurrentUseCount(employee1);//Peter Parker, use count: 3
/* employee 1 visiting coffee house */
CoffeeHouse coffeeHouse;
coffeeHouse.visitCoffeeHouse(employee1);
employee1->printCurrentUseCount(employee1);//Peter Parker, use count: 4
bool finish = true;
Die Print-Ausgaben sollen hier verdeutlichen, wie jeweils der aktuelle Reference Count ist:
Peter Parker, use count: 1
Peter Parker, use count: 2
Peter Parker, use count: 3
use count: 0
Bruce Wayne, use count: 1
Clark Kent, use count: 1
Peter Parker, use count: 5
Peter Parker, use count: 4
Bruce Wayne, use count: 3
Bruce Wayne, use count: 2
Clark Kent, use count: 3
Clark Kent, use count: 2
Peter Parker, use count: 5
Peter Parker, use count: 4
Peter Parker, use count: 3
use count inside context: 5
use count outside context: 4
use count from thread: 6
use count after thread finished : 5
Peter Parker, use count: 4
Die Verwendung von shared_ptr innerhalb von Threads
Es wird vermutet, dass es bei der Verwendung von C++ Shared Pointern zu Problemen im Zusammenhang mit Threads
kommen könnte. Nachfolgend auch ein Beispiel für diesen speziellen Fall: es gibt ein Cafe, welches sich
außerhalb des Gebäudes OfficeBuilding befindet und welches ein Mitarbeiter (Employee) besuchen kann.
Das Cafe kann mittels der Methode visitCoffeeHouse
betreten werden. In
dieser Methode wird
zunächst der Shared Pointer auf den Mitarbeiter einer auto Variable zugewiesen und anschließend einem Shared
Pointer struct, welcher als Container für die Parameter des Threads dient.
#ifndef SHAREDPOINTEREXAMPLEAPPLICATION_COFEEHOUSE_H
#define SHAREDPOINTEREXAMPLEAPPLICATION_COFEEHOUSE_H
#include "Employee.h"
#import <vector>
#import <string>
#include <thread>
#include <cstdlib>
#include <unistd.h>
using namespace std;
static pthread_t threadID = pthread_t();
class CoffeeHouse {
private:
shared_ptr<vector<shared_ptr<Employee>>> cafeteriaCounter;
struct threadParams {
shared_ptr<Employee> employeeCopy;
shared_ptr<vector<shared_ptr<Employee>>> cafeteriaCounterCopy;
};
static void *visitCoffeeHouseAsync(void *context) {
unsigned int microseconds = 5000000; // sleep for 5 seconds
usleep(microseconds);
auto params = *(shared_ptr<threadParams> *) context;
auto employee = params->employeeCopy;
string employeeName = employee->getName();
if (employeeName.length() > 0) {
params->cafeteriaCounterCopy->push_back(employee);
printf("use count from thread: %li n", (employee.use_count()));
}
pthread_detach(pthread_self());
return nullptr;
}
public:
CoffeeHouse(){
cafeteriaCounter = make_shared<vector<shared_ptr<Employee>>>();
}
void visitCoffeeHouse(shared_ptr<Employee> employee) {
{
shared_ptr<threadParams> params = make_shared<threadParams>();
params->employeeCopy = employee;
params->cafeteriaCounterCopy = cafeteriaCounter;
printf("use count inside context: %li n", (employee.use_count())); //use count inside context: 5
pthread_create(&threadID, NULL, visitCoffeeHouseAsync, ¶ms);
}
printf("use count outside context: %li n", (employee.use_count())); //use count outside context: 4
pthread_join(threadID, NULL);
printf("use count after thread finished : %li n", (employee.use_count()));//use count after thread finished : 5
}
};
#endif //SHAREDPOINTEREXAMPLEAPPLICATION_COFEEHOUSE_H
Der Thread wird innerhalb eines speziellen Kontextes gestartet, wobei der Kontext durch die geschweiften
Klammern { } markiert ist. Innerhalb des Kontextes wird der useCount
den
Wert 5 haben, kurz
bevor der Thread gestartet wird. Nachdem der Thread gestartet wurde, wird der Kontext verlassen, während der
Thread für 5 Sekunden schläft und in der Zwischenzeit wird der Reference Count durch den Main Thread auf den
Wert 4 verringert.
Sobald der Thread wieder erwacht, wird dieser eine lokale und eine globale Kopie des Shared Pointer machen,
was zu der Ausgabe des Wertes 6 führt.
Nachdem der Thread beendet wurde, verbleibt lediglich die globale Kopie, was zu einer Ausgabe des Wertes 5
führt. Das Endergebnis nach dem Verlassen der Methode visitCoffeeHouse für den Mitarbeiter “Peter Parker”
ist der Wert 4.
Auf den ersten Blick sieht mit den Threads alles relativ normal aus. Das heißt, die reference counts scheinen
korrekt erhöht und verringert zu werden. Jedoch können Probleme nie völlig ausgeschlossen werden. Möchte man
ganz sicher gehen, empfiehlt es sich, nur auf einer Kopie der Daten zu arbeiten.
Zusammenfassung
Das Konzept der C++ shared_ptr bietet eine Alternative zu den traditionellen C++ Pointern. Es ist jedoch
entscheidend, niemals normale C++ Pointer und Shared Pointer zu vermischen, weil dies zu ernsthaften
Problemen führen kann.
Der große Vorteil der Shared Pointer, die automatische Verwaltung von Speicher-Referenzen, ersetzt außerdem
die mühselige, manuelle Speicherfreigabe. Des Weiteren wurde ein Einblick gegeben, wie sich das Reference
Counting verhält. Einfach gesagt: Wann immer ein neuer Kontext betreten wird z.B. eine Methode oder eine
Kopie eines Shared Pointers, wird der Reference Count um +1 erhöht. Sobald ein Kontext wieder verlassen
wird, wird der Reference Count automatisch verringert, abhängig von der Anzahl der Kopien in diesem Kontext.
Nach intensiver Einarbeitung in die Thematik, lässt sich festhalten, dass Shared Pointer den
Entwicklungsprozess deutlich komfortabler und einfacher machen können. Auch die Kollaboration mit
Teamkollegen wird durch diese deutlich erleichtert. Es dürfte spannend werden, Shared Pointer zukünftig auch
in größeren Projekten einzusetzen.
Einen kleinen Nachteil könnte es dabei jedoch geben: sofern man Shared Pointer in einem Projekt verwendet und
im selben Projekt auch eine externe Bibliothek wie z.B. OpenCV verwenden möchte, welche vielleicht in den
Interfaces keine Shared Pointer anbietet, sollte man sehr vorsichtig damit umgehen. Möglicherweise bietet es
sich an, hier dann eine “tiefe Kopie” der Daten zu verwenden, welche man als Parameter den Schnittstellen
der Bibliothek übergeben möchte.