Lambda Experimente
« | 08 May 2021 | »Da Lambda Ausdrücke
leider erst mit C++11 in den
Standard eingeflossen sind, fällt ihre direkte Nutzung im GATE Projekt
grundsätzlich weg.
Dann dann würden die Builds auf älteren Compilern unmittelbar fehlschlagen.
Aber … da stellt sich mir die Frage, ob man nicht mit Makros etwas basteln kann, was abwärts und aufwärts kompatibel ist?
Gleich eins vorweg: Das folgende Experiment produziert hässlichen
Code. Denn Makros sind immer hässlich.
Wie auch immer … manchmal reduzieren sie jedoch Code-Wiederholungen,
die ansonsten noch viel unübersichtlicher wären.
Von Funktoren zu Lambdas
Das Äquivalent zu Lambda Ausdrücken waren früher freie Funktionen oder Funktoren. Funktoren sind dabei den normalen Funktionen überlegen, weil sie als Objekte Membervariablen haben können um Zustände zu speichern.
In C++03 musste man für viele STL
Algorithmen also immer erst ein
Funktor Objekt erzeugen und dieses einem Algorithmus übergeben, der
über den operator()
den eigentlichen Code aufrufen konnte.
Lambda Ausdrücke reduzieren das wie folgt:
Man sieht also deutlich, wie sich die Schreibarbeit reduziert.
Makros helfen beim Codeverkürzen
Die Idee:
Wenn man ein Makro so gestaltet, dass es eine Funktorklasse versteckt, hätte man ein “Interface”, das sowohl einen Funktor wie auch einen Lambda Ausdruck generieren kann.
Ohne Captures ist dieses Unterfangen leicht. Denn C++ gestattet es innerhalb einer Methode eine lokale Klasse zu definieren. Das könnte dann so aussehen:
1#if defined(CPP11) 2 3#define LAMBDA(instance_name, return_type, arguments, code) \ 4 auto instance_name = [] arguments -> return_type code 5 6#else 7 8#define LAMBDA(instance_name, return_type, arguments, code) \ 9 class instance_name ## _class \ 10 { \ 11 public: \ 12 return_type operator() arguments code 13 } instance_name 14 15#endif
und wird dann so benutzt:
Schwieriger wird es mit Captures. Denn während in C++11 Lambdas
die Typen für die “eingefangenen” Variablen selbst ermittelt,
brauchen wir sie für den Funktor-Konstruktor leider schon.
Und ein weiterer Nachteil ist, dass man Referenzen und Kopien
separat behandeln müsste.
Ich behaupte jedoch, dass Lambdas normalerweise alles per Referenz
fangen können sollten … und folglich implementiere ich auch nur
diesen Fall aus.
Und leider braucht man auch für jede Anzahl von Captures ein eigenes Makro:
1#define LAMBDA_GET_TYPE(type, variable) type 2#define LAMBDA_GET_VAR(type, variable) variable 3 4#if defined(CPP11) 5 6#define LAMBDA_2(instance_name, cap1, cap2, return_type, arguments, code) \ 7 auto instance_name = [& LAMBDA_GET_VAR cap1, \ 8 & LAMBDA_GET_VAR cap2] \ 9 arguments -> return_type \ 10 code 11 12#else 13 14#define LAMBDA_2(instance_name, cap1, cap2, return_type, arguments, code) \ 15class instance_name ## _class \ 16{ \ 17public: \ 18 LAMBDA_GET_TYPE cap1 & LAMBDA_GET_VAR cap1 ; \ 19 LAMBDA_GET_TYPE cap2 & LAMBDA_GET_VAR cap2 ; \ 20 instance_name ## _class ( LAMBDA_GET_TYPE cap1 & arg1, \ 21 LAMBDA_GET_TYPE cap2 & arg2) \ 22 : LAMBDA_GET_VAR cap1 (arg1), \ 23 LAMBDA_GET_VAR cap2 (arg2) \ 24 {} \ 25 return_type operator() arguments code \ 26}; \ 27instance_name ## _class instance_name( \ 28 LAMBDA_GET_VAR cap1, \ 29 LAMBDA_GET_VAR cap2) 30 31#endif
Angewendet würde das dann so:
Fazit
Nun, wie gesagt: Schön sind solche Makros nicht. Und trotzdem sparen sie im Verhältnis zum voll ausprogrammierten Funktor jede Menge Code und machen die Anweisungen dichter und meines Erachtens auch leichter lesbar.
Im GATE Projekt ist dieser Entwurf im gate/lambdas.hpp
Header
mit weiteren Capture-Varianten implementiert.
Nachdem diese Technik nur in high-level App-Codes vorkommen soll, weiß ich
noch nicht, ob ich es häufiger einsetzen werde, oder ob es bei einem einfachen
Experiment bleibt.
Tatsächlich sind überhaupt nur die Capture-Varianten für mich relevant, denn wenn man ohne Captures auskommt, würde ich immer eine kleine statische Funkion schreiben und diese statt einem Lambda oder Funktor einsetzen.
Aber … wenn ich ein modernes C++ Projekt betreiben würde, das nur mit neueren Standards ab C++17 arbeiten müsste, dann wären Lambdas für mich eine willkommene Lösung. Sie sind zwar auch nicht immer ein Augenschmaus, aber sie haben ihren eigenen Charm um Code besser lesbar zu gestalten.