C++ Core Guidelines: Regeln für Smart Pointer

Für viele C++-Entwickler sind Smart Pointer das wichtigste Feature des C++11-Standards. Genau um diese Smart Pointer geht es in dem aktuellen Artikel.

In Pocket speichern vorlesen Druckansicht
Lesezeit: 8 Min.
Von
  • Rainer Grimm

Für viele C++-Entwickler sind Smart Pointer das wichtigste Feature des C++11-Standards. Genau um diese Smart Pointer geht es in dem aktuellen Artikel.

Die C++ Core Guidelines besitzen dreizehn Regeln für Smart Pointer. Die erste Hälfte von ihnen beschäftigt sich mit Besitzverhältnissen, die zweite Hälfte mit der Frage: Wie sollen Smart Pointer an Funktionen übergeben werden?

Hier ist der erste, schnelle Überblick.

Die ersten fünf Regeln (R.20 - R.24) sind ziemlich naheliegend. Ich habe bereits einige Artikel über sie geschrieben. Daher werde ich die Regeln zusammenfassen und auf meine bestehenden Artikel verweisen.

Ein std::unique_ptr ist der exklusive Besitzer seiner Ressource. Daher kannst du ihn nicht kopieren, sondern nur verschieben. Im Gegensatz dazu teilt sich ein std::shared_ptr seine Ressource. Falls du einen std::shared_ptr kopierst oder zuweist ("copy assign"), wird automatisch sein Referenzzähler inkrementiert. Falls du einen std::shared_ptr löschst oder zurücksetzt, wird sein Referenzzähler dekrementiert. Falls der Referenzzähler den Wert 0 erreicht, wird die zugrundeliegende Ressource gelöscht. Aufgrund dieses Verwaltungsaufwandes sollte ein std::unique_ptr verwenden werdet, wenn dies möglich ist (R.21).

Der Verwaltungsaufwand eines std::shared_ptr macht sich vor allem bei seiner Erzeugung bemerkbar. Das Anlegen eines std::shared_ptr stößt zwei Speicherallokationen an: eine Speicherallokation für die Ressource und eine für den Referenzzähler. Das ist ein aufwendiger Job. Die Rettung naht aber in der Form der Fabrikfunktion std::make_shared (R.22). std::make_shared benötigt nur eine Speicherallokation. Das ist eine große Performanzverbesserung. In dem Artikel "Speicher und Performanzoverhead von Smart Pointern" habe ich die Performanzunterschiede beim Anlegen und Löschen von nackten Zeigern, Smart Pointern inklusive deren Fabrikfunktionen std::make_shared und std::make_unique verglichen.

Es gibt noch einen weiteren, wichtigen Grund, einen std::shared_ptr mit std::make_shared oder einen std::unique_ptr mit std::make_unique zu erzeugen: Es lauert kein Speicherleck (R.22 und R.23). Falls zwei Aufrufe von std::shared_ptr oder std::unique_ptr in einem Ausdruck verwendet werden, kann dies zu einem Speicherleck führen, wenn eine Ausnahme auftritt. Die Details zu dieser Gefahr habe ich meinem letzten Artikel "C++ Core Guidelines: Regeln für Allokieren und Deallokieren" (R.13) bereits vorgestellt.

Um ehrlich zu sein, ein std::weak_ptr ist nicht wirklich smart. Er besitzt nicht die Ressource; er leiht sie nur von einem std::shared_ptr aus. Sein Interface ist sehr eingeschränkt. Indem du die Methode lock eines std::weak_ptr verwendest, kannst du einen std::weak_ptr zu einem std::shared_ptr erweitern. Natürlich stellt sich da die Frage: Warum benötigen wir einen std::weak_ptr in C++? Er hilft, Zyklen von std::shared_ptr zu brechen (R.24). Solche Zyklen sind der Grund, dass ein std::shared_ptr nicht automatisch seine Ressource freigeben kann. Oder anders herum ausgedrückt. Falls du einen Zyklus von std::shared_ptr besitzt, dann auch ein Speicherleck. Die Details zu std::weak_ptr und wie sich dieser verwenden lässt, um Speicherlecks von std::shared_ptr zu vermeiden, gibt es in meinem Artikel zu std::weak_ptr.

Das war meine Zusammenfassung zu Smart-Pointern. Diese Punkte sollten bereits mehr oder weniger Allgemeinwissen zu Smart-Pointern sein. Das gilt aber nicht für die verbleibenden Regeln. Sie beschäftigen sich mit der Frage: Wie soll ein Smart Pointer an eine Funktion übergeben werden?

R.30: Take smart pointers as parameters only to explicitly express lifetime semantics

Diese Regel ist ein wenig knifflig. Falls du einen Smart Pointer als Funktionsparameter und in dieser Funktion lediglich die dem Smart Pointer zugrundeliegende Ressource verwendest, machst du etwas falsch. In diesem Fall reicht es vollkommen aus, einen Zeiger oder eine Referenz als Funktionsparameter einzusetzen, da du nicht die Lebenszeitsemantik des Smart Pointer verwendest.

Hier ist ein Beispiel zu der ziemlich anspruchsvollen Lebenszeitsemantik eines Smart Pointer.

// lifetimeSemantic.cpp

#include <iostream>
#include <memory>

using std::cout;
using std::endl;

void asSmartPointerGood(std::shared_ptr<int>& shr){
std::cout << "shr.use_count(): " << shr.use_count() << endl; // (3)
shr.reset(new int(2011)); // (5)
cout << "shr.use_count(): " << shr.use_count() << endl; // (4)
}

void asSmartPointerBad(std::shared_ptr<int>& shr){
// doSomethingWith(*shr);
*shr += 19;
}

int main(){

cout << endl;

auto firSha = std::make_shared<int>(1998);
auto secSha = firSha;
cout << "firSha.use_count(): " << firSha.use_count() << endl; // (1)

cout << endl;

asSmartPointerGood(firSha); // (2)

cout << endl;

cout << "*firSha: " << *firSha << endl;
cout << "firSha.use_count(): " << firSha.use_count() << endl;

cout << endl;

cout << "*secSha: " << *secSha << endl;
cout << "secSha.use_count(): " << secSha.use_count() << endl;

cout << endl;

asSmartPointerBad(secSha); // (6)
cout << "*secSha: " << *secSha << endl;

cout << endl;

}

Los geht es mit dem Gutfall für einen std::shared_ptr. Der Referenzzähler in Zeile (1) ist 2, da der Shared Pointer firSha zum Einsatz kam, um den Shared Pointer secSha zu initialisieren. Ein genauerer Blick auf die Verwendung der Funktion asSmartPointerGood(2) lohnt sich. Zuerst ist der Referenzzähler in der Zeile (3) zwei und dann wird in der Zeile (4) zu eins. Was ist in der Zeile (5) passiert? Ich habe den Shared Pointer shr auf eine neue Ressource new int(2011) gesetzt. Konsequenterweise werden dadurch beide Shared Pointer firSha und secSha geteilte Besitzer von unterschiedlichen Ressourcen. Dies Verhalten lässt sich schön im Screenshot nachvollziehen.

Beim Einsatz von reset auf einem Shared Poitner geschieht viel Magie unter der Decke.

Falls du reset

  • ohne Argument verwendest, wird der Referenzzähler um eins dekrementiert.
  • mit einem Argument verwendest und der Referenzzähler war zu mindestens 2, erhälst du zwei unabhängige Shared Pointer, die verschiedene Ressourcen besitzen. Dies ist eine Art tiefes Kopieren (deep copy) eines Shared Pointer.
  • mit einem Argument oder ohne Argument verwendest und der Referenzzähler bekommt den Wert 0, wird die Ressource freigegeben.

Diese Magie ist nicht notwendig, falls du nur an der dem Shared Pointer zugrunde liegenden Ressource interessiert bist. Daher ist in diesem Fall ein Zeiger oder eine Referenz der angemessener Funktionsparameter für die Funktion asSmartPointerBad(6).

Sechs Regeln der Guidelines beschäftigen sich noch mit der Frage: Wie soll ein Smart Pointer einer Funktion übergeben werden? Mit der Beantwortung dieser Frage beschäftigt sich mein nächster Artikel.

Für meine drei offenen Seminare im ersten Halbjahr 2018 sind noch Plätze frei:

  • Embedded-Programmierung mit modernem C++: 16. bis 18. Januar 2018 (Anmeldeschluss 20.12)
  • C++11 und C++14: 13. bis 15. März 2018
  • Multithreading mit modernem C++: 8. bis 9. Mai. 2018

()