Suche Erklärung für C++ Code

OP #7744467
Lesenswert?

Hallo Zusammen,

bei meinen programmier-Ausflügen bin ich auf ein Code Stück gestoßen dass ich nicht so recht interpretieren kann.

1
 server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
2
    request->send_P(200, "text/html", index_html, processor);
3
  });

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

OP #7744622
Lesenswert?

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.

Persönliche Seite #7744634
Lesenswert?

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.

#7744674
Lesenswert?

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.

OP #7744684
Lesenswert?

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.

#7744711
Lesenswert?

Niklas G. schrieb:

Dann brauchst du dringend ein Neues, in einem Vierteljahrhundert hat sich viel getan.

Dieses "Lambda"-Ding gibt es aber schon länger. Es hilft auch, ein wenig damit herumzuspielen. https://de.wikipedia.org/wiki/Lambda-Kalkül C++ mit Lambda noch nicht, aber.. ein wenig hat sich schon getan https://www.programiz.com/cpp-programming/lambda-expression https://learn.microsoft.com/de-de/cpp/cpp/lambda-expressions-in-cpp?view=msvc-170

Persönliche Seite #7744722
Lesenswert?

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.

#7744748
Lesenswert?

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)

#7744749
Lesenswert?

Oliver S. schrieb:

C++ hat über die Jahre einiges dazu bekommen. Lambda-Funktionen gibts erst ab C++11.

Hihi... erst ab C++11... die IT ist ja bekanntermaßen auch wirklich langlebig. ;-)

Oliver P. schrieb:

Das Buch ist tatsächlich von 99

Wie willst du damit C++ lernen? Also... so ernsthaft?

Übrigens führt die Suche nach der ersten Code-Zeile fast unmittelbar zu der gleichen Frage in den Foren

Auch das selbstständige Suchen von Informationen und Lösungen ist eine Entwickler-Kompetenz.

#7744755
Lesenswert?

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.

#7744773
Lesenswert?

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.

Moderator #7744884
Lesenswert?

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:

1
Lambda-Kalkül:  λ       x  .           2 · x
2
Rust:                  |x|             2 * x
3
JavaScript C#:          x  =>          2 * x
4
Haskell:        \       x  ->          2 * x
5
                \       x  →           2 * x
6
Java:                  (x) ->          2 * x
7
Python:         lambda  x  :           2 * x
8
Lisp:         ( lambda (x)          (* 2   x) )
9
C++:            [] (int x)    { return 2 * x; }

So groß sind die Unterscheide also gar nicht.

Beitrag #7744888 wurde von einem Moderator gelöscht.
Persönliche Seite #7745086
Lesenswert?

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) = [] (int x) -> int { return x + 1; };

Eine Möglichkeit der Ausfühung ist wie von C gewohnt:

1
int run (int x, int (*fun)(int))
2
{
3
    return fun (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) = [] (int x) { return x + 1; };

Das ist auch die in der Frage verwendete Syntax: { <code> }

  1. 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
void run (void (*f)(void))
4
{
5
    f ();
6
}
7

8
int main (int argc, char *argv[])
9
{
10
    void worker (void)
11
    {
12
        printf ("argc = %d\n", argc);
13
    }
14

15
    run (worker);
16
    argc = -1;
17
    run (worker);
18

19
    return 0;
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
void run (std::function<void (void)> f)
5
{
6
    f ();
7
}
8

9
int main (int argc, char *[])
10
{
11
    auto worker = [&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
auto worker = [argc] () { ... };

und eine "capture all" als:

1
auto worker = [&] () { ... };

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.

(Firma: Gast) #7745105
Lesenswert?

Johann L. schrieb:

1
int (*inc)(int) = [] (int x) -> int { return x + 1; };

Das finde ich nicht so schön.....

1
int (*inc)(int) {[] (int x) -> int { return x + 1; }};

Hier als Initialiserung ohne vorgetäuschte Zuweisung

1
using MyFunkPtr = int (*)(int);
2
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.

Aufruf, dann z.B. irgendwann so.

1
void setup() 
2
{
3
  Serial.begin(9600);
4
  cout << F("Start: ") << F(__FILE__) << endl;
5
  cout <<  inc(42) << endl;
6
}
Moderator #7745177
Lesenswert?

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 Programmiersprachen

Arduino 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 ;-)

(Firma: Gast) #7745253
Lesenswert?

Niklas G. schrieb:

Das sieht aber einer normalen Funktion verdammt ähnlich,

Naja..

Wie schon gesagt, ich bevorzuge Arduino F. schrieb im Beitrag #7745105:

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

Persönliche Seite #7745386
Lesenswert?

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.

Moderator #7745494
Lesenswert?

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.

(Firma: Gast) #7745516
Lesenswert?

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.

Moderator #7745600
Lesenswert?

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
int test = 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 ;-)

#7745604
Lesenswert?

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.

(Firma: Gast) #7745611
Lesenswert?

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.

Moderator #7745614
Lesenswert?

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 ;-)

Moderator #7745615
Lesenswert?

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?

(Firma: Gast) #7745650
Lesenswert?

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 im Beitrag #7745516:

Ich weiche davon nicht ab, es (möglichst) einheitlich zu tun.

Moderator #7745659
Lesenswert?

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.

#7746475
Lesenswert?

Yalu X. schrieb:

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; }
10
>

:) 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.

OP #7748176
Lesenswert?

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

Persönliche Seite #7748183
Lesenswert?

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:

1
// Library
2
class Server {
3
  public:
4
    void on(const char* file, int method, void (*callback)(AsyncWebServerRequest *, void*), void* userData);
5
};
6

7
// Usercode
8

9
struct CallbackData {
10
  const char* index_html;
11
  Processor processor;
12
};
13

14
void myCallback (AsyncWebServerRequest *request, void* userData) {
15
  CallbackData* data = reinterpret_cast<CallbackData*> (userData);
16
  request->send_P(200, "text/html", data->index_html, data->processor);
17
}
18

19
int main () {
20
  const char* index_html = ...;
21
  Processor processor = ... ;
22

23
  server.on("/", HTTP_GET, &myCallback, CallbackData { index_html, processor });
24
);

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):

1
// Library
2

3
class HTTPOberserver {
4
  public:
5
    virtual void onComplete (AsyncWebServerRequest *request) = 0;
6
};
7

8
class Server {
9
  public:
10
    void on(const char* file, int method, HTTPObserver& observer);
11
};
12

13
// Usercode
14

15
class MyHTTPObserver : public HTTPObserver {
16
  const char* index_html;
17
  Processor processor;
18
  virtual void onComplete (AsyncWebServerRequest *request) override {
19
    request->send_P(200, "text/html", index_html, processor);
20
  }
21
};
22

23
int main () {
24
  const char* index_html = ...;
25
  Processor processor = ... ;
26

27
  server.on("/", HTTP_GET, MyHTTPObserver { index_html, processor });
28
);

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:

1
class CallbackData {
2
  const char* index_html;
3
  Processor processor;
4
  void operator () (AsyncWebServerRequest *request) {
5
    request->send_P(200, "text/html", index_html, processor);
6
  }
7
};
8

9
int main () {
10
  const char* index_html = ...;
11
  Processor processor = ... ;
12

13
  server.on("/", HTTP_GET, CallbackData { index_html, processor });
14
);

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:

1
void myCallback (AsyncWebServerRequest *request, const char* index_html, Processor processor) {
2
  request->send_P(200, "text/html", index_html, processor);
3
}
4

5
int main () {
6
  const char* index_html = ...;
7
  Processor processor = ... ;
8

9
  server.on("/", HTTP_GET, std::bind (&myCallback, std::placeholders::_1, index_html, processor));
10
);

Aber seit über 10 Jahren gibt es eben Lambdas, mit denen das dann wie eingangs gezeigt geht:

1
int main () {
2
  const char* index_html = ...;
3
  Processor processor = ... ;
4

5
  server.on("/", HTTP_GET, [&](AsyncWebServerRequest *request){
6
   request->send_P(200, "text/html", index_html, processor);
7
  });
8
);

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.

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren