Dreimal C++ – lambdas in legacy code

Wie schon zuletzt beschrieben, galt es C++ Software aus einem Nachbarprojekt zu übernehmen. Die dort tätigen Kollegen verwendeten begeistert die Features von C++ 2014. Vor allem der Datentyp „auto“ und die Initialisierung mit geschweiften Klammern hatte es ihnen angetan. Auch setzten sie shared_ptr und unique_ptr ein. Falls diese dann doch mit traditionellen Pointer darunter mischen, startet unter Umständen eine interessante Fehlersuche.

Aber mich interessierte die Art und Weise wie sie lambdas einbauten. Ich suchte nach eckigen Klammer gefolgt von einer geöffneten Klammer und konnte drei Sorten identifizieren:

  1. lokale Subroutinen (hätte man auch mit #defines machen können)
  2. Abräumer von von Windows handles (scope guard, geht kaum ohne lambdas)
  3. zur Initialisierung von static Variablen (das fand ich cool)

lokale Subroutinen

In legacy code wachsen in aller Regel einige Steuerfunktionen immer weiter an. Es werden hier alle möglichen Schritte nacheinander ausgeführt. Zwischen den Schritten erfolgt eine immer ähnliche Behandlung von Fehlermeldungen und Sonderfällen.
Diese Teile duplizieren sich im lauf der Wartungen und Erweiterungen.
Ein verantwortungsvoller Softwerker möchte so etwas aufräumen. Dann sind diese Abschnitte gleich gestaltet und lassen sich leichter anpassen.
Das Standardverfahren hier sind #define Macros:

void very_long_function(){
#define MACRO do { \ 
      xyzzy(); \   
      other_func(); \ 
}while(0)      
    ....
    MACRO;
    ....      
   MACRO;
#undef MACRO
}
Die Konstruktion mit do{ … } while(0) kommt am ehesten einem Funktionsaufruf nahe. Eine Funktion in einer Funktion gibt es in C++ nicht.
Mit lambdas kann geholfen werden:

void very_long_function()
{
     auto macro = [&]() {
         xyzzy();
         other_func();
     };
     ...
     macro();
     ...
     macro();
}
Das macro hat einen vom Compiler erzeugten auto Datentyp. Die Instanz der Klasse implementiert den Funktionaufrufoperator und schon sieht das aus wie der Aufruf einer funktionslokalen Subroutine.
Vermutlich ist der erzeugte Assemblercode, wenn die Optimierungen eingeschaltet sind, auch genau so, wie der bei der Verwendung von #debug Macros erzeugte.
Als erster Schritt in die Welt der lambdas war das leicht verdauliche Kost.

Abräumer

Das RAII Prinzip ist hier das Schlagwort. In C++ fordert eine Instanz auf dem Stack eine Resource im Konstruktor an und gibt sie im Destruktor wieder frei.

Allerdings gibt es in gewachsenen Source mit Microsoft API Einschlägen immer die Tendenz Handles außerhalb von Objekten zu verwenden. Diese dann wieder in allen Fehlerfällen freizugeben und das auch nur, wenn sie wirklich angelegt wurden, erfordert ziemliches Geschick und führt vielen Möglichkeiten Fehler zu machen.

Nun gibt es scope_guard. Im Source habe ich zwei Implementierungen entdeckt. Beide arbeiten ähnlich:


// irgendwo in einer Funktion
    HBITMAP hBitmap = (HBITMAP)::LoadImage(hInst, MAKEINTRESOURCE(ressourceId), 
                IMAGE_BITMAP, 0, 0, LR_CREATEDIBSECTION);
    auto _ = scope::finally([&] { ::DeleteObject(hBitmap); });

// Wann immer dieser Block verlassen wird, wird hBitmap entfern!. 
Der Name “_” für die Abräumer Instanz ist der Knaller. Wenn es noch mehr Handles gäbe, wären das wohl “_1”, “_2” und so weiter.
Die Diskussion in Stackoverflow stellt mehrere Implementierungen zum Thema Abräumer vor.

Initialisierung von static Variablen

Die einfachste Implementierung eines singleton Patterns besteht nur aus einer Funktion. Innerhalb der Funktion ist der Wert des singletons in einer statischen Variable gespeichert. So gehört sich das für ein singleton Pattern.
Die Funktion unten gibt den Pfad zu den Ikons zurück. Diese liegen relativ zum Installationsverzeichnis. Die Variante vor lambdas prüfte zunächst mit if(s_sPathOtProIcons.empty()) ob die statische Variable gesetzt ist. Sie wird dann zunächst initialisiert und anschließt zurückgegeben.
Mit lambdas geht ganz ohne Fallunterscheidung:

const CString& CDlBmpReplacer ::GetPathToProIcons()
{
    static CString s_sPathToProIcons = []() {
        const int verylongNumbertoholdthepath = (1024 * 8);

        CHAR buf[verylongNumbertoholdthepath];

        DWORD lenOfPath = GetModuleFileName(NULL, buf, verylongNumbertoholdthepath);
        assert(lenOfPath > 0); // not long enough

        for (; buf[lenOfPath - 1] != '\\' && buf[lenOfPath - 1] != '/'; lenOfPath--)
            ;
        strcpy(buf + lenOfPath, cpcProIconPathRelative);
        return CString(buf);
    }();
    return s_sPathToProIcons;
}
Ist nicht toll, dass das lambda gar nicht einer Variablen zugewiesen wird? Es wird deklariert, instanziiert und ausgeführt. Das erfolgt alles beim Laden des Moduls um die statische Variable zu initialisieren.
Tatsächlich öffnet ein breakpoint in der Funktion bei Programmstart den Debugger.
Das ist nur unwesentlich schneller, als die Variante mit der Fallunterscheidung. Nun wird Abfrage nur noch der gelesene Wert zurückgegeben. Unter Umständen könnte ein ausgefeilter Optimierer den Funktionsaufruf ganz entfernen.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert