Es geht mir dabei primär um den Teil ab den eckigen Klammern.
Offenbar wird in dem Funktionsaufruf auch ein Befehlsblock übergeben.
Könnte das mal jemand aufdröseln so dass ich es verstehe? Im dicken C++ Buch hab ich nichts dazu gefunden bzw. hab ich vielleicht nicht nach den richtigen Begriffen gesucht.
LG Oliver
Das ist eine callback Funktion als Lambda Ausdruck. Da wird anstelle einer benannten Funktion eine Funktion übergeben die gleich in dem Aufruf von Server.on() definiert wird.
Besten Dank für Eure Hinweise. Das Buch ist tatsächlich von 99.
C++ ist teilweise schon ganz schön kryptisch (geworden).
Ich denke ich hab so halbwegs verstanden wie es funktioniert. Allerdings nicht für was man das jetzt unbedingt braucht oder warum man das nun unbedingt in einer Library-Schnittstelle einsetzen muss. Aber das ist wohl eher eine Glaubensfrage.
Allerdings nicht für was man das jetzt unbedingt braucht
Braucht man nicht unbedingt, überall wo du ein Lambda angeben kannst, kannst du auch einfach einen Funktionszeiger übergeben oder ein beliebiges aufrufbares Objekt (welches den operator () implement).
warum man das nun unbedingt in einer Library-Schnittstelle einsetzen
muss
Die Schnittstelle setzt das gar nicht ein, die akzeptiert irgendwas aufrufbares. Das Lambda kommt vom Usercode.
Es ist aber einfach praktischer und kompakter wenn die Callback-Funktion da steht wo man sie nutzt, damit man nicht endlos hin-und-her scrollen muss. Gerade auch wenn man sehr viele Callbacks hat, die selber auch wieder Funktionen mit Callbacks aufrufen. Außerdem können Lambdas lokale Variablen einfangen (capture), das manuell mit Funktional-Klassen zu implementieren ist einfach sehr viel mehr Tipparbeit, gerade auch bei Metaprogrammierung.
Und mehr Code bedeutet auch immer mehr Fehlermöglichkeiten und mehr Aufwand bei späteren Modifikationen.
Daher haben vielen andere Sprachen Lambdas, von LISP bis JavaScript. Im Prinzip gibt es Lambdas schon seit der Ur-Programmiersprache, dem namensgebenden Lambda-Kalkül, seit den 1930ern. C++ hat lediglich 80 Jahre später nachgezogen.
Allerdings
nicht für was man das jetzt unbedingt braucht oder warum man das nun
unbedingt in einer Library-Schnittstelle einsetzen muss. Aber das ist
wohl eher eine Glaubensfrage.
das ist eines der wenigen Dinge, die ich in C vermisse.
Jede Funktion auf File-scope wird früher oder später von woanders genutzt werden (wollen). Und braucht darum Vorannahmen und Checks, die im lokalen Kontext nicht nötig sind. Egal ob, wie hier, als Callback oder als Code-Snippet, dass innerhalb einer Funktion an 3 stellen benötig wird.
Braucht man nicht unbedingt, überall wo du ein Lambda angeben kannst,
kannst du auch einfach einen Funktionszeiger übergeben oder ein
beliebiges aufrufbares Objekt (welches den operator () implement).
Ach ok, wieder was gelernt. Bei einem Funktionszeiger hätte ich kein Verständnisproblem gehabt. Aber jeder halt wie er's kennt.
Ich werde mal etwas mit dem Code spielen und beide Varianten ausprobieren.
Hier mal ein paar, welche sowohl sehr nützlich, als auch auf AVR nutzbar:
uses - in verschiedenen Geschmacksrichungen
Range based for loop
explicit Konstruktoren
constexpr - nicht nur readonly, wie const, sondern wirklich Konstant
auto
Ja, aber der ist ja dann auch widersprüchlich, wenn man nicht extra nochmal darauf hindeutet.
Außerdem kann man den Text wohl auch schnell mal überlesen ;)
Im Studium wurde der Lambda-Kalkül bei mir witzigerweise mit Python
erläutert und dessen Lambda-Funktionalität.
Python hatte auch die List-comprehensions von Haskell gemopst.
Manches sollte man dann zwar doch lieber selber machen, teilweise wegen der Auswalkriterien, oder teilweise, weil es schneller ist - aber die Lcs decken schon eine Menge ab ;)
Ich hatte ja jetzt zum ersten Mal damit zu tun. Man muss es vielleicht
erst ein paar mal nutzen um den Sinn und die Vorteile besser zu
erkennen.
Ein paar Dinge, die mir gerade einfallen (falls etwas nicht stimmt, bitte korrigieren):
Lambdas sind nicht einfach ein Ersatz für Funktionszeiger, sondern eine "Kurzschreibweise" für Funktoren.
Funktion/Funktionszeiger != Lambda/generierter Funktor.
Lambdas konstruieren ein Closure. Du kannst darin Variablen aus dem umschließenden Scope verwenden.
Es ist manchmal (gerade beim Aufruf von Bibliotheksfunktionen) einfach praktischer, ein Lambda genau dort zu definieren, wo es benötigt wird (statt eine Mini-Funktion mit komischem Namen zu definieren).
Der Scope ist bei Lambdas potentiell kleiner. Bei Funktionszeigern muss eine Funktion existieren, deren Scope mindestens eine Klasse ist. Bei einem lokalen Lambda ist die Kapselung also besser.
Bei Funktionszeigern kann der Compiler nicht inlinen, bei Lambdas schon. Bei vielen Aufrufen kann das relevant sein (ansonsten natürlich nicht).
Wobei das Lambda-Kalkül mit den Lamba-Funktionen oder -Ausddrücken
nichts zu tun hat
Vielleicht sollte man deswegen eher von Lambda-"Schreibweise" sprechen.
Im Lambda-Kalkül wird bspw. eine Funktion, die ihr Argument verdoppelt,
als λx.2x geschrieben.
Die Lambda-Syntax der einzelnen Programmiersprachen variiert zwar etwas,
das betrifft aber vor allem die Schreibweise des Nicht-ASCII-Zeichens
"λ" und die Delimiter-Symbole (Klammer, Pfeile usw.), die den Ausdruck
mit der restlichen Syntax der Sprache kompatibel machen:
1
Lambda-Kalkül: λx.2x
2
Rust: |x| 2 * x
3
JavaScript, C#: x => 2 * x
4
Haskell: \x -> 2 * x
5
\x → 2 * x (mit Unicode-Extension)
6
Java: (x) -> 2 * x
7
Python: lambda x: 2 * x
8
Lisp: (lambda (x) (* 2 x))
9
C++: [] (int x) { return 2 * x; }
Mit zusätzlichem Leerraum, um die Syntaxen besser vergleichen zu können:
In C++ kann man zwei Kategorien von Lambdas unterscheiden:
Lambdas ohne Capture. Diese entsprechen normalen Funktionen, und wie gewohnt kann ein Funktionspointer solche Adressen aufnehmen:
1
int(*inc)(int)=[](intx)->int{returnx+1;};
Eine Möglichkeit der Ausfühung ist wie von C gewohnt:
1
intrun(intx,int(*fun)(int))
2
{
3
returnfun(x);
4
}
Noch eine Anmerkung zur Syntax: Das "-> Return-Typ" kann auch weggelassen werden wenn der Return-Typ vom Compiler eindeutig bestimmt werden kann:
1
int(*inc)(int)=[](intx){returnx+1;};
Das ist auch die in der Frage verwendete Syntax: { <code> }
Die zweite Kategorie sind Lambdas mit Capture, diese haben kein Analogon in C, sondern lediglich in GNU-C. Dafür zunächst mal ein Beispiel für lokale Funktionen in GNU-C:
1
#include<stdio.h>
2
3
voidrun(void(*f)(void))
4
{
5
f();
6
}
7
8
intmain(intargc,char*argv[])
9
{
10
voidworker(void)
11
{
12
printf("argc = %d\n",argc);
13
}
14
15
run(worker);
16
argc=-1;
17
run(worker);
18
19
return0;
20
}
1
argc = 1
2
argc = -1
"run" macht wie gewohnt einen indirekten Funktionsaufruf. Das besondere an der lokalen Funktion "worker" ist, dass sie auf den Kontext (argc von main) zugreift. Gleichwohl weiß "run" nichts davon, d.h. ein normaler Funktionspointer kann die Adresse von "worker" halten, und am Prototyp von "run" ist nicht erkennbar, dass "f" evtl. Zugriff auf irgendeinen Kontext hat oder haben könnte.
Die Implementierung erfordert compilerseitig einige Hacks, was z.B. dazu führt, dass lokale Funktionen in avr-gcc nicht unterstützt werden (können).
Mit C++11 Lambdas kann man dieses Feature über ein so genanntes Capture abbilden und damit den obigen GNU-C Code folgendermaßen in C++11 darstellen:
1
#include<cstdio>
2
#include<functional>
3
4
voidrun(std::function<void(void)>f)
5
{
6
f();
7
}
8
9
intmain(intargc,char*[])
10
{
11
autoworker=[&argc]()->void
12
{
13
printf("argc = %d\n",argc);
14
};
15
16
run(worker);
17
argc=-1;
18
run(worker);
19
}
Hier reicht ein normaler Funktionspointer nicht mehr aus, um "worker" indirekt aufrufen zu können. Es wäre also nicht mehr möglich, "run" in einem C-Modul zu implementieren. Vorteil ist, dass dies auch auf Architekturen wie AVR implementierbar ist, auf denen man keine Trampolines implementieren kann.
Falls man keine Referenz von argc übergeben will sondern lediglich den Wert von argc (zum "Zeitpunkt" der Instanziierung von "worker"), dann geht das mit folgendem Capture:
1
autoworker=[argc](){...};
und eine "capture all" als:
1
autoworker=[&](){...};
Allein "#include <functional>" präcompiliert bereits zu ca. 7000 Zeilen Code ohne Sub-Includes...
Wie bei lokalen Funktionen auch kommt man an dynamischer Speicherallokation nicht vorbei. Im Gegensatz zu GNU-C lokalen Funktionen / Trampolines muss der so allokierte Speicherbereich jedoch nicht ausführbar sein.
int (*inc)(int) = [] (int x) -> int { return x + 1; };
Das finde ich nicht so schön.....
Na ja, was ist an C++ schon schön? Es tut, was es soll, keine Frage,
aber das ist schon alles. Speziell die Lambda-Ausdrücke (aber auch fast
alle anderen Dinge) sehen in den meisten anderen Programmiersprachen
deutlich weniger holprig aus (s. Beispiele in meinem letzten Beitrag):
int (*inc)(int) {[] (int x) -> int { return x + 1; }};
Hier als Initialiserung ohne vorgetäuschte Zuweisung
Man könnte auch sagen: Mit getarnter Initialisierung. Bei obiger
Variante mit dem "=" kann man wenigstens noch auf einen Blick erkennen,
wo die Variablendeklaration endet und der Initialisierungsausdruck
beginnt. Bei deiner Variante hingegen ist man erst einmal damit
beschäftigt, zusammengehörige Klammern zu finden. Moderner ist nicht
immer besser.
using MyFunkPtr = int (*)(int);
MyFunkPtr inc {[] (int x) -> int { return x + 1; }};
Der Typealias machts ein wenig schöner, gerade wenn man den ansonsten
recht kryptischen/hässlichen Datentype häufiger nutzen muss.
Man könnte auch sagen: Mit getarnter Initialisierung. Bei obiger
Variante mit dem "=" kann man wenigstens noch auf einen Blick erkennen,
wo die Variablendeklaration endet und der Initialisierungsausdruck
beginnt.
Bedenkliches Argument.
Warum sollte man Zeiger anders initialisieren als normale Variablen?
Leuchtet mir nicht ein.
-Wall
int a = 34.6;
geht ohne Gemeckere durch.
int b {34.6};
Im Konstruktor Stil wird es angemosert.
Ein float passt eben nicht in einen int.
Ein bisschen Typesicherheit mehr.
Denn: Die impliziten Konvertierungen können einem auch mal Streiche spielen.
Das Prinzip der Geringsten Verwunderung:
Einmal Konstruktor Stil, dann überall Konstruktor Stil.
int c[] = {1,2,3,}; // naja
das = ist da überflüssig, ist ja keine Zuweisung.
Gehört nicht zu -Wall
Drum habe ich auch extra -Wall mit in den Text aufgenommen.
Außer -Wall kennt GCC noch andere Warning Optionen wie zum Beispiel -Wconversion. Wenn du an entsprechenden Warnungen interessiert bist, dann solltest du die entsprechenden Optionen auch setzen um diese Warnings zu aktivieren.
Man könnte auch sagen: Mit getarnter Initialisierung. Bei obiger
Variante mit dem "=" kann man wenigstens noch auf einen Blick erkennen,
wo die Variablendeklaration endet und der Initialisierungsausdruck
beginnt.
Bedenkliches Argument.
Warum sollte man Zeiger anders initialisieren als normale Variablen?
Leuchtet mir nicht ein.
Mach ich ja nicht. Ich initialisiere all diejenigen Variablen mit "=",
bei denen es mir logisch sinnvoll erscheint (s.u.).
int b {34.6};
Im Konstruktor Stil wird es angemosert.
Mal Hand aufs Herz: Wie oft ist dir dieser Fehler schon passiert? Mir
jedenfalls noch nie. Das "int" und die 34.6 stehen so dicht beieinander,
dass sich meine Finger schlichtweg weigern, so etwas einzutippen. Wenn
einem das dennoch öfter passiert, kann man ja immer noch das von Johann
vorgeschlagene -Wfloat-conversion verwenden.
Ganz abgesehen davon ist es ja auch ziemlich unsinnig, wenn die
strengere Typprüfung nur in einem speziellen Fall (Initialisierung),
nicht aber in allen anderen Fällen (Zuweisungen, Übergabe von
Funktionsargumenten usw.) stattfindet. Gerade bei Funktionsargumenten
ist die Gefahr, ein Float für ein Integer zu übergeben, viel größer, da
man nicht immer die Funktionssignatur vor Augen hat. Aber auch das wird
mit -Wfloat-conversion abgedeckt.
Das Prinzip der Geringsten Verwunderung:
Einmal Konstruktor Stil, dann überall Konstruktor Stil.
Ist der Konstruktorstil nicht der mit den runden Klammern? Das mit den
geschweiften Klammern nennt sich doch "uniform initialization syntax"?
Egal. Das eigentliche Problem ist, dass es in C++ (anders als in C)
mehrere Wege gibt, eine Variable zu initialisieren, nämlich u.a. mit =,
mit () und mit {}. Leider ist keiner davon in allen Fällen anwendbar
(sonst würde ich mich für diesen entscheiden), man braucht mindestens
die ()- und die {}-Syntax, um alle Fälle abzudecken.
Um ein wenig Ordnung in dieses Chaos zu bringen, bin ich irgendwann dazu
übergegangen, von allen in einem konkreten Fall möglichen Optionen die
am logischsten erscheinende auszuwählen:
Hat die Initialisierung den Charakter einer Zuweisung (die
Initialisierungswerte werden 1:1 in die Variable kopiert, die auch ein
Struct oder ein Array sein kann) verwende ich =, ebenso für andere
Sequenztypen wie bspw. die C++-Containerklassen.
Entsprechen die Initialisierungswerte direkt den Formalparametern des
Konstruktors, dann hat die Initialisierung den Charakter eines
Funktionsaufrufs, und ich nehme ().
Die {}-Initialisierung hingegen erscheint mir in keinem Fall logisch,
weswegen ich sie nur in Ausnahmefällen verwende, wo es nicht anders
geht.
Als ich gestern noch ein wenig recherchiert habe, wie andere mit dem
Thema umgehen, bin ich auf diesen Artikel des nicht ganz unbekannten
Titus Winters von den Google C++ Style Arbiters gestoßen:
Das, was er unter "Best Practices for Initialization" vorschlägt, deckt
sich exakt mit meiner Vorgehensweise und bestärkt mich darin, diese
beizubehalten.
Da ist es mir schon mehrfach passiert, dass sowas als Funktionsprototype erkannt wurde und die Kompilierung entsprechend, mit einer recht kryptischen Meldung, abgebrochen wurde.
Die {}-Initialisierung hingegen erscheint mir in keinem Fall logisch,
weswegen ich sie nur in Ausnahmefällen verwende, wo es nicht anders
geht.
Structs, Arrays, Variablen, Konstruktoren.
Du nennst es "uniform initialization syntax", und ich stimme zu.
Ich weiche davon nicht ab, es (möglichst) einheitlich zu tun.
Da ist es mir schon mehrfach passiert, dass sowas als Funktionsprototype
erkannt wurde und die Kompilierung entsprechend, mit einer recht
kryptischen Meldung, abgebrochen wurde.
Beispiel:
int test (42);
Die Initialisierung einer int-Variable entspricht einer initialen
Zuweisung, deswegen wird das ganz klar so geschrieben:
1
inttest=42;
Das Schöne an meinem Stil (und dem von Titus Winters) ist, dass
sämtliche Fallstricke (davon gibt es noch einige mehr, s.u.) sicher
umschifft werden.
Wenn du dich auf die {}-Initialisierung versteifst, wird du früher oder
später auf folgendes Problem stoßen:
Es soll ein int-Vektor der 10 Elementen anlegt werden. Dafür gibt es
einen speziellen Konstruktor, der die Anzahl der Elemente als Argument
entgegennimmt.
Du würdest das natürlich so machen:
1
std::vector<int>v{10};
Aber so geht das nicht, denn {10} wird in diesem Kontext als
initializer_list interpretiert, weswegen ein anderer Konstruktor
aufgerufen wird, der einen Vektor mit einem einzelnen Element erzeugt,
das den Wert 10 hat.
Nach meinem einfachen Regelwerk ist das eine Initialisierung mit
Funktionscharakter, weswegen () verwendet wird:
1
std::vector<int>v(10);
Das Ergebnis ist wie gewünscht ein Vektor mit 10 Elementen, von denen
jedes den Wert 0 hat (mit einem zweiten Konstruktorargument kann man
auch einen von 0 abweichenden Wert für die Elementinitialisierung
angeben).
S. auch folgendes Beispiel aus dem oben verlinkten Artikel:
1
std::vector<std::string>strings{2};// A vector of two empty strings.
2
std::vector<int>ints{2};// A vector containing only the integer 2.
Beide Vektoren werden syntaktisch exakt gleich, nämlich mit {2}
initialisiert. Nur weil der Elementtyp verschieden ist (oben string und
unten int), wird die {2} auf völlig verschiedene Weise interpretiert.
Wie krank ist das denn?
Das ist auch der Grund, warum die so genannte "uniform initialization",
die – wie obige Beispiel zeigen – alles andere als "uniform" ist, nur
dort verwendet werden sollte, wo es absolut keine andere Möglichkeit der
Initialisierung gibt.
Ok, in deiner kleinen Arduino-Welt gibt es gar kein std::vector,
deswegen wird dich das Problem kaum tangieren ;-)
Für einen Anfänger ist es durchaus verwirrend, sich durch das Kalkül zu
arbeiten.
Man kann darauf verzichten, ist aber ein nettes Werkzeug, wenn man sich mit Algorithmen, zahlentheoretischen Fragen, oder mit einfachen Formeln beschäftigt. Je nach Vorlieben kann das auch helfen, sich mit komplexen Verknüpfungen auf der Bitebene auseinanderzusetzen.
Mir reichen da meist 2 oder 3 Bit - aber eine zusätzliche Perspektive kann auch willkommen sein.
Klar, geht es oben nur um ein einfaches Code-Beispiel. Aber es sollte nicht übersehen werden, dass das Lambda-Kalkül die funktionale Programmierung recht gut vermittelt, bzw. bestimmte Grundlagen.
Je nach Vorlieben, kann man sich Internetseiten aussuchen die helfen, wie z.B.
https://www.cs.uni-potsdam.de/ti/lehre/downloads/TI-II/95-lambda-kalkuel.pdf
Es sieht von außen immer sehr viel komplizierter aus, als es tatsächlich ist.
Die Nützlichkeit, die Mächtigkeit, wie auch die Einfachheit erschließt sich beim Arbeiten damit.
Das ist ganz ähnlich wie beim Jonglieren: man muss es üben.
"Meine" AVR Implementierung, abweichend vom Original Arduino, kann/hat
vector. Damit bin ich nicht der einzige in der AVR Arduinowelt.
Sehr gut. Dann wirst auch du sicher irgendwann erkennen, dass die
generelle Verwendung der {}-Initialisierung ihre Grenzen hat. Vielleicht
wirst du dich dann an diesen Thread erinnern und noch einmal nachlesen,
wie man es besser macht ;-)
Lambda-Ausdrücke sind Funktionen und unterscheiden sich von regulären
Funktionen im Wesentlichen nur dadurch, dass sie keinen Namen haben und
optional auf lokale Variablen einer übergeordneten Funktion zugreifen
können. Letzteres ist regulären Funktionen auch nur deswegen verwehrt,
weil es für sie keine übergeordnete Funktion geben kann.
Wenn ich dich richtig verstehe, sollten deiner Ansicht nach anonyme
Funktionen eine von regulären Funktionen stark abweichende Syntax haben,
um den minimalen Unterschied deutlicher hervorzuheben.
Ich glaube, dies würde bei den meisten Nutzern nur ein unverständiges
"Häh?" provozieren.
Ich bevorzuge es wenn klar ersichtlich ist was für ein Sprachelement
das ist.
Es gibt ja das eckige Klammerpaar, das anstelle des Funktonnamens
erscheint. Ist das für dich nicht klar genug?
Yalu X. schrieb:
Wieso ist das gefährlich?
Verwechslungen können zu Fehlern führen.
Das kann ich mir nicht vorstellen. Hast du vielleicht ein einfaches
Beispiel, wo dir so etwas schon passiert ist?
:) und vollständigkeitshalber Clojure (fn[x] (* 2 x))
Nicht zu verwechseln mit closure = Umgebung, die aus Lambda Funktion
erreichbar ist. Bei Zugriffen auf Container und andere Objekte mit
innerem Zustand, vielleicht auch aus parallelen oder nebenläufigen
Prozessen, kann man "schöne" Bugs damit machen.
In C++ würde ich lambdas meiden.
Bei funktionalen Sprachen ist das weniger ein Problem.
Dort kann man beliebig viele Funktionen wrappen bzw. curring machen.
Übertreibt man es dort, wird der Code gegenüber Änderung unflexibel
und unüberschaubar.
Ich sehe das eher nicht als Problem, sondern eher als Segen.
Allgemein ist wohl C++ eine der vielfältigsten Sprachen.
Für jeden was dabei.
Ich sehe das schon als ein Problem. Zumindest für Leute wie mich die Programmierung nicht im Haupterwerb betreiben sondern nur gelegentlich damit zu tun haben. C++ wird immer umfangreicher und die Ausdrücke immer kryptischer. Bei jedem Release gibt es neue Sprachkonstukte um die Selben Dinge auf andere (bessere?) Art und Weise zu tun.
Da reicht es eben nicht, sich das raus zu picken was dem eigenen Gusto gefällt. Um fremden Code zu verstehen muss man trotzdem alle Varianten kennen. Hätte in meinem Code ein "normaler" Funktionszeiger gestanden wäre alles klar gewesen
Ich sehe das schon als ein Problem. Zumindest für Leute wie mich die
Programmierung nicht im Haupterwerb betreiben sondern nur gelegentlich
damit zu tun haben.
Für die ist C++ einfach nicht gemacht. Es gibt diverse Sprachen, die man "nebenher" nutzen kann. Wenn man aber jedes Werkzeug so konzipiert, bleibt für komplexe Anwendungen halt nichts übrig. Ironischerweise haben aber z.B. Python und JS schon lange Lambdas, weil sie einfach so praktisch sind.
Wenn du neue Libraries nutzen willst wie diese HTTP-Library musst du wohl auch neue Sprachfeatures kennen. Wenn du nur alte Sprachfeatures nutzen möchtest, kannst du immer noch alte Bibliotheken und alte Compiler verwenden.
Um fremden Code zu verstehen muss man trotzdem alle Varianten
kennen.
Ein Kfz-Mechaniker muss sich auch mit allen neuen Modellen und den dazugehörigen Werkzeugen auskennen. Da muss man eben dran bleiben. Autos sind auch nicht für Hobbyschrauber konstruiert.
Hätte in meinem Code ein "normaler" Funktionszeiger gestanden
wäre alles klar gewesen
Ein normaler Funktionszeiger reicht eben nicht immer. Im gezeigten Minimalbeispiel schon, aber das ändert sich sehr schnell, und ohne Lambdas wird es dann sehr schnell sehr unübersichtlich.
Die Variablen "index_html" und "processor" sind ja offensichtlich global oder statisch; wären es lokale Variablen, wäre es mit dem gegebenen API überhaupt nicht möglich diese bei Verwendung eines Funktionszeigers innerhalb der Callback-Funktion zu nutzen.
Traditionell hätte man es so gelöst, dass das API zusätzlich einen "void* user_data"-Parameter akzeptiert. Dann müsste man ein struct definieren welches die Variablen enthält und dieses übergeben, und sich dann auch noch Gedanken machen wie lange dieses im Speicher bleiben soll. Typsicher ist es auch nicht, weil man dann von void* auf den eigenen Typ casten muss. Also irgendwie so:
Die klassische OOP-Variante wären virtuelle Funktionen, wie es in Java exzessiv gemacht wird, wo aber auch keine expliziten Funktionszeiger mehr sichtbar sind (die sind versteckt in der vtable):
Das gezeigte API ist aber offenbar für aufrufbare Objekte konzipiert (entweder als template oder via std::function was dies kapselt). Man könnte eine eigene aufrufbare Klasse mit "operator ()" implementieren welche diese beiden Variablen enthält und eine Instanz davon übergeben:
Dann gibt es noch std::bind, aber das erstellt letztendlich genau das intern und der Weg des Funktionszeiger ist auch nicht mehr besonders klassisch/nachvollziehbar:
Das "[&]" sorgt dafür dass die lokalen Variablen durchgereicht werden. Das finde ich so deutlich übersichtlicher als die anderen klassischen Varianten. Ehrlich gesagt kann ich mir die Syntax von std::bind auch überhaupt nicht merken...
Falls das API (die on-Funktion) per template implementiert ist (statt per std::function), ist es mit dem Lambda oder std::bind eventuell sogar effizienter als die klassischen Varianten, weil die Callback-Funktion dann ggf. inlined wird.
Ich sehe das schon als ein Problem. Zumindest für Leute wie mich die
Programmierung nicht im Haupterwerb betreiben sondern nur gelegentlich
damit zu tun haben. C++ wird immer umfangreicher und die Ausdrücke immer
kryptischer.
Danke für die coole und ausführliche Erklärung. Auch wenn ich (noch) nicht alles verstanden habe erschließt sich der Vorteil so langsam. Den Beitrag muss ich sicher noch 2-3 mal in Ruhe durchgehen.