C++: Defer Execution

von Hubert Schmid vom 2013-06-02

In der Programmiersprache Go gibt es das Schlüsselwort defer, mit dem sich Operationen zurückstellen lassen. Die zurückgestellten Operationen werden vor dem Verlassen der Funktion in umgekehrter Reihenfolge ausgeführt – unabhängig davon wie die Funktion beendet wird. Üblicherweise wird dieser Mechanismus verwendet, um zuverlässig Ressourcen vor Beenden der Funktion freizugeben. Das folgende Listing enthält ein typisches Beispiel zur Thread-Synchronisation.

func foobar() { lock(l) defer unlock(l) // unlock at the end of the function // ... do some work while locked }

In C++ werden Ressourcen üblicherweise in Destruktoren freigegeben. Dieses Idiom ist unter der Abkürzung RAII bekannt und funktioniert ausgezeichnet, solange die Klassen und Funktionen dafür geeignet konzipiert sind. Wenn man allerdings C-Bibliotheken einbindet, benötigt man häufig zusätzlichen Code, um die C-Funktionen passend zu verpacken. Auch wenn generische Bausteine dabei unterstützen: Das Lesen einer Datei mit C-Funktionen ist gefühlt zu umständlich. Das folgende Listing demonstriert das Problem anhand eines std::unique_ptr.

auto&& fclose_if_not_null = [](FILE* fp) { if (fp) { std::fclose(fp); } }; std::unique_ptr<FILE, void (*)(FILE*)> fp{ std::fopen(pathname, "r"), fclose_if_not_null}; if (fp) { // work with open file } else { // handle error }

In solchen Fällen würde ein defer wie in Go helfen. Denn aus meiner Sicht liest sich der folgende Code wesentlich einfacher als die vorherige Version mit std::unique_ptr.

FILE* fp = std::fopen(pathname, "r"); if (fp) { DEFER { std::fclose(fp); }; // work with open file } else { // handle error }

Ein weiteres Beispiel hat mit Threads zu tun. Die Klasse std::thread ist zwar unter Berücksichtigung der aktuellen Idiome konzipiert. Dennoch muss die Member-Funktion join vor dem Thread-Destruktor aufgerufen werden. Denn andernfalls beendet sich das Programm hart durch einen Abbruch.

Mit DEFER würde der Code beispielsweise wie folgt aussehen. Dabei wird unabhängig vom sonstigen Ablauf vor der Beendigung der Funktion für jeden erzeugten Thread join aufgerufen, und eventuell zuvor auftretende Ausnahmen transparent durchgereicht.

std::vector<std::thread> threads; DEFER { for (auto&& thread : threads) { thread.join(); } }; for (auto&& action : actions) { threads.emplace_back(worker, action); }

C++ enthält kein Sprachkonstrukt, um Operationen auf diese Weise zurückzustellen. Interessanterweise lässt sich ein entsprechender Mechanismus aber mit den Bordmitteln von C++ vergleichsweise einfach nachbauen – und tatsächlich ist das bereits mehrfach passiert. Die bekannteste Implementierung ist vermutlich auf Andrei Alexandrescu zurückzuführen und findet sich in der Bibliothek Folly. An Stelle von DEFER nutzt sie den Namen SCOPE_EXIT. Abgesehen davon verwendet sie aber exakt die in meinen Beispielen verwendete Syntax und Semantik.

Also, Problem gelöst.