C++ Core Guidelines: Smart Pointer als Funktionsparameter

Modernes C++  –  0 Kommentare

Die Übergabe von Smart-Pointern an Funktionen ist ein wichtiges Thema, das selten adressiert wird. Das gilt aber nicht mehr mit den C++ Core Guidelines, denn diese bieten sechs Regeln für std::unique_ptr und std::shared_ptr an.

Die sechs Regeln verletzen das wichtige DRY-Prinzip (Don't Repeat Yourself) der Softwareentwicklung. Am Ende sind es nur vier Regeln, die unser Leben als Softwareentwickler deutlich einfacher machen. Hier sind sie.

Los geht es mit den ersten zwei Regeln für std::unique_ptr.

R.32: Take a unique_ptr<widget> parameter to express that a function assumes ownership of a widget

Falls eine Funktion Besitzer eines Widgets werden soll, soll die Funktion ihren std::unique_ptr<Widget> per Copy annehmen. Die Konsequenz ist, dass der Aufrufer der Funktion den std::unique_ptr<Widget> verschieben muss, damit der Code übersetzt werden kann.

#include <memory>
#include <utility>

struct Widget{
Widget(int){}
};

void sink(std::unique_ptr<Widget> uniqPtr){
// do something with uniqPtr
}

int main(){
auto uniqPtr = std::make_unique<Widget>(1998);

sink(std::move(uniqPtr)); // (1)
sink(uniqPtr); // (2) ERROR
}

Der Aufruf (1) ist syntaktisch richtig, aber der Aufruf (2) schlägt fehl, da ein std::unique_ptr nicht kopiert werden kann. Falls deine Funktion das Widget nur verwenden will, solltest du das Widget per Zeiger oder Referenz annehmen. Der Unterschied zwischen einem Zeiger einer Referenz ist es, dass der Zeiger ein Nullzeiger sein kann.

void useWidget(Widget* wid);
void useWidget(Widget& wid);

R.33: Take a unique_ptr<widget>& parameter to express that a function reseats the widget

Manchmal möchte eine Funktion ein Widget neu setzen. In diesem Anwendungsfall solltest du den std::unique_ptr<Widget> als nichtkonstante Referenz annehmen.

#include <memory>
#include <utility>

struct Widget{
Widget(int){}
};

void reseat(std::unique_ptr<Widget>& uniqPtr){
uniqPtr.reset(new Widget(2003)); // (0)
// do something with uniqPtr
}

int main(){
auto uniqPtr = std::make_unique<Widget>(1998);

reseat(std::move(uniqPtr)); // (1) ERROR
reseat(uniqPtr); // (2)
}

Nun schlägt der Aufruf (1) schief, da ein Rvalue nicht an eine nichtkonstante Lvalue-Referenz gebunden werden kann. Das gilt aber nicht für das Kopieren in (2). Ein Lvalue kann an eine nichtkonstante Lvalue Referenz gebunden werden. Ich möchte noch einen wichtigen Punkt hinzufügen. Der Aufruf (0) erzeugt nicht nur ein neues Widget(2003), auch das alte Widget(1998) wird automatisch destruiert.

Die Erklärungen zu den nächsten drei Regeln zu std::shared_ptr sind buchstäblich Wiederholungen. Daher werde ich eine Regel daraus machen.

Hier sind die drei entscheidenden Funktionssignaturen.

void share(std::shared_ptr<Widget> shaWid);
void reseat(std::shard_ptr<Widget>& shadWid);
void mayShare(const std::shared_ptr<Widget>& shaWid);

Die Funktionssignaturen sollten wir in Isolation betrachten. Was bedeutet die Signatur aus der Sicht der Funktion?

  • void share(std::shared_ptr<Widget> shaWid);: Ich bin für die Lebenszeit des Funktionskörpers ein Miteigentümer der Widget. Am Anfang des Funktionskörpers inkrementiere ich den Referenzzähler und am Ende dekrementiere ich den Referenzzähler. Daher bleibt das Widget so lange am Leben, wie ich es benötige.
  • void reseat(std::shard_ptr<Widget>& shadWid);: Ich bin nicht der Miteigentümer des Widget, da ich seinen Referenzzähler nicht erhöhe. Ich besitze keine Garantie, dass das Widget gültig ist, während ich es verwende. Ich kann das Widget aber neu setzen. Ein nichtkonstante Referenz ist eine Art ausleihen, das es erlaubt, die Ressource neu zu setzen.
  • void mayShare(const std::shared_ptr<Widget>& shaWid);: Ich leihe mir nur das Widget aus. Weder kann ich seine Lebenszeit verlängern noch es zurücksetzen. Um ehrlich zu sein, in diesem Fall solltest du einen Zeiger (Widget*) oder eine Referenz (Widget&) als Parameter verwenden, denn ein std::shared_ptr fügt keinen Mehrwert hinzu.

R.37: Do not pass a pointer or reference obtained from an aliased smart pointer

Um die Regel verständlich zu machen, kommt hier ein kleines Codeschnipsel.

void oldFunc(Widget* wid){
// do something with wid
}

void shared(std::shared_ptr<Widget>& shaPtr){ // (2)

oldFunc(*shaPtr); // (3)

// do something with shaPtr

}

auto globShared = std::make_shared<Widget>(2011); // (1)


...

shared(globShared);

globShared (1) ist ein globaler, geteilter std::shared_ptr. Die Funktion shared nimmt ihr Argument als Referenz (2) an. Daher wird der Referenzzähler von shaPtr nicht erhöht, und die Funktion verlängert konsequenterweise auch nicht die Lebenszeit von Widget(2011). Das Problem beginnt mit (3). oldFunc erwartet einen Zeiger auf ein Widget. Damit besitzt oldFunc keine Garantie, dass Widget während ihrer Ausführung gültig bleibt. oldFunc leiht sich nur das Widget aus.

Das Heilmittel ist einfach. Du musst sicherstellen, dass der Referenzzähler von globShared vor dem Aufruf von oldFunc erhöht wird. Das heißt, du solltest std::shared_ptr kopieren.

  • Übergebe den std::shared_ptr per Copy:
void shared(std::shared_ptr<Widget> shaPtr){

oldFunc(*shaPtr);

// do something with shaPtr

}
  • Lege ein Kopie von shaPtr in der Funktion shared an:
void shared(std::shared_ptr<Widget>& shaPtr){

auto keepAlive = shaPtr;
oldFunc(*shaPtr);

// do something with keepAlive or shaPtr

}

Dieselbe Argumentation lässt sich natürlich auch auf einen std::unique_ptr anwenden. Aber für std::unique_ptr gibt es kein einfaches Heilmittel, da dieser nicht kopiert werden kann. Daher schlage ich vor, dass du den std::unique_ptr gegebenenfalls klonst und damit einen neuen std::unique_ptr erzeugst.

Das war der letzte von vier Artikel zum Ressourcen-Management in den C++ Core Guidelines. Die C++ Core Guidelines bietet mehr als 50 Regeln für Ausdrücke und Anweisungen an. Ich werde in meinem nächsten Artikel einen genaueren Blick auf diese Regeln werfen.