Lambda Experimente

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.

 1class MyFunctor
 2{
 3public:
 4  void operator()(type_t arg)
 5  {
 6    // do something
 7  }
 8};
 9
10int main()
11{
12  type_t arg;
13  MyFunctor functor;
14  functor(arg);
15  return 0;
16}

Lambda Ausdrücke reduzieren das wie folgt:

 1int main()
 2{
 3  type_t arg;
 4  auto lambda = [](type_t arg) -> void
 5  {
 6    // do something
 7  };
 8  lambda(arg);
 9  return 0;
10}

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:

1int main()
2{
3  LAMBDA(lambda_add, int, (int x, int y), { 
4    return x + y; 
5  });
6  int sum = lambda_add(1, 2);
7  return 0;
8}

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:

 1int main()
 2{
 3  int a = 1000; // to be captured
 4  int b = 100;  // to be captured
 5  LAMBDA_2(lambda_add_ex, (int, a), (int, b), 
 6    int, (int x, int y),
 7    {
 8      return a + b + x + y;        
 9    });
10  int sum = lambda_add_ex(10, 1);
11}

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.