Forum: Mikrocontroller und Digitale Elektronik Suche Erklärung für C++ Code


von Oliver P. (mace_de)


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

von J. S. (jojos)


Lesenswert?

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.

von Oliver S. (oliverso)


Lesenswert?

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

von Arduino F. (Firma: Gast) (arduinof)


Lesenswert?

Oliver P. schrieb:
> hab ich vielleicht nicht nach den
> richtigen Begriffen gesucht.

Hier eine aktuelle Quelle:
https://en.cppreference.com/w/cpp/language/lambda

von Oliver P. (mace_de)


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.

von Niklas G. (erlkoenig) Benutzerseite


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.

: Bearbeitet durch User
von Bruno V. (bruno_v)


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.

von Oliver P. (mace_de)


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.

von Arduino F. (Firma: Gast) (arduinof)


Lesenswert?

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

: Bearbeitet durch User
von Rbx (rcx)


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

von Niklas G. (erlkoenig) Benutzerseite


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.

von Peter D. (peda)


Lesenswert?


von Rbx (rcx)


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)

von Dennis S. (eltio)


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
- 
https://arduino.stackexchange.com/questions/89362/why-is-the-server-on-function-from-espasyncwebserver-h-executed-in-the-setup
- 
https://forum.arduino.cc/t/please-explain-syntax-asyncwebserverrequest-request/1020064

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

von Michael K. (brutzel)


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.

: Bearbeitet durch User
von Bruno V. (bruno_v)


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.

von Yalu X. (yalu) (Moderator)


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.

: Bearbeitet durch Moderator
Beitrag #7744888 wurde von einem Moderator gelöscht.
von Johann L. (gjlayde) Benutzerseite


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: [](<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
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.

: Bearbeitet durch User
von Arduino F. (Firma: Gast) (arduinof)


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
}

von Yalu X. (yalu) (Moderator)


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

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

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.

von Yalu X. (yalu) (Moderator)


Lesenswert?

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?

von Arduino F. (Firma: Gast) (arduinof)


Lesenswert?

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

: Bearbeitet durch User
von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

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.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Arduino F. schrieb:
> int a = 34.6;
> geht ohne Gemeckere durch.
1
warning: conversion from 'double' to 'int' changes value from '3.46e+0' to '3' [-Wfloat-conversion]
2
   20 | int a = 3.46;
3
      |         ^~~~

von Arduino F. (Firma: Gast) (arduinof)


Lesenswert?

Johann L. schrieb:
> -Wfloat-conversion

Gehört nicht zu -Wall
Drum habe ich auch extra -Wall mit in den Text aufgenommen.

von Johann L. (gjlayde) Benutzerseite


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.

: Bearbeitet durch User
von Arduino F. (Firma: Gast) (arduinof)


Lesenswert?

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.

von Yalu X. (yalu) (Moderator)


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.

: Bearbeitet durch Moderator
von Arduino F. (Firma: Gast) (arduinof)


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.

von Yalu X. (yalu) (Moderator)


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

von Rbx (rcx)


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.

von Arduino F. (Firma: Gast) (arduinof)


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.

: Bearbeitet durch User
von Yalu X. (yalu) (Moderator)


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

von Yalu X. (yalu) (Moderator)


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?

von Arduino F. (Firma: Gast) (arduinof)


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:
> Ich weiche davon nicht ab, es (möglichst) einheitlich zu tun.

von Yalu X. (yalu) (Moderator)


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.

von Daniel -. (root)


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.

von Oliver P. (mace_de)


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

von Niklas G. (erlkoenig) Benutzerseite


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.

: Bearbeitet durch User
von Arduino F. (Firma: Gast) (arduinof)


Lesenswert?

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.

von Oliver S. (oliverso)


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. C++ wird immer umfangreicher und die Ausdrücke immer
> kryptischer.

Da bleibt man jung im Kopf.

Oliver

von Oliver P. (mace_de)


Lesenswert?

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

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.