Forum: Mikrocontroller und Digitale Elektronik c > struct > funktionszeiger casten


von Jan H. (janiiix3)


Lesenswert?

wie castet man den funktionszeiger richtig?
es gibt verschiedene funktionszeiger also verschiedene rückgabetypen..
1
float read_bat_voltage_avg();
2
uint8_t send_bat();
3
4
typedef struct{
5
  char id;
6
  void (* receive_function )( void );
7
}lora_cmd_t;
8
9
lora_cmd_t lora_cmd[] =
10
{
11
  { '0', (float)read_bat_voltage_avg }, // returns battery voltage
12
  { '1', (uint8_t)send_bat }, // dummy
13
};

Danke schon mal =)

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


Lesenswert?

Jan H. schrieb:
> wie castet man den funktionszeiger richtig?
> es gibt verschiedene funktionszeiger also verschiedene rückgabetypen..

Casten?
Gar nicht!
Ein solcher Zeiger sollte Typsicher sein.
Also nix mit cast!

von Jan H. (janiiix3)


Lesenswert?

Arduino F. schrieb:
> Jan H. schrieb:
>> wie castet man den funktionszeiger richtig?
>> es gibt verschiedene funktionszeiger also verschiedene rückgabetypen..
>
> Casten?
> Gar nicht!
> Ein solcher Zeiger sollte Typsicher sein.
> Also nix mit cast!

was könnte ich stattdessen machen, wenn dort es dort mehrere 
rückgabetypen gibt?

von Dergute W. (derguteweka)


Lesenswert?

Moin,

Jan H. schrieb:
> wie castet man den funktionszeiger richtig?

Wuerde ich garnicht machen. Ich halt's nicht fuer klug, dem Compiler 
erst zu versichern, dass eine Funktion so rein ueberhaupt garnix 
zurueckgibt, und dann tut sie's ploetzlich doch, und dann auch noch 
verschiedenes...

Gruss
WK

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


Lesenswert?

Ansonsten, wenn wirklich, dann den Umweg über void zeiger.
1
typedef struct{
2
  char id;
3
  void *receive_function;
4
}lora_cmd_t;

Oder was mit union basteln.

: Bearbeitet durch User
von Jan H. (janiiix3)


Lesenswert?

Arduino F. schrieb:
> Ansonsten, wenn wirklich, dann den Umweg über void zeiger.typedef
> struct{
>   char id;
>   void *receive_function;
> }lora_cmd_t;
>
> Oder was mit union basteln.

ich bin für vorschläge offen, wie würdet ihr das machen?

von Harald K. (kirnbichler)


Lesenswert?

Jan H. schrieb:
> was könnte ich stattdessen machen, wenn dort es dort mehrere
> rückgabetypen gibt?

Das geht nicht, das Konzept ist kaputt.

Was Du stattdessen machen könntest: Den Funktionen wird ein void-Pointer 
übergeben, der auf einen Puffer zeigt, und dazu ein Argument, das dessen 
Größe in Bytes signalisiert. Zusätzlich noch ein weiterer Pointer auf 
ein enum, das den schlussendlich rauskommenden Typ signalisiert.

Das Konzept entspricht dem eines "Variant"-Datentyps.

Das würde funktionieren, machts aber letztlich auch kaum sinnvoller, 
weil der Aufrufer jeweils das "zurückgegebene" Resultat korrekt 
auseinanderpflücken und einsortieren muss.

Vielleicht solltest Du nochmal genauer darüber nachdenken, welches 
Problem Du eigentlich lösen möchtest.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Jan H. schrieb:
> ich bin für vorschläge offen, wie würdet ihr das machen

Sag doch erstmal wie du den Zeiger aufrufen und dabei den Rückgabewert 
nutzen willst...

von Dergute W. (derguteweka)


Lesenswert?

Moin,

Jan H. schrieb:
> wie würdet ihr das machen?

Bau wrapper um deine Funktionen, die verschiedene Typen zurueckgeben, 
die die verschiedenen Typen auf einen Typen entsprechend wandeln.
Diese Wrapperfunktionen kannste dann ueber fktptr aufrufen.

Gruss
WK

von Ko (emko)


Lesenswert?

Hey,
du willst zu einem Funktionspointer casten und nicht einfach zum 
Rückgabetyp.

Also nach (void (*)(void)) und beim Aufruf der Funktion nach ((T 
(*)(void))

Gruß
emko

von Jan H. (janiiix3)


Lesenswert?

Harald K. schrieb:
> Jan H. schrieb:
>> was könnte ich stattdessen machen, wenn dort es dort mehrere
>> rückgabetypen gibt?
>
> Das geht nicht, das Konzept ist kaputt.
>
> Was Du stattdessen machen könntest: Den Funktionen wird ein void-Pointer
> übergeben, der auf einen Puffer zeigt, und dazu ein Argument, das dessen
> Größe in Bytes signalisiert. Zusätzlich noch ein weiterer Pointer auf
> ein enum, das den schlussendlich rauskommenden Typ signalisiert.
>
> Das Konzept entspricht dem eines "Variant"-Datentyps.
>
> Das würde funktionieren, machts aber letztlich auch kaum sinnvoller,
> weil der Aufrufer jeweils das "zurückgegebene" Resultat korrekt
> auseinanderpflücken und einsortieren muss.
>
> Vielleicht solltest Du nochmal genauer darüber nachdenken, welches
> Problem Du eigentlich lösen möchtest.

okay, ich verstehe..

es geht darum eine funktion nach erhalt eines bestimmten commands 
auszuführen..

empfange ich z.b.: "toggle#0" soll halt die funktion mit der id.: 0 
ausgeführt werden usw.

das war der plan dahinter.. quasi eine tabelle wo meine ganzen 
funktionen vorhanden sind die dann nach "index" aufgerufen werden 
können.

von Jan H. (janiiix3)


Lesenswert?

Dergute W. schrieb:
> WK

haste mal ein beispiel wie du das meinst?

von Jan H. (janiiix3)


Lesenswert?

Memphis R. schrieb:
> Also nach (void (*)(void)) und beim Aufruf der Funktion nach ((T
> (*)(void))

was soll "T" sein? :-O

von Dergute W. (derguteweka)


Lesenswert?

Jan H. schrieb:
> Dergute W. schrieb:
>> WK
>
> haste mal ein beispiel wie du das meinst?

Damit haben send_bat_wrapper() und read_bat_voltage_avg() die selbe 
Geschmacksrichtung von Funktionspointer:
1
float send_bat_wrapper(void) {
2
    float rc;
3
    rc = (float)send_bat();
4
    return rc;
5
}

Gruss
WK

von Ko (emko)


Lesenswert?

Jan H. schrieb:
> empfange ich z.b.: "toggle#0" soll halt die funktion mit der id.: 0
> ausgeführt werden usw.

Dann solltest du aber deine Datenstruktur überdenken und nicht versuchen 
alles mit nur einem Funktionspointer zu lösen.

Jan H. schrieb:
> was soll "T" sein? :-O

T ist ein Platzhalter für den Rückgabetyp der Funktion die aufgerufen 
wird.

von Sherlock 🕵🏽‍♂️ (rubbel-die-katz)


Lesenswert?

Jan H. schrieb:
> das war der plan dahinter.. quasi eine tabelle wo meine ganzen
> funktionen vorhanden sind die dann nach "index" aufgerufen werden
> können.

Warum nimmst du nicht einfach einrn switch-case?

Gute programmierer setzen solche Konstrukte nur ein, wenn sie wirklich 
notwendig sind.

von Harald K. (kirnbichler)


Lesenswert?

Jan H. schrieb:
> es geht darum eine funktion nach erhalt eines bestimmten commands
> auszuführen..

Das ist soweit klar, warum aber sollen all' diese Funktionen einen 
unterschiedlichen Rückgabewert haben?

von Jan H. (janiiix3)


Lesenswert?

Dergute W. schrieb:
> Damit haben send_bat_wrapper() und read_bat_voltage_avg() die selbe
> Geschmacksrichtung von Funktionspointer:
> float send_bat_wrapper(void) {
>     float rc;
>     rc = (float)send_bat();
>     return rc;
> }
>
> Gruss
> WK

die beiden haben aber nicht die gleiche "geschmacksrichtung" das eine 
ist uint8_t und das andere float ;)

von Jan H. (janiiix3)


Lesenswert?

Harald K. schrieb:
> Jan H. schrieb:
>> es geht darum eine funktion nach erhalt eines bestimmten commands
>> auszuführen..
>
> Das ist soweit klar, warum aber sollen all' diese Funktionen einen
> unterschiedlichen Rückgabewert haben?

also bis jetzt haben nur zwei funktionen float rückgabewerde die anderen 
werden wohl uint8_t haben

von Rolf M. (rmagnus)


Lesenswert?

FÜr sowas sind Unions gemacht. Da kann man alle Funktionszeigertypen 
drin definieren und dann über das richtige Element die Funktion 
aufrufen:
1
union MeineFunktion
2
{
3
    float (*floatFunktion)(void);
4
    int (*intFunktion)(void);
5
};
6
7
int funktion1(void)
8
{
9
    return 42;
10
}
11
12
float funktion2(void)
13
{
14
    return M_PI;
15
}
16
17
int main()
18
{
19
    union MeineFunktion fkt;
20
    fkt.intFuktion = funktion1;
21
    fkt.intFunktion();
22
23
    fkt.floatFuktion = funktion2;
24
    fkt.floatFunktion();
25
}

Natürlich muss man irgendwo noch die Information haben, welcher 
Funktionszeiger der richtige ist, um die Funktion entsprechend 
aufzurufen (man darf nicht intFunktion befüllen und dann floatFunktion 
aufrufen) und den Returnwert korrekt zu verarbeiten, aber das müsste man 
beim Cast ja auch.

Arduino F. schrieb:
> Ansonsten, wenn wirklich, dann den Umweg über void zeiger.

Die funktionieren zwar meistens auch, aber nur, weil die meisten 
Compiler an der Stelle gegen den Standard verstoßen. Eigentlich sind 
Zeiger auf Objekte und Zeiger auf Funktionen nicht kompatibel und können 
auch mit einem Cast nicht einfach in einander umgewandelt werden. Wenn 
man einen "generischen" Funktionszeiger in der Art nutzen möchte, sollte 
man void(*)(void) verwenden.

: Bearbeitet durch User
von Daniel V. (danvet)


Lesenswert?

Wie verwendest du denn die Struktur?
Ich würde beim Verwenden der Struktur anhand der ID entscheiden, welchen 
Rückgabewert die Funktion hat und dann entsprechend casten (da es ja ein 
Void-Pointer ist).
Also irgendwie so

typedef struct{
  char id;
  void (* receive_function )( void );
}lora_cmd_t;
lora_cmd_t lora_cmd[] =
{
  { '0', read_bat_voltage_avg }, // returns battery voltage
  { '1', send_bat }, // dummy
};

if(lora_cmd[0].id == 0)
{
   float return_value = (float)lora_cmd[1].receive_function();
}
else
{
   uint8_t return_value = (uint8_t)lora_cmd[1].receive_function();
}

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


Lesenswert?

Daniel V. schrieb:
> if(lora_cmd[0].id == 0)
> {
> float return_value = (float)lora_cmd[1].receive_function();

Wie genau soll da ein void nach float gecastet werden?

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


Lesenswert?

Rolf M. schrieb:
> Die funktionieren zwar meistens auch, aber nur, weil die meisten
> Compiler an der Stelle gegen den Standard verstoßen.

Ok....
Danke!

Die Zeiger casterei ist mir sowieso suspekt!
Und dank C++ auch meist vermeidbar.

von Rolf M. (rmagnus)


Lesenswert?

Niklas G. schrieb:
> Daniel V. schrieb:
>> if(lora_cmd[0].id == 0)
>> {
>> float return_value = (float)lora_cmd[1].receive_function();
>
> Wie genau soll da ein void nach float gecastet werden?

Klappt so natürlich nicht. Und nicht nur der Cast ist falsch, sondern es 
wird auch noch eine Funktion über den falschen Zeigertyp aufgerufen. Es 
müsste eher sowas sein:
1
float return_value = (float(*)(void))(lora_cmd[1].receive_function)();

Oder man nimmt eben die unions und spart sich die ganze Cast-Orgie.

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


Lesenswert?

Rolf M. schrieb:
> Die funktionieren zwar meistens auch, aber nur, weil die meisten
> Compiler an der Stelle gegen den Standard verstoßen. Eigentlich sind
> Zeiger auf Objekte und Zeiger auf Funktionen nicht kompatibel und können
> auch mit einem Cast nicht einfach in einander umgewandelt werden.

Unter POSIX-Systemen ist es erlaubt, dies ist für die Funktion dlsym() 
erforderlich. Aber C und C++ an sich garantieren das nicht.

Rolf M. schrieb:
> Wenn
> man einen "generischen" Funktionszeiger in der Art nutzen möchte, sollte
> man void(*)(void) verwenden.

Zwischen Funktionszeigern hin-und her-casten ist erlaubt, aber aufrufen 
darf man ihn nur über den "richtigen" (ursprünglichen) Typ.

Rolf M. schrieb:
> Oder man nimmt eben die unions und spart sich die ganze Cast-Orgie.

Ja, wobei man sich natürlich fragt: Wenn man sowieso ein switch-case für 
den Rückgabetyp braucht - wozu dann noch der Funktionszeiger?

von Rolf M. (rmagnus)


Lesenswert?

Niklas G. schrieb:
> Unter POSIX-Systemen ist es erlaubt, dies ist für die Funktion dlsym()
> erforderlich. Aber C und C++ an sich garantieren das nicht.

Windows hat eine vergleichbare Funktion, weshalb das da normalerweise 
auch funktioniert. Wie gesagt: Die meisten Compiler erlauben es. Ich 
vermeide es trotzdem, wenn ich kann.

> Rolf M. schrieb:
>> Wenn
>> man einen "generischen" Funktionszeiger in der Art nutzen möchte, sollte
>> man void(*)(void) verwenden.
>
> Zwischen Funktionszeigern hin-und her-casten ist erlaubt, aber aufrufen
> darf man ihn nur über den "richtigen" (ursprünglichen) Typ.

Ja, richtig. Man castet dann vom Typ der tatsächlichen Funktion auf 
void(*)(void) und vor dem Aufruf wieder zurück auf den ursprünglichen 
Typ. Deshalb hab ich auch geschrieben, dass man irgendwo noch die Info 
braucht, was denn der ursprüngliche Typ war.

> Rolf M. schrieb:
>> Oder man nimmt eben die unions und spart sich die ganze Cast-Orgie.
>
> Ja, wobei man sich natürlich fragt: Wenn man sowieso ein switch-case für
> den Rückgabetyp braucht - wozu dann noch der Funktionszeiger?

Es kann ja auch mehrere Einträge mit dem selben Typ geben. Soweit ich 
verstehe, will er eine Zuordnungstabelle haben zwischen bestimmten 
Parametern und den Funktionen, um diese in irgendeiner Form zu 
verarbeiten. Wenn's jetzt z.B. 100 float-Parameter gibt und 100 
Funktionen dafür, muss man in der Tabelle nur die Zeiger darauf 
hinterlegen und nicht im switch/case 100 separate Funktionsaufrufe dafür 
haben, also nicht ein case pro Funktion, sondern nur einer pro 
Funktions-Typ.

von Sherlock 🕵🏽‍♂️ (rubbel-die-katz)


Lesenswert?

Rolf M. schrieb:
> Wenn's jetzt z.B. 100 float-Parameter gibt und 100
> Funktionen dafür, muss man in der Tabelle nur die Zeiger darauf
> hinterlegen und nicht im switch/case 100 separate Funktionsaufrufe dafür
> haben, also nicht ein case pro Funktion, sondern nur einer pro
> Funktions-Typ.

Dafür erhält man einen Quelltext, der nur mühsam zu durchblicken ist. 
Das geht am Sinn einer Hochsprache vorbei. Code soll für andere 
Entwickler gut lesbar sein. Gut lesbarer Code vermeidet 
Missverständnisse und Fehler.

100 case sind gut lesbar. Deren große Anzahl ist kein Problem bezüglich 
Lesbarkeit.

: Bearbeitet durch User
von Rolf M. (rmagnus)


Lesenswert?

Sherlock 🕵🏽‍♂️ schrieb:
> Dafür erhält man einen Quelltext, der nur mühsam zu durchblicken ist.

Das sehe ich anders. Ich finde so eine Tabelle lesbarer, als so ein 
riesiges switch/case. Und wenn pro Eintrag noch mehr als eine ID und 
eine Funktion benötigt werden, ist die Lesbarkeit beim switch/case 
endgültig dahin.

> 100 case sind gut lesbar. Die schiere Anzahl ist kein Problem.

Nicht so gut wie die Tabelle meiner Meinung nach.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Arduino F. schrieb:
> Und dank C++ auch meist vermeidbar.

Jau:
1
template <typename Ret>
2
void wrapTypeErased (Ret (*fPtr) ()) {
3
    // Rufe Funktion auf
4
    Ret ret (fPtr ());
5
6
    // Mach was mit dem Rückgabewert "ret"
7
    std::cout << "LoRa-Funktion wurde aufgerufen, Rückgabewert ist: "
8
            // Hack: Nutze '+' Operator um char-Typen (inklusive (u)int8_t) nach Integer zu promoten
9
            // um diese als Zahl auszugeben statt als Zeichen
10
        << +ret
11
        << std::endl;
12
}
13
14
template <auto F>
15
constexpr void (*wrap ()) () {
16
    if constexpr (std::is_void_v<decltype (F())>) return F;
17
    else return [] () { return wrapTypeErased (F); };
18
}
19
20
lora_cmd_t lora_cmd[] =
21
{
22
  { '0', wrap<read_bat_voltage_avg> () }, // returns battery voltage
23
  { '1', wrap<send_bat> () }, // dummy
24
  { '2', wrap<do_something> () } // A function that returns void
25
};

In wrapTypeErased kann man dann was beliebiges mit dem Rückgabetyp 
machen, und weil es ein template ist hat "ret" halt immer den richtigen 
Rückgabetyp der jeweiligen Funktion. Hier wird es einfach auf den stdout 
ausgegeben, automatisch entsprechend formatiert. Man kann den 
Rückgabewert auch ignorieren aber wozu gibt es ihn dann überhaupt?

Das Ganze kommt ganz ohne Casts aus und ist typsicher. Das 
"wrap<EineFunktion> ()" wird letztlich zu einem Funktionszeiger auf eine 
automatisch generierte Funktion für den richtigen Rückgabewert, welche 
die eigentliche Funktion aufruft und dann hier als Beispiel den 
Rückgabewert ausgibt. Als Sonderfall wird bei void-Funktionen direkt der 
gewünschte Funktionszeiger verwendet, ohne Wrapper. Es wird pro 
Rückgabetyp eine Funktion generiert (nicht pro Funktion).

Es gibt aber noch mehr Möglichkeiten, z.B. mit std::function oder 
vielleicht auch std::variant + std::visit oder ganz klassisch 
Polymorphie.

von Nikolaus S. (Firma: Golden Delicious Computers) (hns)


Lesenswert?

Wenn es Standard-C bleiben muss (wie z.B. ein Linux-Kernel) muss man 
einen eigenen Datentyp definieren, den alle Funktionen als Ergebnis 
liefern können. Typischerweise ein Struct mit einer Typ-ID und einer 
eigebetteten Union.
1
struct return_typ {
2
  enum { TYP_FLOAT, TYP_UINT8, TYP_STRING } typ;
3
  union {
4
    float float_val;
5
    uint8_t uint8_val;
6
    char *string_val;
7
  } data;
8
};
9
10
struct return_typ read_bat_voltage_avg();
11
struct return_typ send_bat();
12
13
typedef struct{
14
  char id;
15
  struct return_typ (* receive_function )( void );
16
}lora_cmd_t;
17
18
lora_cmd_t lora_cmd[] =
19
{
20
  { '0', read_bat_voltage_avg }, // returns battery voltage
21
  { '1', send_bat }, // dummy
22
};
23
24
struct return_typ result = (*lora_cmd[1])();
25
switch (result.typ) {
26
  case TYP_FLOAT:  ... result.data.float_val;
27
  case TYP_UINT8:  ... result.data.uint8_val;
28
  ...
29
};
Evtl. wäre es notwendig einen pointer auf ein struct return_typ als 
Ergebnis zu liefern.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Nikolaus S. schrieb:
> Typischerweise ein Struct mit einer Typ-ID und einer
> eigebetteten Union.

Kriegt man das nicht besser mit Generic selection hin? Da muss man den 
Wrappercode halt für jeden Typ einzeln schreiben (statt mit dem template 
in C++), aber es spart die Laufzeit-Prüfung und das Verpacken/Auspacken 
im struct.

von Jan H. (janiiix3)


Lesenswert?

Rolf M. schrieb:
> union MeineFunktion
> {
>     float (*floatFunktion)(void);
>     int (*intFunktion)(void);
> };
> int funktion1(void)
> {
>     return 42;
> }
> float funktion2(void)
> {
>     return M_PI;
> }

Dann kann ich aber nicht mit meiner "Tabelle" weiterarbeiten?

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Jan H. schrieb:
> Dann kann ich aber nicht mit meiner "Tabelle" weiterarbeiten?

Doch, du musst die union halt statt des Funktionszeigers in die Tabelle 
eintragen.

von Jan H. (janiiix3)


Lesenswert?

Nikolaus S. schrieb:
> struct return_typ result = (*lora_cmd[1])();

Was genau bewirkt diese Zeile?

von Sherlock 🕵🏽‍♂️ (rubbel-die-katz)


Lesenswert?

Jan H. schrieb:
>> struct return_typ result = (*lora_cmd[1])();
> Was genau bewirkt diese Zeile?

Führe die Funktion aus, auf die "lora_cmd[1]" zeigt, und zwar ohne 
Argumente, und lege das Ergebnis in der Variable "result" ab, die eine 
Struktur vom Typ "return_typ" ist.

von Jan H. (janiiix3)


Angehängte Dateien:

Lesenswert?

Nikolaus S. schrieb:
> struct return_typ result = (*lora_cmd[1])();
> switch (result.typ) {
>   case TYP_FLOAT:  ... result.data.float_val;
>   case TYP_UINT8:  ... result.data.uint8_val;
>   ...
> };

ich habe das mal so in den online debugger gehauen und der wirft mir da 
fehler, siehe foto.

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


Lesenswert?

Jan H. schrieb:
> ich habe das mal so in den online debugger gehauen und der wirft mir da
> fehler, siehe foto.

Ja es muss so sein:
1
struct return_typ result = (lora_cmd[1].receive_function)();

von Jan H. (janiiix3)


Lesenswert?

Niklas G. schrieb:
> Jan H. schrieb:
>> ich habe das mal so in den online debugger gehauen und der wirft mir da
>> fehler, siehe foto.
>
> Ja es muss so sein:
>
> 1
> struct return_typ result = (lora_cmd[1].receive_function)();

Okay dann verstehe ich es.
Und das mit den cases funktioniert so auch nicht oder?
1
struct return_typ result = (*lora_cmd[1])();
2
switch (result.typ) {
3
  case TYP_FLOAT:  ... result.data.float_val;
4
  case TYP_UINT8:  ... result.data.uint8_val;
5
  ...
6
};

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


Lesenswert?

Jan H. schrieb:
> Und das mit den cases funktioniert so auch nicht oder?

Na du musst halt noch was einfügen, was den Rückgabewert verarbeitet. 
Und ein "break;" in jeden "case". Schreib doch mal was du mit dem 
Rückgabewert überhaupt machen würdest, da könnte man dir vermutlich viel 
besser helfen.

: Bearbeitet durch User
von Jan H. (janiiix3)


Lesenswert?

Niklas G. schrieb:
> Jan H. schrieb:
>> Und das mit den cases funktioniert so auch nicht oder?
>
> Na du musst halt noch was einfügen, was den Rückgabewert verarbeitet.
> Und ein "break;" in jeden "case". Schreib doch mal was du mit dem
> Rückgabewert überhaupt machen würdest, da könnte man dir vermutlich viel
> besser helfen.

Es geht ja erstmal um das Prinzip.
Der Compiler findet die enums im Struct nicht.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Jan H. schrieb:
> Der Compiler findet die enums im Struct nicht.

Zeig die Fehlermeldung. Es müsste eigentlich so gehen.

Jan H. schrieb:
> Es geht ja erstmal um das Prinzip.

Naja, es gibt hier viele prinzipielle Möglichkeiten. Dein ursprünglicher 
Wunsch passt nicht so recht auf die üblichen Prinzipien einer statisch 
typisierten Sprache, insbesondere nicht von C. Daher gibt es kein 
Patentrezept. Deine konkrete Anwendung besser zu verstehen könnte helfen 
das Problem vielleicht ganz anders besser zu lösen (X-Y-Problem).

Am Einfachsten ist es, die Verarbeitung, die in das switch-case kommen 
soll, einfach in die einzelnen Funktionen zu verschieben. Dann löst sich 
das alles in Luft auf.

von Harald K. (kirnbichler)


Lesenswert?

Niklas G. schrieb:
> Am Einfachsten ist es

... die kaputte Designentscheidung über den Haufen zu werfen, Funktionen 
mit unterschiedlicher Signatur gemischt verwenden zu wollen.

Jan H. schrieb:
> also bis jetzt haben nur zwei funktionen float rückgabewerde die anderen
> werden wohl uint8_t haben

Warum nur?

von Sherlock 🕵🏽‍♂️ (rubbel-die-katz)


Lesenswert?

Man kann Werte von 0-256 durchaus auch als float zurück geben.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Sherlock 🕵🏽‍♂️ schrieb:
> Man kann Werte von 0-256 durchaus auch als float zurück geben.

Nach dem Motto: Der Code ist schlecht, lasst uns ihn noch schlechter 
machen!

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

In reinem C könnte man es mit _Generic so machen:
1
void callWrapper_float (float (*fPtr) ()) {
2
    float result = fPtr ();
3
    printf ("Float result: %f\n", result);
4
}
5
6
void callWrapper_uint8_t (uint8_t (*fPtr) ()) {
7
    uint8_t result = fPtr ();
8
    printf ("uint8_t result: %d\n", (int) result);
9
}
10
11
#define DEFINE_WRAPPER(fun) void callWrapper_ ## fun () { \
12
                        _Generic ((fun) (), \
13
                            float: callWrapper_float ((float (*) ()) &fun), \
14
                            uint8_t: callWrapper_uint8_t ((uint8_t (*) ()) &fun) \
15
                        ); \
16
                    }
17
18
#define GET_WRAPPER(fun) (callWrapper_ ## fun)
19
20
DEFINE_WRAPPER(read_bat_voltage_avg)
21
DEFINE_WRAPPER(send_bat)
22
23
typedef struct{
24
  char id;
25
  void (* receive_function )( void );
26
} lora_cmd_t;
27
28
lora_cmd_t lora_cmd[] =
29
30
{
31
  { '0', GET_WRAPPER (read_bat_voltage_avg) }, // returns battery voltage
32
  { '1', GET_WRAPPER (send_bat) }, // dummy
33
};
34
35
int main () {
36
    lora_cmd [0].receive_function ();
37
    lora_cmd [1].receive_function ();
38
}

Man muss für jeden Typ eine eigene "callWrapper_XXX" -Funktion schreiben 
welche das Ergebnis verarbeitet, und dies dann im DEFINE_WRAPPER in das 
_Generic eintragen. Blöderweise muss man den Pointer noch mal casten, 
weil bei _Generic alle Zweige gültig sein müssen, auch wenn sie nie 
evaluiert werden. Dann muss man für jede der Handler-Funktionen per 
GET_WRAPPER einen Wrapper definieren und diesen per GET_WRAPPER in die 
Liste eintragen.

Macht im Endeffekt das Gleiche wie obige C++ -Version, aber eben in 
umständlich dank C, ist aber ein bisschen effizienter als das 
switch-case mit dem return_typ.

von Sherlock 🕵🏽‍♂️ (rubbel-die-katz)


Lesenswert?

Johann L. schrieb:
> Nach dem Motto: Der Code ist schlecht, lasst uns ihn noch schlechter
> machen!

Das finde ich gar nicht. Wenn alle Funktionen den gleichen Rückgabetyp 
haben, wäre die ganze Diskussion hier überflüssig. Man könnte das 
einfach so ohne Tricks runter programmieren.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

So ginge es in C++ mit einer std::variant:
1
using RetType = std::variant<std::uint8_t, float>;
2
3
typedef struct{
4
5
  char id;
6
7
  RetType (* receive_function )( void );
8
9
}lora_cmd_t;
10
11
template <auto F>
12
constexpr RetType (*wrap ()) () {
13
    return [] () -> RetType {
14
        return RetType { std::in_place_type_t<decltype (F ())> {}, F() };
15
    };
16
}
17
18
lora_cmd_t lora_cmd[] =
19
{
20
  { '0', wrap<read_bat_voltage_avg> () }, // returns battery voltage
21
  { '1', wrap<send_bat> () } // dummy
22
};
23
24
int main () {
25
    int lokaleVariable = 42;
26
27
    std::visit ([&] (const auto& ret) {
28
            // Mach was mit dem Rückgabewert "ret"
29
            std::cout << "LoRa-Funktion wurde aufgerufen, Rückgabewert ist: "
30
                // Hack: Nutze '+' Operator um char-Typen (inklusive (u)int8_t) nach Integer zu promoten
31
                // um diese als Zahl auszugeben statt als Zeichen
32
                << +ret
33
                << ", dabei ist lokaleVariable=" << lokaleVariable
34
                << std::endl;
35
    }, lora_cmd [0].receive_function ());
36
}

Der Rückgabewert wird in ein Variant eingepackt, und beim Aufruf gibt 
man ein generisches Lambda an, was dann mit dem korrekten Typ aufgerufen 
wird. Der Vorteil daran ist, dass man bei jedem Aufruf einen 
individuellen Codeblock angeben kann, der auch auf lokale Variablen 
zugreifen kann, aber es nicht wie in C für jeden Typ manuell machen 
muss. Im Endeffekt ist es das gleiche wie der Vorschlag mit dem 
return_typ, aber handlicher.

von Peter D. (peda)


Lesenswert?

Wozu das umständliche switch-case Gehampel. Der Aufrufer weiß doch, was 
er auswerten will. Einfach nur eine union aus float und int als 
Returnwert und gut. Für sowas sind unions schließlich gedacht.
1
#include <stdint.h>
2
3
union fi {
4
  uint8_t u8 ;
5
  float f;
6
};
7
8
union fi read_bat_voltage_avg()
9
{
10
  union fi r;
11
  r.f = 1.23;
12
  return r;
13
}
14
15
union fi send_bat()
16
{
17
  union fi r;
18
  r.u8 = 123;
19
  return r;
20
}
21
22
typedef struct{
23
  char id;
24
  union fi (* receive_function )( void );
25
}lora_cmd_t;
26
27
lora_cmd_t lora_cmd[] =
28
{
29
  { '0', read_bat_voltage_avg }, // returns battery voltage
30
  { '1', send_bat }, // dummy
31
};

von Daniel A. (daniel-a)


Lesenswert?

Ich kann mir noch nicht so recht vorstellen, wie das nachher aussehen / 
verwendet werden soll. Sind das quasi Requests, die immer genau eine 
Response haben, und dort ist immer nur 1 Wert drin? Dann kann man das 
eventuell machen.
Andernfalls hätte man normalerweise irgend eine Funktion, die eine 
Nachricht zurück sendet, die man dann halt aufruft, und die das dann in 
eine Queue packt.

Wie dem auch sei, wie wäre es mit einer tagged Union?
1
enum lora_ret_type {
2
  LRT_u8,
3
  LRT_f,
4
};
5
6
typedef struct lora_ret {
7
  enum lora_ret_type type;
8
  union {
9
    uint8_t u8;
10
    float   f;
11
  };
12
} lora_ret_t;
13
14
#define LORA_RET(T, V) return ((lora_ret_t){ .type=LRT_ ## T, .T=(V) })
15
16
typedef lora_ret_t lora_cmd_handler(void);
17
18
lora_cmd_handler read_bat_voltage_avg;
19
lora_cmd_handler send_bat;
20
21
typedef struct{
22
  char id;
23
  lora_cmd_handler* handler;
24
}lora_cmd_t;
25
26
lora_cmd_t lora_cmd[] =
27
{
28
  { '0', read_bat_voltage_avg }, // returns battery voltage
29
  { '1', send_bat }, // dummy
30
};
31
32
lora_ret_t send_bat(void){
33
  LORA_RET(u8, 123);
34
}
35
36
...
37
  lora_ret_t ret = lora_cmd[X].handler();
38
  switch(ret.type){
39
    case LRT_u8: printf("LRT_u8: %d\n", ret.u8); break;
40
    case LRT_f : printf("LRT_f: %f\n", ret.f); break;
41
  }

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


Lesenswert?

Daniel A. schrieb:
> Wie dem auch sei, wie wäre es mit einer tagged Union?

Genau das hat Nikolaus oben schon so implementiert.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Jan H. schrieb:
> Und das mit den cases funktioniert so auch nicht oder?
1
> struct return_typ result = (*lora_cmd[1])();
2
> switch (result.typ) {
3
>   case TYP_FLOAT:  ... result.data.float_val;
4
>   case TYP_UINT8:  ... result.data.uint8_val;
5
>   ...
6
> };

Ich verstehe immer noch nicht, welches Problem du so lösen willst.

Spätestens wenn es an die Auswertung der Rückgabewerte geht, braucht's 
doch eh ein if-else oder switch-case?

Also warum dann nicht einfach:
1
switch (lora_id)
2
{
3
   case LORA_ID_DIES:
4
   {
5
      uint8_t val8 = lora_mach_dies ();
6
      // Eval val8...
7
      break;
8
   }
9
   case LORA_ID_JENES:
10
   {
11
      float f = lora_mach_jenes ();
12
      // Eval f...
13
      break;
14
   }

Ist das mit einer Union und Funktionszeiger-Tabelle so viel effizienter?
Oder so viel übersichtlicher?

Kaum.

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


Lesenswert?

Johann L. schrieb:
> Kaum.

Mir schmeckt die id (bei dir lora_id) auch nicht.

Alleine schon weil man min. 2 Stellen im Code hat, die man zueinander 
konsistent halten muss.
Einmal im Array, und dann in dem switch/case
Vertut man sich da, kann das zu recht schwer zu findenden Fehlern 
führen.
Schöner ist es, wenn der Compiler die Chance hat da einen Irrtum 
anzumeckern.

von Veit D. (devil-elec)


Lesenswert?

Jan H. schrieb:

> es geht darum eine funktion nach erhalt eines bestimmten commands
> auszuführen..
>
> empfange ich z.b.: "toggle#0" soll halt die funktion mit der id.: 0
> ausgeführt werden usw.

Ich würde mir einen Parser schreiben der den seriellen Empfang, oder 
woher auch immer das kommt, aufdrösselt in das Kommando 'toggle' und den 
weiteren Parameter '0'. Dann hat man 2 Parameter die man auf alles 
vergleichen kann was man möchte. Ist irgendein Vergleich 'true' wird 
entsprechend irgendwas aufgerufen oder verarbeitet oder was weiß ich.
Am Ende kann man sonstwas Empfangen solange das Protokoll passt und 
verarbeiten.

von Frank M. (ukw) (Moderator) Benutzerseite


Lesenswert?

Meiner Meinung nach ist die Verwendung von Funktionspointern bei 
unterschiedlichen Rückgabewerten ein kaputtes Konzept.

Begründung

Funktionspointer sind dafür da, gezielt eine Funktion aus einer Familie 
von Funktionen aufzurufen, die das gleiche Interface benutzen. Nur hier 
kann man die Parameter und Rückgabewerte gleich behandeln.

Bei unterschiedlichen Interfaces wie zum Beispiel unterschiedlichen 
Rückgabewerten wird immer der Kontext relevant, wie der Rückgabewert 
zu behandeln ist. Das heißt: Spätestens nach Aufruf der Funktion wird 
eine Bewertung des Kontextes notwendig, um den Rückgabewert richtig 
interpretieren zu können. Dann kann man auch die Funktion in der 
Kontextbehandlung direkt aufrufen statt vorher abstrakt über einen 
Pointer zu gehen.

Fazit

Nimm einen Case-Switch und rufe hier die Funktionen konkret auf. Dort 
kannst Du dann auch den Rückgabewert korrekt (d.h. im Kontext) 
auswerten.

von Rocco F. (rocco122)


Lesenswert?

Frank M. schrieb:
> Meiner Meinung nach ist die Verwendung von Funktionspointern bei
> unterschiedlichen Rückgabewerten ein kaputtes Konzept.
>
> Begründung
>
> Funktionspointer sind dafür da, gezielt eine Funktion aus einer Familie
> von Funktionen aufzurufen, die das gleiche Interface benutzen. Nur hier
> kann man die Parameter und Rückgabewerte gleich behandeln.

> Nimm einen Case-Switch und rufe hier die Funktionen konkret auf. Dort
> kannst Du dann auch den Rückgabewert korrekt (d.h. im Kontext)
> auswerten.

Ich verstehe Ihren Standpunkt, aber manchmal sind Funktionszeiger 
nützlich, wenn Sie eine von mehreren Funktionen zur Ausführung auswählen 
müssen. In diesem Fall ist es wichtig, die richtige Schnittstelle zu 
wählen. Wenn die Rückgabewerte jedoch unterschiedlich sind, kann dies 
wirklich zu einem Problem werden.

von Bruno V. (bruno_v)


Lesenswert?

Jan H. schrieb:
> wie castet man den funktionszeiger richtig?

Es spricht nichts dagegen, Unions, void-Rückgabewerte oder irgendwas zu 
verwenden. Casten ist aber auch möglich. Am einfachsten mit Typedefs:
1
typedef float    (*pFloatFncType)(void);
2
typedef uint8_t  (*pu8FncType)(void);
3
typedef void     (*pVoidFncType)(void);
4
5
float read_bat_voltage_avg(void);
6
uint8_t send_bat(void);
7
typedef struct{
8
   char id;
9
   pVoidFncType fnc;
10
}lora_cmd_t;
11
12
lora_cmd_t lora_cmd[] =
13
{
14
   { '0', (pVoidFncType) read_bat_voltage_avg }, 
15
   { '1', (pVoidFncType) send_bat }, 
16
};
17
18
lora_cmd_t cmd = lora_cmd[x];
19
float f;
20
uint8_t u8;
21
22
   switch(cmd.id)
23
   {
24
   case 0: f = ((pFloatFncType) cmd.fnc)(); 
25
           break;
26
   case 1: u8 = ((pu8FncType) cmd.fnc)(); 
27
           break;
28
   }

Mag sein, dass ich ein * oder sowas vergessen habe, aber prinzipiell 
geht das. (pVoidFncType ist nicht notwendig, aber vielleicht ein 
Hinweis, dass man in jedem Fall den richtigen cast auswählen muss)

: Bearbeitet durch User
von Harald K. (kirnbichler)


Lesenswert?

Rocco F. schrieb:
> Wenn die Rückgabewerte jedoch unterschiedlich sind, kann dies
> wirklich zu einem Problem werden.

Darum dreht es sich hier doch die ganze Zeit, von Anfang an.

Das ist das, was an der Idee hier kaputt ist. Nicht die Verwendung von 
Funktionspoinern, sondern die Verwendung von Funktionspointern mit 
inkompatibler Funktionssignatur.

Und bislang wurde auch noch nicht erwähnt, welchen tieferen Sinn es 
haben soll, daß die verwendeten Funktionen unterschiedliche 
Rückgabetypen haben.

So viel Aufwand, wie hier darauf gelegt wurde, Krücken zu basteln, um 
diese kaputte Idee irgendwie zu ermöglichen, so WENIG Aufwand wurde vom 
Threadstarter betrieben, auch nur einen einzigen Erklärungsversuch zu 
bringen. Mehr als "ist so" kam nicht.

von Bruno V. (bruno_v)


Lesenswert?

Harald K. schrieb:
> Nicht die Verwendung von
> Funktionspoinern, sondern die Verwendung von Funktionspointern mit
> inkompatibler Funktionssignatur.

Ein guter Einsatz von Funktionspointern ist, wenn beim Init oder 
Konfigurieren (oder in der Statemachine) die (nächste) Funktion gesetzt 
wird und der Aufrufer einfach den Ptr aufruft.

Wenn (wie hier) ein Eintrag aus einer Liste gewählt wird, dann ist das 
aufwändiger und nicht mehr sooo smart. Auf den case für den Cast kommt 
es dann auch nicht mehr an. Meist kann das ganze Konstrukt durch plain 
Code vereinfacht werden.

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.