zurück zum Artikel

C++ Core Guidelines: Smart Pointer als Funktionsparameter

Rainer Grimm

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.

C++ Core Guidelines: Smart Pointer als Funktionsparameter

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 [1]) 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 [8]

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 [9]

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?

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

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.

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

oldFunc(*shaPtr);

// do something with shaPtr

}
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.


URL dieses Artikels:
https://www.heise.de/-3927673

Links in diesem Artikel:
[1] https://en.wikipedia.org/wiki/Don't_repeat_yourself
[2] http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rr-uniqueptrparam
[3] http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rr-reseat
[4] http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rr-sharedptrparam-owner
[5] http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rr-sharedptrparam
[6] http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rr-sharedptrparam-const
[7] http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rr-smartptrget
[8] http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rr-uniqueptrparam
[9] http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rr-reseat
[10] http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rr-sharedptrparam
[11] http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rr-sharedptrparam
[12] http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rr-sharedptrparam-const
[13] http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rr-smartptrget
[14] http://www.grimm-jaud.de/index.php/blog/das-neue-pdf-paeckchen-ist-fertig-multithreading-die-high-level-schnittstelle
[15] http://www.modernescpp.com/index.php/the-new-pdf-bundle-is-available-embedded-performance-matters