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.
Oliver P. schrieb:> Im dicken C++> Buch hab ich nichts dazu gefunden
Von wann ist denn dein Buch?
C++ hat über die Jahre einiges dazu bekommen. Lambda-Funktionen gibts
erst ab C++11.
Oliver
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.
Oliver P. schrieb:> 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).
Oliver P. schrieb:> 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.
Oliver P. schrieb:> Das Buch ist tatsächlich von 99
Dann brauchst du dringend ein Neues, in einem Vierteljahrhundert hat
sich viel getan.
Oliver P. schrieb:> 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.
Versuche einfach, Dein Beispiel umzuschreiben.
Niklas G. schrieb:> 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.
Niklas G. schrieb:> Dann brauchst du dringend ein Neues, in einem Vierteljahrhundert hat> sich viel getan.
Ist bestellt :-)
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.
Oliver P. schrieb:> den Sinn und die Vorteile
Es sind weitere wichtige Dinge hinzugekommen.
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
Rbx schrieb:> Dieses "Lambda"-Ding gibt es aber schon länger. Es hilft auch, ein wenig> damit herumzuspielen.> https://de.wikipedia.org/wiki/Lambda-Kalkül
Du hast meinen Beitrag gelesen?
Niklas G. schrieb:> Im> Prinzip gibt es Lambdas schon seit der Ur-Programmiersprache, dem> namensgebenden Lambda-Kalkül, seit den 1930ern.
Im Studium wurde der Lambda-Kalkül bei mir witzigerweise mit Python
erläutert und dessen Lambda-Funktionalität.
Niklas G. schrieb:> Du hast meinen Beitrag gelesen?
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 ;)
Niklas G. schrieb:> 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 ;)
(https://en.wikipedia.org/wiki/List_comprehension)
Oliver P. schrieb:> 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).
- std::bind ist mehr oder weniger obsolet.
Rbx schrieb:> Dieses "Lambda"-Ding gibt es aber schon länger. Es hilft auch, ein wenig> damit herumzuspielen.> https://de.wikipedia.org/wiki/Lambda-Kalkül
Wobei das Lambda-Kalkül mit den Lamba-Funktionen oder -Ausddrücken
nichts zu tun hat, außer dass das Kalkül
Niklas G. schrieb:> namensgebend
ist, weil dort von diesem Konstrukt gebrauch gemacht wird.
Für einen Anfänger ist es durchaus verwirrend, sich durch das Kalkül zu
arbeiten.
Bruno V. schrieb:> 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:
1) 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: [](<args>) { <code> }
2) 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.
Hier als Initialiserung ohne vorgetäuschte Zuweisung
1
usingMyFunkPtr=int(*)(int);
2
MyFunkPtrinc{[](intx)->int{returnx+1;}};
Der Typealias machts ein wenig schöner, gerade wenn man den ansonsten
recht kryptischen/hässlichen Datentype häufiger nutzen muss.
Aufruf, dann z.B. irgendwann so.
Arduino F. schrieb:> 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):
Yalu X. schrieb:> Die Lambda-Syntax der einzelnen ProgrammiersprachenArduino F. schrieb:> 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.
Aber nur ein ganz kleines Bisschen ;-)
Arduino F. schrieb:> int (*inc)(int) {[] (int x) -> int { return x + 1; }};
Das sieht aber einer normalen Funktion verdammt ähnlich, und kann auch
fast so genutzt werden. Gefährliches Verwirrspiel.
Niklas G. schrieb:> Arduino F. schrieb:>> int (*inc)(int) {[] (int x) -> int { return x + 1; }};>> Das sieht aber einer normalen Funktion verdammt ähnlich, und kann auch> fast so genutzt werden.
Das ist ja u.a. auch die Absicht dahinter.
> Gefährliches Verwirrspiel.
Wieso ist das gefährlich?
Niklas G. schrieb:> Das sieht aber einer normalen Funktion verdammt ähnlich,
Naja..
Wie schon gesagt, ich bevorzuge
Arduino F. schrieb:> MyFunkPtr inc {[] (int x) -> int { return x + 1; }};
Wenn ich schon sowas bauen muss.
Yalu X. schrieb:> 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.
int d[] {1,2,3,}; // ausreichend
Yalu X. schrieb:> Das ist ja u.a. auch die Absicht dahinter.
Es ist Absicht Dinge als andere Dinge zu tarnen? Ich bevorzuge es wenn
klar ersichtlich ist was für ein Sprachelement das ist.
Yalu X. schrieb:> Wieso ist das gefährlich?
Verwechslungen können zu Fehlern führen.
Arduino F. schrieb:> Johann L. schrieb:>> -Wfloat-conversion>> 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.
https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html
Was in -Wall drinne ist und was nicht hat vor allem historische Gründe.
...ist aber alles nicht Gegenstand dieses Threads; ebensowenig wie die
zig verschiedenen Arten von C++ Objekt-Initialisierungen.
Johann L. schrieb:> Außer -Wall kennt GCC noch andere Warning Optionen
Ich danke dir für diese unglaublich wichtige Information!
Sie wird mein Leben verändern.
Arduino F. schrieb:> Yalu X. schrieb:>> 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.).
Arduino F. schrieb:> int a = 34.6;> geht ohne Gemeckere durch.>> 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.
Arduino F. schrieb:> 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:
https://abseil.io/tips/88
Das, was er unter "Best Practices for Initialization" vorschlägt, deckt
sich exakt mit meiner Vorgehensweise und bestärkt mich darin, diese
beizubehalten.
Yalu X. schrieb:> Ist der Konstruktorstil nicht der mit den runden Klammern? Das mit den> geschweiften Klammern nennt sich doch "uniform initialization syntax"?
Wie auch immer!
Yalu X. schrieb:> und ich nehme ().
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);
Yalu X. schrieb:> 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.
Yalu X. schrieb:> Das eigentliche Problem ist, dass es in C++ (anders als in C)> mehrere Wege gibt, eine Variable zu initialisieren,
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.
Arduino F. schrieb:> Yalu X. schrieb:>> und ich nehme ().> 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 ;-)
Bruno V. schrieb:> 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.
Yalu X. schrieb:> Ok, in deiner kleinen Arduino-Welt gibt es gar kein std::vector,> deswegen wird dich das Problem kaum tangieren ;-)
Das ist gelogen!
"Meine" AVR Implementierung, abweichend vom Original Arduino, kann/hat
vector. Damit bin ich nicht der einzige in der AVR Arduinowelt.
Und für "meine" 32Bit und 64Bit Dinge sowieso für alle Boards original
vorhanden.
Also nein!
Meine Arduino Welt ist nicht so klein wie du gerne hättest und
behauptest.
Arduino F. schrieb:> "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 ;-)
Niklas G. schrieb:> Yalu X. schrieb:>> Das ist ja u.a. auch die Absicht dahinter.>> Es ist Absicht Dinge als andere Dinge zu tarnen?
Da wird nichts getarnt.
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?
Yalu X. schrieb:> Sehr gut.
Eine Bitte um Verzeichnung wäre angemessener, für deinen Mobbing
Versuch.
Yalu X. schrieb:> Dann wirst auch du sicher irgendwann erkennen, dass die> generelle Verwendung der {}-Initialisierung ihre Grenzen hat.
Im verstehenden lesen bist du scheinbar nicht sonderlich fit...
Liegt es an deinen Vorurteilen?
Versuchs doch bitte nochmal:
Arduino F. schrieb:> Ich weiche davon nicht ab, es (möglichst) einheitlich zu tun.
Arduino F. schrieb:> Yalu X. schrieb:>> Sehr gut.> Eine Bitte um Verzeichnung wäre angemessener, für deinen Mobbing> Versuch.
Jetzt sei deswegen nicht gleich eingeschnappt.
> Versuchs doch bitte nochmal:> Arduino F. schrieb:>> Ich weiche davon nicht ab, es (möglichst) einheitlich zu tun.
Weiter oben hat sich das noch anders angehört:
Arduino F. schrieb:> Einmal Konstruktor Stil, dann überall Konstruktor Stil.
Und genau das geht eben nicht.
>
:) 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.
Arduino F. schrieb:> 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
Oliver P. schrieb:> 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.
Oliver P. schrieb:> 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.
Oliver P. schrieb:> 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.
Oliver P. schrieb:> Hätte in meinem Code ein "normaler" Funktionszeiger gestanden> wäre alles klar gewesen
Hätte wäre könnte.
Die Lambdafunktion zerfällt zu einem Zeiger auf eine anonyme Funktion.
Also im Grunde genau das was du willst.
Nur, dass der Geltungsbereich mit einem Bezeichner weniger geflutet
wird.
Oliver P. schrieb:> 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.
Da bleibt man jung im Kopf.
Oliver
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.
Oliver S. schrieb:> Da bleibt man jung im Kopf.
Das ist wohl eines der Probleme. Auch wenn man will, es geht einfach
nicht mehr so wie früher :-)