QVINTVS · SCRIBET

Defer in C++

Es wird eine Implementation des aus Go bekannten defer() in C++ vorgestellt.

I. 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 aquiriert 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 Overkill. In diesen Fällen wünscht man sich dann ein Äquivalent zu Gos defer.

II. Lösung

Ich habe mich heute damit beschäftigt und habe dabei folgende Implementation eines defer für C++ zu Wege gebracht:

/*
 * 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

Im Einsatz sieht das dann so aus:

#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;
}

Die Ausgabe dieses Programms lautet dann:

Normal stuff
More
Cleaning up!

Oder auf obiges Beispiel angewandt:

#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;
}

III. 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(); };

Den erstellten Code veröffentliche ich wie im Lizenzvermerk angegeben unter BSD-2-Klausel-Lizenz. Vielleicht findet ihn ja jemand nützlich.