Defer in C++
Marvin Gülker · 02.01.2020
Es wird eine Implementation des aus Go bekannten defer() in C++ vorgestellt.
Problem
Die Programmiersprache Go enthält ein Kommando namens defer. Dieser Befehl führt dazu, daß der übergebene Ausdruck am Ende des aktuellen Gültigkeitsbereichs ausgeführt wird. Auf diese Weise wird ein Problem behoben, welches man aus C und C++ in dieser Form kennt:
bool myfunc() { resource* r = alloc_resource(); do_something(r); int result = do_something_else(); if (result == DO_FAILED) { dealloc_resource(r); return false; } do_something_else_with_r(r); dealloc_resource(r); return true; }
Das Beispiel zeigt den Fall, daß eine Ressource akquiriert wird, mit
der später noch gearbeitet werden soll. Wenn aber ein Funktionsaufruf
zwischenzeitlich fehlschlägt, dann muß die Ressource ebenso
freigegeben werden, wie als wenn die Funktion korrekt zu Ende gelaufen
wäre. Das ist fehleranfällig. Go löst das Problem mit defer
. Die
idiomatische Antwort für C++ wäre der Einsatz von RAII, d.h. die
Definition eines eigenen Typs, dessen Konstruktor/Destruktor-Pärchen
die Akquirirung und Freigabe der Ressource übernähmen. Für sehr kleine
Ressourcen ist das oft ein erheblicher Aufwand. In diesen Fällen
wünscht man sich dann ein Äquivalent zu Gos defer
.
Lösung
Die persönliche Lösung des Verf. nach einiger Überlegung:
/* * Copyright © 2020 Marvin Gülker * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef defer template<typename T> struct defer_it { ~defer_it() { deferred_func(); } const T& deferred_func; }; typedef int defer_helper; template<typename T> defer_it<T> operator+=(const defer_helper&, const T& other) { return defer_it<T>{deferred_func: other}; } #define defer_line_resolver1(a, b) a ## b #define defer_line_resolver2(a, b) defer_line_resolver1(a, b) #define defer defer_helper defer_line_resolver2(WVZQ_defer_helper_, __LINE__); \ auto defer_line_resolver2(WVZQ_defer_, __LINE__) = defer_line_resolver2(WVZQ_defer_helper_, __LINE__) += [&]()->void #endif
Anwendung:
#include <defer.hpp> void foo() { std::cout << "Cleaning up!" << std::endl; } int main() { defer { foo(); }; defer { std::cout << "More" << std::endl; }; std::cout << "Normal stuff" << std::endl; return 0; }
Ausgabe des Programms:
Normal stuff More Cleaning up!
Anhand des obigen Beispiels:
#include <defer.hpp> bool myfunc() { resource* r = alloc_resource(); defer { dealloc_resource(r); }; do_something(r); int result = do_something_else(); if (result == DO_FAILED) { return false; } do_something_else_with_r(r); return true; }
Erklärung
Die Grundidee ist simpel. Das Argument für defer()
wird in ein
C++11-Lambda eingesetzt und es wird ein Struct definiert, dessen
Destruktor dann dieses Lambda ausführt. Der Einsatz eines Lambda statt
eines Funktionszeigers erlaubt es insbesondere, noch den Kontext des
defer()
-Ausdrucks miteinzubeziehen. Der Struct wird von defer()
dann
auf dem Stack erzeugt und bei Ende des momentanen Gültigkeitsbereichs
verfällt er und sein Destruktor wird ausgeführt. Wenn man aber eine
syntaktisch ansprechende Umsetzung möchte, dann ist der Weg über den
Präprozessor unvermeidlich. Im Einzelnen:
#ifndef defer // ... #endif
defer()
wird als Makro definiert. Praktischerweise kann es damit
zugleich als Header-Guard dienen.
template<typename T> struct defer_it { ~defer_it() { deferred_func(); } const T& deferred_func; };
Zunächst wird ein Struct definiert, welcher auf dem Stack liegen und in
seinem Destruktor die zu verzögernden Ausdrücke ausführen soll. Diese
sind in einem Lambda deferred_func
enthalten, dessen Typ T
ist. Hier
kommt ein Template-Parameter statt std::function
zum Einsatz, weil
std::function
Speicher auf dem Heap
in
Anspruch nimmt, d.h. diese Lösung ist etwas performanter – das Lambda
liegt insgesamt auf dem Stack. std::function
kann das nicht
gewährleisten, weil es die Möglichkeit unterstützen muß, auch über die
Lebenszeit eingefangener Bezeichner hinaus noch zu funktionieren. Dieses
Feature wird aber für diese spezielle Anwendung nicht benötigt.
typedef int defer_helper; template<typename T> defer_it<T> operator+=(const defer_helper&, const T& other) { return defer_it<T>{deferred_func: other}; }
Diese Zeilen definieren einen Helfer-Typ defer_helper
als Typalias von
int
. Das ist wegen der noch vorzustellenden Definition des
defer
-Makros nötig; man kann nicht direkt eine Instanz von defer_it
erzeugen, weil dessen vollständiger Typ erst beim zweiten Ausdruck des
Makros überhaupt feststeht. Die Überbrückung zum Template-Typ findet
sich dann in operator+=
. Dieser Operator wird entgegen seiner normalen
Funktion dazu mißbraucht, einfach nur eine Instanz des eigentlichen
Zieltyps zurückzugeben. T
stellt hier den Typ des Lambdas dar.
#define defer_line_resolver1(a, b) a ## b #define defer_line_resolver2(a, b) defer_line_resolver1(a, b)
Das ist ein Präprozessor-Trick, um das sogleich benötigte __LINE__
als
Teil eines Bezeichners verwenden zu können. Der Präprozessor wertet
__LINE__
nämlich unglücklicherweise nicht aus, wenn es auf den
Konkatenierungsoperator ##
folgt. Indem __LINE__
als Argument an
defer_line_resolver2()
übergeben wird, kann man diese Beschränkung
aufheben. Leider ist der Trick nicht mit weniger als zwei Hilfsmakros
möglich. Eine andere Lösung für dieses Problem existiert nicht.
#define defer defer_helper defer_line_resolver2(WVZQ_defer_helper_, __LINE__); \ auto defer_line_resolver2(WVZQ_defer_, __LINE__) = defer_line_resolver2(WVZQ_defer_helper_, __LINE__) += [&]()->void
Zugegebenermaßen ist dieses Makro schwer lesbar. Das liegt an dem angesprochenen Präprozessor-Trick. Löst man diesen auf, erhält man folgendes, deutlich leserlicheres Makro (das so aber wegen des angesprochenen Problems leider nicht funktioniert):
#define defer defer_helper WVZQ_defer_helper_ ## __LINE__; \ auto WVZQ_defer_ ## __LINE__ = WVZQ_defer_helper_ ## __LINE__ += [&]()->void
Das Makro erzeugt zunächst eine Instanz des Helfer-Typs defer_helper
und gibt ihr einen eindeutigen Namen, der aus einem sehr
unwahrscheinlichen Präfix, dem Begriff defer_helper_
und der aktuellen
Zeilennummer besteht (es ist daher nicht möglich, defer()
zweimal in
derselben Zeile zu verwenden). Anschließend wird auf diesem Objekt dann
der Operator +=
aufgerufen und dessen Ergebnis an eine weitere
Variable mit einem anhand der Zeilennummer generierten Namen zugewiesen.
Diese Variable – das ist der Kniff – hat den Typ auto
, weil man
C++-Lambdas nicht anders darstellen kann. Der Typ eines Lambdas ist
nämlich
nicht spezifiziert. Trotzdem kann man ihn verwirrenderweise als
Template-Parameter verwenden, wie es hier der Fall ist. Die Variable
erhält damit eine spezialisierte Fassung des defer_it
-Typs. Fällt sie
aus dem Gültigkeitsbereich, wird dessen Destruktor aufgerufen – der
dank des Umwegs über operator+=
das Lambda enthält.
Das Ende des Makros ist der Anfang des C++-Lambda-Syntax. Die geschweiften Klammern, die noch fehlen, stellt der Nutzer des Makros:
defer { cleanup(); };
Daher erklärt sich auch, weshalb am Ende noch ein Semikolon erforderlich ist. Voll expandiert kommt etwa folgendes heraus:
defer_helper WVZQ_defer_helper_54; auto WVZQ_defer_54 = WVZQ_defer_helper_54 += [&]()->void { cleanup(); };
Der erstellte Code wird hiermit wie im Lizenzvermerk angegeben unter BSD-2-Klausel-Lizenz veröffentlicht.