Forum: PC-Programmierung Vertracktes typedef für Funktionspointer


von Uhu U. (uhu)


Lesenswert?

Ein Funktionspointer auf eine Funktion, die einen void-Pointer 
zurückgibt:
1
typedef void *(*TimeoutHandler)(void);

Wie muss das Ding aussehen, wenn die Funktion statt einem void * einen 
Funktionspointer auf eine Funktion des Typs zurückgibt, der mit dem 
typedef definiert wird?

von Arc N. (arc)


Lesenswert?

Uhu U. schrieb:
> Ein Funktionspointer auf eine Funktion, die einen void-Pointer
> zurückgibt:
>
1
> typedef void *(*TimeoutHandler)(void);
2
>
>
> Wie muss das Ding aussehen, wenn die Funktion statt einem void * einen
> Funktionspointer auf eine Funktion des Typs zurückgibt, der mit dem
> typedef definiert wird?

Zwei Typedefs?
1
typedef void* (*FP1)(void);
2
typedef FP1 (*Handler)(void);

von Praktiker (Gast)


Lesenswert?

Uhu U. schrieb:
> Ein Funktionspointer auf eine Funktion, die einen void-Pointer
> zurückgibt:typedef void *(*TimeoutHandler)(void);

Na, na, na, ...

von Uhu U. (uhu)


Lesenswert?

Arc N. schrieb:
> Zwei Typedefs?typedef void* (*FP1)(void);
> typedef FP1 (*Handler)(void);

Nein, eher sowas hier:
1
typedef Handler (*Handler)(void);

Nur: wie sag ich meinem Compiler?

von Arc N. (arc)


Lesenswert?

Uhu U. schrieb:
> Arc N. schrieb:
>> Zwei Typedefs?typedef void* (*FP1)(void);
>> typedef FP1 (*Handler)(void);
>
> Nein, eher sowas hier:
>
1
> typedef Handler (*Handler)(void);
2
>
>
> Nur: wie sag ich meinem Compiler?

Nochmal auf Start:
1
// Ein Handler soll sein?
2
void IAmAHandler(void) { }
3
4
// Ein Zeiger darauf wäre dann
5
typedef void (*HandlerPtr)(void);
6
7
// Eine Funktion die einen HandlerPtr zurückliefert dann bspw.
8
HandlerPtr ReturnsAHandler(void) { return &IAmAHandler; }
9
10
// und ein Zeiger auf eine Funktion die einen HandlerPtr zurückliefert bspw.:
11
typedef HandlerPtr (*WhateverThisIsSupposedToBe1)(void);
12
13
// andere "Schreibweise" für WhateverThisIsSupposedToBe1:
14
typedef void (*(*WhateverThisIsSupposedToBe2)(void))(void);
15
16
WhateverThisIsSupposedToBe2 Whatever(void) {
17
    return &ReturnsAHandler;
18
}
19
20
// und wenn das noch eine Ebene weiter gehen soll
21
typedef void (*(*(*WhateverThisIsSupposedToBe3)(void))(void))(void);
22
23
WhateverThisIsSupposedToBe3 Whatever2(void) {
24
    return &Whatever;
25
}

: Bearbeitet durch User
von Daniel A. (daniel-a)


Lesenswert?

Um einen Pointer auf einen Datentyp in sich selbst verwenden zu können 
must du diesen erst als "incomplete type" deklarieren, also sagen das er 
existiert, aber nicht, was er beinhaltet. Dies ist bei Funktionspointern 
nicht möglich, bei structs und unions aber schon. Der folgende c code 
kommt dem, was der TO sucht noch am nächsten:
1
typedef struct function function;
2
struct function {
3
  function(*call)(void);
4
};
5
6
function myfunc(){
7
  printf("test\n");
8
  return (function){myfunc};
9
}
10
11
int main(){
12
  function f = myfunc().call().call();
13
  f.call();
14
  return 0;
15
}

In C++ könnte man jetzt noch den Funktionsoperator des structs function 
überladen. ( Mein erster versuch in c++11 war "using f = f(*)();", 
funktioniert leider nicht ).

von Arc N. (arc)


Lesenswert?

Daniel A. schrieb:
> In C++ könnte man jetzt noch den Funktionsoperator des structs function
> überladen. ( Mein erster versuch in c++11 war "using f = f(*)();",
> funktioniert leider nicht ).

Mit std::add_pointer sollte das machbar sein.

von Uhu U. (uhu)


Lesenswert?

Arc N. schrieb:
> // Ein Handler soll sein?
> void IAmAHandler(void) { }

In Worten: eine Funktion, die einen Pointer auf sich selbst zurück gibt.

Mit einem typecast kann man das hin biegen, wenn die Funktion einen void 
* zurück gibt:
1
typedef void *(*TimeoutHandler)(void);
2
static uint16_t timeout;
3
static TimeoutHandler  timeoutHandler;
4
5
void *repeater(void) {
6
   if (done)
7
      return NULL;
8
   ...
9
   return repeater;
10
}
11
12
13
ISR(TIMER2_COMPA_vect) {
14
   if (timeout && !--timeout) {
15
      if (timeoutHandler) {
16
         timeoutHandler = (TimeoutHandler) timeoutHandler();
17
      }
18
   }
19
}
20
21
void ScheduleTimer2(uint16_t ticks, TimeoutHandler handler) {
22
   timeoutHandler = handler;
23
   timeout    = ticks;
24
}
25
26
void main(void) {
27
   ScheduleTimer2(100, repeater);
28
}

Sinn der Übung: die Funktion, die an ScheduleTimer2 übergeben wird, soll 
so lange alle 100 Ticks aufgerufen werden, wie sie einen Pointer auf 
sich selbst zurück gibt; beim Rückgabewert NULL soll beendet werden.

Die Frage lautet also: wie kann man mit einem typedef [in c!] den 
typcast (TimeoutHandler) in der ISR umgehen?

: Bearbeitet durch User
von Daniel A. (daniel-a)


Lesenswert?

Hier noch eine Variante mit C++11:
1
#include <functional>
2
#include <iostream>
3
4
template<typename... T>
5
class SelfRetFunction {
6
  public:
7
    typedef std::function<SelfRetFunction(T...)> type;
8
  private:
9
    type func;
10
  public:
11
    SelfRetFunction(type func)
12
     : func(func)
13
    {}
14
    SelfRetFunction(SelfRetFunction<T...>(*x)(T...))
15
     : func(x)
16
    {}
17
    SelfRetFunction(const SelfRetFunction<T...>& x)
18
     : func(x)
19
    {}
20
    type operator()(T... args){
21
      return func(args...);
22
    }
23
    operator type(){
24
      return func;
25
    }
26
};
27
28
SelfRetFunction<> myfunc(){
29
  std::cout << "test" << std::endl;
30
  return myfunc;
31
}
32
33
SelfRetFunction<int> myfunc2(int x){
34
  std::cout << x << std::endl;
35
  return myfunc2;
36
}
37
38
int main(){
39
  auto x = myfunc()()();
40
  x();
41
  myfunc2(1)(2);
42
  return 0;
43
}

von Daniel A. (daniel-a)


Lesenswert?

Uhu U. schrieb:
> Die Frage lautet also: wie kann man mit einem typedef [in c!] den
> typcast (TimeoutHandler) in der ISR umgehen?

Das ist in c einfach nicht möglich. Du kannst den Funktionstyp nicht 
nutzen, bevor du ihn deklariert hast, und du kannst Funktionstypen nicht 
unvollständig deklarieren. Somit kannst du den Funktionstyp den du 
möchtest, nicht deklarieren und somit auch nicht definieren.

von Mark B. (markbrandis)


Lesenswert?

Uhu U. schrieb:
> Sinn der Übung: die Funktion, die an ScheduleTimer2 übergeben wird, soll
> so lange alle 100 Ticks aufgerufen werden, wie sie einen Pointer auf
> sich selbst zurück gibt; beim Rückgabewert NULL soll beendet werden.

Das kann man sicher auch anders lösen. Zum Beispiel mit einem Flag.

> Die Frage lautet also: wie kann man mit einem typedef [in c!] den
> typcast (TimeoutHandler) in der ISR umgehen?

In C? Gar nicht.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Uhu U. schrieb:
> Sinn der Übung:

fraglich.  Wenn du zu so seltsamen Konstrukten geführt wirst, stimmt 
vermutlich was am Design nicht.  Auch wenn du es funktionierend 
hinbekommst, ist die Wartbarkeit nicht nerauschend.

> Die Frage lautet also: wie kann man mit einem typedef [in c!] den
> typcast (TimeoutHandler) in der ISR umgehen?

Genauso wie in C++ auch: Alles in einen Struct packen und es werden 
keine Casts mehr benötigt, hier in C99:
1
typedef struct F
2
{
3
    struct F (*f) (void);
4
} F;
5
6
extern F func2 (void);
7
extern int i;
8
9
F func1 (void)
10
{
11
    switch (i)
12
    {
13
        case 1: return func1().f();
14
        case 2: return func1().f().f();
15
        case 3: return func2().f().f().f();
16
    }
17
    return (F) { func1 };
18
}

von Rufus Τ. F. (rufus) Benutzerseite


Lesenswert?

Johann L. schrieb:
> return (F) { func1 };

Das ... kommt mir komisch vor.

von Daniel A. (daniel-a)


Lesenswert?

Rufus Τ. F. schrieb:
> Johann L. schrieb:
>> return (F) { func1 };
>
> Das ... kommt mir komisch vor.

Keine sorge, das ist ein ganz normales Compound literal, die Dinger sind 
echt Praktisch. Man könnte das ganze natürlich auch Ausschreiben, aber 
das ist nun-mal einer der Vorteile von C gegenüber C++, welcher ich auch 
ganz gerne Verwende.

von Uhu U. (uhu)


Lesenswert?

Johann L. schrieb:
> Uhu U. schrieb:
>> Sinn der Übung:
>
> fraglich.  Wenn du zu so seltsamen Konstrukten geführt wirst, stimmt
> vermutlich was am Design nicht.  Auch wenn du es funktionierend
> hinbekommst, ist die Wartbarkeit nicht nerauschend.

Wenn sich das auf deinen Beispielcode bezieht: ja, das ist wirklich 
fraglich, weil einfach nur vergurkt.

Meine Anwendung ist in der ISR in 
Beitrag "Re: Vertracktes typedef für Funktionspointer" skizziert.

Fazit: der typecast ist das kleinere Übel...

von Yalu X. (yalu) (Moderator)


Lesenswert?

Uhu U. schrieb:
> Johann L. schrieb:
>> Uhu U. schrieb:
>>> Sinn der Übung:
>>
>> fraglich.  Wenn du zu so seltsamen Konstrukten geführt wirst, stimmt
>> vermutlich was am Design nicht.  Auch wenn du es funktionierend
>> hinbekommst, ist die Wartbarkeit nicht nerauschend.
>
> Wenn sich das auf deinen Beispielcode bezieht: ja, das ist wirklich
> fraglich, weil einfach nur vergurkt.

Das "Problem" (falls man hier überhaupt von einem Problem sprechen
kann), liegt darin, dass C keine rekursiven Typsynonyme (aka Typedefs)
zulässt. Deswegen gelingt die Definition rekursiver Datentypen nur über
einen benamsten (Hilfs-)Typ. In C sind die einzigen benamsten Typen
Structs und Unions. Deswegen ist der Weg, den Daniel und Johann
vorgeschlagen haben, nämlich den Funktionszeiger in einem Struct zu
verpacken, nicht vergurkt, sondern die einzige saubere Lösung.

Wenn, dann würde ich eher deine Casterei als vergurkt bezeichnen, zumal
du dabei auch noch unnötigerweise zwischen Daten- und Funktionszeigern
hin- und herkonvertierst, was zwar meist funktioniert, aber durch den
ISO-Standard nicht abgedeckt ist.

Da rekursive Datentypen normalerweise sowieso nur im Zusammenhang mit
Structs auftreten, würde es sich nicht lohnen, für die wenigen
Ausnahmefälle spezielle Sprachfeatures einzuführen. Das gilt nicht nur
für C, sondern für praktisch alle Programmiersprachen. Oder kennt von
euch jemand eine Sprache mit statischer Typisierung, in der der von Uhu
gewünschte Funktionstyp definiert werden kann?

Deswegen ist es auch in anderen Sprachen in solchen und ähnlichen Fällen
üblich, den Datenwert (in diesem Fall den Funktionszeiger) einfach in
ein benamstes Päckchen zu verpacken. Das erfordert nur unwesentlich mehr
Schreibaufwand und verursacht i.Allg. überhaupt keine Effizienzeinbußen.
Im Gegensatz zur Cast-Methode bleibt dabei aber die Typischerheit voll
erhalten.

von Peter D. (peda)


Lesenswert?

Uhu U. schrieb:
> Sinn der Übung: die Funktion, die an ScheduleTimer2 übergeben wird, soll
> so lange alle 100 Ticks aufgerufen werden, wie sie einen Pointer auf
> sich selbst zurück gibt; beim Rückgabewert NULL soll beendet werden.

Warum so kompliziert, gib einfach TRUE oder FALSE zurück.
1
      if (timeoutHandler && !timeoutHandler())
2
         timeoutHandler = NULL;

von (prx) A. K. (prx)


Lesenswert?

Yalu X. schrieb:
> Da rekursive Datentypen normalerweise sowieso nur im Zusammenhang mit
> Structs auftreten,

Eher selbstbezüglich als rekursiv.

Das Problem hängt auch mit der vergurkten Syntax zu tun. Typedefs wurden 
nachträglich regelrecht in die Sprache reingewürgt und da der Name des 
Typs nicht das erste sondern das letzte syntaktische Element ist, kann 
er vorher eben auch nicht bekannt sein. Müsste er aber, weil Typnamen in 
C dummerweise eine Rolle ähnlich der von Keywords haben.

: Bearbeitet durch User
von (prx) A. K. (prx)


Lesenswert?

> euch jemand eine Sprache mit statischer Typisierung, in der der von Uhu
> gewünschte Funktionstyp definiert werden kann?

Ohne auf konkrete Sprachen einzugehen: Bei so etwas wie
  type T: pointer to F;                 //(1)
  type F: function (p: T) returning T;  //(2)
wäre (1) eine unvollständige Typdeklaration sowohl von T als auch von F, 
die beide erst mit (2) vollständig werden. Dass F in (1) noch nicht 
bekannt ist wäre an dieser Stelle kein Problem, weil anhand der 
syntaktischem Position als (Typ)Name erkennbar.

In C gibts eine analoge Konstruktion nur bei "struct S" (sowie union).

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


Lesenswert?

A. K. schrieb:
> Yalu X. schrieb:
>> Da rekursive Datentypen normalerweise sowieso nur im Zusammenhang mit
>> Structs auftreten,
>
> Eher selbstbezüglich als rekursiv.

Hast recht, zu Datentypen passt "selbstbezüglich" besser, auch wenn die
beiden Begriffe oft synonym verwendet werden.

> Das Problem hängt auch mit der vergurkten Syntax zu tun. Typedefs wurden
> nachträglich regelrecht in die Sprache reingewürgt und da der Name des
> Typs nicht das erste sondern das letzte syntaktische Element ist, kann
> er vorher eben auch nicht bekannt sein.

Das spielt sicher auch eine Rolle. Ein wesentlicher Grund liegt aber
auch darin, dass selbstbezügliche Typsynonyme die Typprüfung durch den
Compiler arg erschweren. Deswegen sind sie bspw. auch in Pascal und
Haskell nicht zugelassen, obwohl bei beiden Sprachen der neue Typname
jeweils am Anfang der Deklaration steht:

1
type NeuerTypName = TypAusdruck

: Bearbeitet durch Moderator
von Uhu U. (uhu)


Lesenswert?

Yalu X. schrieb:
> Deswegen ist der Weg, den Daniel und Johann
> vorgeschlagen haben, nämlich den Funktionszeiger in einem Struct zu
> verpacken, nicht vergurkt, sondern die einzige saubere Lösung.

Das habe ich auch nicht behauptet. Vergurkt ist Johanns 
Anwendungsbeispiel, aus dem er gleich großzügig ein Designproblem für 
die ganze Geschichte herbeizaubert.

> Wenn, dann würde ich eher deine Casterei als vergurkt bezeichnen
...
> Da rekursive Datentypen normalerweise sowieso nur im Zusammenhang mit
> Structs auftreten, würde es sich nicht lohnen, für die wenigen
> Ausnahmefälle spezielle Sprachfeatures einzuführen.

Na ja...

von Uhu U. (uhu)


Lesenswert?

Peter D. schrieb:
> Warum so kompliziert, gib einfach TRUE oder FALSE zurück.
>      if (timeoutHandler && !timeoutHandler())
>          timeoutHandler = NULL;

Dat Dingens kann ja nu noch ein bischen mehr, als nur NULL zurückgeben. 
Denkbar wäre ja auch die Adresse einer Folgefunktion, die den nächsten 
Teiljob erledigt usw...

von Mark B. (markbrandis)


Lesenswert?

Ich denke, dass dieser Thread ein klassisches Beispiel ist für:

Beschreibe nicht Deinen Lösungsansatz. Sondern beschreibe das zu lösende 
Problem.

:-(

: Bearbeitet durch User
von Frank M. (ukw) (Moderator) Benutzerseite


Lesenswert?

Uhu U. schrieb:
> Sinn der Übung: die Funktion, die an ScheduleTimer2 übergeben wird, soll
> so lange alle 100 Ticks aufgerufen werden, wie sie einen Pointer auf
> sich selbst zurück gibt; beim Rückgabewert NULL soll beendet werden.

Das halte ich für "von hinten durch die Brust ins Auge".

Ich formuliere Deine Aufgabenstellung einfach mal um:

Sinn der Übung: die Funktion, die an ScheduleTimer2 übergeben wird, soll
so lange alle 100 Ticks aufgerufen werden, wie sie 1 zurückgibt; beim 
Rückgabewert 0 soll beendet werden.

Fertig. Warum einen Funktionspointer auf sich selbst zurückgeben? Der 
aufrufenden Funktion ist der Funktionspointer doch längst bekannt!?!

von (prx) A. K. (prx)


Lesenswert?

Frank M. schrieb:
> Fertig. Warum einen Funktionspointer auf sich selbst zurückgeben? Der
> aufrufenden Funktion ist der Funktionspointer doch längst bekannt!?!

Vielleicht steht dahinter eine verkettete Liste. Und die Funktion gibt 
den nächsten Eintrag zurück.

Nur könnte man die Liste natürlich auch ausserhalb des Funktionsaufrufs 
abklappern.

: Bearbeitet durch User
von Frank M. (ukw) (Moderator) Benutzerseite


Lesenswert?

A. K. schrieb:
> Vielleicht steht dahinter eine verkettete Liste. Und die Funktion gibt
> den nächsten Eintrag zurück.

Ja, okay. Ursprünglich schrieb Uhu aber, dass die Funkton einen Pointer 
auf sich selbst zurückliefern würde. Erst heute vormittag erwähnt er, 
dass auch "die Adresse einer Folgefunktion" denkbar wäre. Denkbar, aber 
bisher nicht gefordert.

> Nur könnte man die Liste natürlich auch ausserhalb des Funktionsaufrufs
> abklappern.

Eben. Warum soll eine Teilfunktion (als kleines Rädchen im Uhrwerk) 
wissen sollen, welche Folgefunktion als nächstes kommt? Die Koordination

            Teil1 -> Teil2 -> Teil3 ... -> TeilN

sollte der aufrufenden Funktion überlassen sein. Dann reicht als 
Rückgabewert 0 oder 1:

            1: Mach weiter
            0: Stop!

: Bearbeitet durch Moderator
von Micha (Gast)


Lesenswert?

Uhu U. schrieb:
> Sinn der Übung: die Funktion, die an ScheduleTimer2 übergeben wird, soll
> so lange alle 100 Ticks aufgerufen werden, wie sie einen Pointer auf
> sich selbst zurück gibt; beim Rückgabewert NULL soll beendet werden.

Wieso übergibst du nicht die Variable mit der Callback-Adresse 
by-Reference an die Funktion? Innerhalb der Funktion kannst sie dann 
ggf. auf NULL setzen.

von Uhu U. (uhu)


Lesenswert?

Jetzt driftet ihr ab. Es steht nicht zur Debatte, wie ich mein Programm 
strukturiere.

Der Thread kann geschlossen werden.

: Bearbeitet durch User
von Frank M. (ukw) (Moderator) Benutzerseite


Lesenswert?

Uhu U. schrieb:
> Jetzt driftet ihr ab. Es steht nicht zur Debatte, wie ich mein Programm
> strukturiere.

Frage:

  Wie bekomme ich mit einem Kreuzschraubendreher eine Mutter
  festgezogen?

Antwort:

  Nimm einen Schraubenschlüssel!

Reaktion:

  Jetzt driftet Ihr ab, es steht nicht zur Debatte, welches Werkzeug
  ich wähle.

: Bearbeitet durch Moderator
von Mark B. (markbrandis)


Lesenswert?

Uhu U. schrieb:
> Es steht nicht zur Debatte, wie ich mein Programm strukturiere.

Das muss auch zur Debatte stehen können, wenn man eine vernünftige 
Lösung finden will.

von Uhu U. (uhu)


Lesenswert?

Mark B. schrieb:
> Das muss auch zur Debatte stehen können, wenn man eine vernünftige
> Lösung finden will.

Jetzt lass mal die Kirche im Dorf. Der Cast in der Timer-ISR ist ein 
Schönheitsfehler, mehr nicht.

von A. H. (ah8)


Lesenswert?

Johann L. schrieb:
> Wenn du zu so seltsamen Konstrukten geführt wirst, stimmt
> vermutlich was am Design nicht.  Auch wenn du es funktionierend
> hinbekommst, ist die Wartbarkeit nicht nerauschend.

So seltsam finde ich das Konstrukt gar nicht, ganz im Gegenteil. Es ist 
meine bevorzugte Variante, einen ereignisgesteuerten (event driven) 
deterministischen endlichen Automaten (DFA: deterministic finite 
automaton, gerne auch FSM: finite state machine) zu implementieren:

Jeder Zustand des DFA wird durch eine Funktion repräsentiert, die das 
nächste Eingabesymbol als Parameter erhält, die entsprechend des daraus 
resultierenden Zustandsüberganges notwendigen Aktionen ausführt und den 
Folgezustand als Resultat zurück gibt. Diese wird dann in einer 
Zustandsvariable bis zum Eintreffen des nächsten Eingabesymbols 
gespeichert. Die Callback-Funktion wird dadurch extrem einfach:

1
void onInput(Input input) {
2
  state = state(input);
3
  if (! state )
4
    terminateDFA();
5
}

Oder gerne auch frei laufend:

1
while (state) {
2
  state = state(getInput());
3
}

Das ist deutlich flexibler, sicherer und letztlich auch effizienter als 
jede andere mir bekannte Methode.

Genau das hat der TO hier auch implementiert, allerdings hat sein DFA 
(neben dem trivialen terminalen) nur einen einzigen nichttrivialen 
Zustand. Insofern ist die Frage natürlich berechtigt, ob die Komplexität 
des Problems einen solchen Aufwand rechtfertigt. Elegant finde ich den 
Ansatz aber allemal.

von Uhu U. (uhu)


Lesenswert?

A. H. schrieb:
> Genau das hat der TO hier auch implementiert, allerdings hat sein DFA
> (neben dem trivialen terminalen) nur einen einzigen nichttrivialen
> Zustand. Insofern ist die Frage natürlich berechtigt, ob die Komplexität
> des Problems einen solchen Aufwand rechtfertigt.

Um das Prinzip zu demonstrieren, reicht das doch völlig aus. Da stellt 
sich die Frage nach der Berechtigtheit des Aufwandes nicht.

> Elegant finde ich den Ansatz aber allemal.

Nicht nur das. Man bekommt auch den Scheduler vom Anwendungscode sauber 
getrennt und kann ihn unverändert weiterverwenden.

: Bearbeitet durch User
von Micha (Gast)


Lesenswert?

A. H. schrieb:
> void onInput(Input input) {
>   state = state(input);
>   if (! state )
>     terminateDFA();
> }
>
> Oder gerne auch frei laufend:
>
> while (state) {
>   state = state(getInput());
> }

Kannst du dafür auch noch Kontext (vor allem Deklarationen) liefern? Mir 
ist leider noch nicht ganz klar wie das im Ganzen aussieht.

von Arc N. (arc)


Lesenswert?

Micha schrieb:
> Kannst du dafür auch noch Kontext (vor allem Deklarationen) liefern? Mir
> ist leider noch nicht ganz klar wie das im Ganzen aussieht.

Zwar C++, Lösung ist dieselbe wie die weiter oben im Thread mit typedef 
struct
http://www.gotw.ca/gotw/057.htm

von A. H. (ah8)


Lesenswert?

Micha schrieb:
> A. H. schrieb:
>> void onInput(Input input) {
>>   state = state(input);
>>   if (! state )
>>     terminateDFA();
>> }
>>
>> Oder gerne auch frei laufend:
>>
>> while (state) {
>>   state = state(getInput());
>> }
>
> Kannst du dafür auch noch Kontext (vor allem Deklarationen) liefern? Mir
> ist leider noch nicht ganz klar wie das im Ganzen aussieht.

Das haben Daniel und Johann oben schon wunderbar gemacht. Dem ist 
eigentlich nichts hinzu zu fügen, außer vielleicht, dass ich bei dieser 
Interpretation entsprechend angepasste Namen verwenden würde. Also so 
etwa (ungetestet):

1
typedef int Input;
2
typedef struct State {
3
    struct State (*next) (Input);
4
} State;
5
6
State state_0 (Input);
7
State state_1 (Input);
8
State state_2 (Input);
9
... // more state deklarations
10
11
State state_0 (Input i)
12
{
13
    switch (i)
14
    {
15
        case 1: /* do somthing */ return (State) { state_0 } ;
16
        case 2: /* do somthing */ return (State) { 0 } ; // terminate
17
        case 3: /* do somthing */ return (State) { state_2 } ;
18
    }
19
    return (State) { state_1 };
20
}
21
22
... // more state definitions
23
24
25
State state = (State) { state_0 }:
26
27
void onInput(Input input) {
28
    state = state.next(input);
29
    if ( ! state.next )
30
        terminateDFA();
31
}
32
33
while ( state.next ) {
34
    state = state.next(getInput());
35
}

PS: Ach so, mein ursprünglicher Code war als Pseudocode gedacht der 
funktionieren würde, wenn C rekursive Typdeklarationen der Form
1
typedef State (*State) (Input);
erlauben würde. Sorry, falls das zu Missverständnissen geführt hat.

von Tom (Gast)


Angehängte Dateien:

Lesenswert?

Vielen Dank für diesen Thread, besonders für die Idee mit den 
Zustandsautomaten. Um das Konzept zu verstehen, habe ich ein 
Minibeispiel gebaut und in linereader.c weitergesponnen, bis in den 
Zuständen nur Flusskontrolle passiert und die Aktionen in den Übergängen 
geschehen, wobei die Übergänge ebenfalls Funktionen sind, die zum neuen 
Zustand führen.

Nett finde ich dabei die kurzen, primitiven Funktionen und die 
(scheinbare) 1:1-Beziehung zur grafischen Darstellung. Unschön ist die 
wacklige Abstraktion und der höhere Aufwand, selbst wenn man wie hier 
pragmatische Abkürzungen nimmt. Ob das ganze elegant oder albern ist, 
ist mir noch nicht ganz klar.

Der Ansatz ohne die Übergangs-Funktionen ist der Vollständigkeit halber 
in linereader2.c, ist aber insgesamt nicht viel kürzer.

von A. H. (ah8)


Lesenswert?

Tom schrieb:
> Vielen Dank für diesen Thread, besonders für die Idee mit den
> Zustandsautomaten. Um das Konzept zu verstehen, habe ich ein
> Minibeispiel gebaut und in linereader.c weitergesponnen, bis in den
> Zuständen nur Flusskontrolle passiert und die Aktionen in den Übergängen
> geschehen, wobei die Übergänge ebenfalls Funktionen sind, die zum neuen
> Zustand führen.

Schön, dass wir Dich begeistern konnten. Aber bitte nicht der Versuchung 
erliegen, eine neue Programmiertechnik einzusetzen, nur weil sie cool 
ist (oder zumindest so erscheint). Am Anfang einer solchen 
Implementierung steht immer der DFA. Der ist das Ergebnis eines eigenen 
Entwicklungsprozesses, der auch gut dokumentiert sein sollte, denn er 
ist wichtig für das Verständnis des Codes.

Ist der DFA vorhanden wird er einfach 1:1 in Code übertragen. (Das kann 
unter Umständen sogar automatisch geschehen!) In der Regel sollte ein 
DFA so wenige Zustände haben wie möglich. Es ist meist keine gute Idee, 
zusätzliche Zustände einzuführen, nur weil das ein paar Zeilen Code 
spart. Erfahrungsgemäß kommt es immer wieder mal zu Code-Teilen, die 
sich in mehreren – gerne auch mal in den meisten – Zuständen eines DFA 
wiederholen. Dann diese lieber in eine ganz normale Funktion 
ausgliedern, die innerhalb einer Zustandsfunktion aufgerufen wird. Das 
ist meist deutlich übersichtlicher.

Eine Funktion, die kein Eingabesymbol erhält, kann keine Transition 
sein, weil ein Zustandsübergang immer das Ereignis eines eintreffenden 
Zeichens voraussetzt. (Das Zeichen kann ggf. auch das leere Zeichen 
sein, meist als Epsilon bezeichnet.) Die Funktionen, die Du in Deinem 
Code als Transitions bezeichnest, sind letzten Endes ganz normale 
Funktionen im obigen Sinn. (Ob sie in Deinem Fall wirklich etwas 
bringen, sei einmal dahin gestellt.) Als solche ist es aber auch nicht 
wirklich sinnvoll, den Folgezustand in ihnen festzulegen, den dieser ist 
Teil des Modells des DFA, nicht des Teilproblems, was Du in der Funktion 
löst.

Es gibt auch keinen Grund, zwischen Zustand und Transition zu 
unterscheiden. Eine Zustandsfunktion ist beides: sowohl ein Zustand als 
auch die Menge aller aus diesem Zustand heraus führenden Transitions. 
(Genauer müsste man wohl formulieren, dass der Pointer auf die Funktion 
der Zustand ist, die Funktion selber die Menge der Transitions.)

Zustände, die nur einen Folgezustand haben, machen in der Regel wenig 
Sinn, es sei denn, Zustände werden genutzt, um etwas zu zählen. Das kann 
im Einzelfall sinnvoll sein, meist ist aber eine explizite Zählvariable 
die bessere Alternative.

Alles in allem gefällt mir die zweite Version Deiner Implementierung 
(linereader2.c) deutlich besser.

Übrigens, Variablen, die innerhalb des DFA gebraucht werden, müssen 
nicht global sein. Meist ist es übersichtlicher, sie in einer Struktur 
zusammenzufassen und diese den Zustandsfunktionen als Referenz mit zu 
übergeben. Genau genommen könnte sogar die Zustandsvariable (also der 
Pointer auf den Folgezustand) Teil dieser Struktur sein. Der 
Zustandsübergang wäre dann eine einfache Zuweisung innerhalb der 
Zustandsübergangsfunktion. Das finde ich persönlich aber weniger 
elegant, das es das Grundprinzip des DFA weniger offensichtlich macht. 
Besser finde ich, ggfs. die Zustandsvariable gemeinsam mit der 
Parameterstruktur in einer eigenen übergeordneten Struktur 
zusammenzufassen. Diese kann dann je nach Bedarf global, lokal oder auch 
dynamisch angelegt werden. Bei mehr als zwei gleichzeitig laufenden DFAs 
ist das sogar zwingend erforderlich.

von W.S. (Gast)


Lesenswert?

Uhu U. schrieb:
> Sinn der Übung: die Funktion, die an ScheduleTimer2 übergeben wird, soll
> so lange alle 100 Ticks aufgerufen werden, wie sie einen Pointer auf
> sich selbst zurück gibt; beim Rückgabewert NULL soll beendet werden.

Und dafür läßt du die Köpfe aller Anwesenden derart rauchen?

Also, ich repetiere das mal:
Du brauchst eine Funktion, die was Anderes als NULL zurückgibt, wenn sie 
nach 100 Ticks wieder aufgerufen werden soll?

Warum muß das unbedingt ein Zeiger auf sich selbst sein? Ein schlichtes 
boolean würde den Zweck der Entscheidung doch auch erfüllen. Oder ist 
das mit dem zurückgegebenen  Funktionspointer ein besonderer Kick für 
dich?

Noch einfacher wäre es, mit verzögerten oder geplanten Events zu 
arbeiten. Dann hat der Systemtick die alleinige Arbeit und man kommt 
ohne jegliche C-Akrobatik aus.

W.S.

von Uhu U. (uhu)


Lesenswert?

> Also, ich repetiere das mal:

Du kannst zwar schreiben, aber nicht lesen. Und auch im Spekulieren bist 
du eine absolute Null.

: Bearbeitet durch User
von Mark B. (markbrandis)


Lesenswert?

Uhu U. schrieb:
>> Also, ich repetiere das mal:
>
> Du kannst zwar schreiben, aber nicht lesen. Und auch im Spekulieren bist
> du eine absolute Null.

Wenn Du Hilfe von anderen Leuten ersuchst, solltest Du vielleicht mal 
verbal einen Gang zurück schalten.

von Uhu U. (uhu)


Lesenswert?

Mark B. schrieb:
> Wenn Du Hilfe von anderen Leuten ersuchst, solltest Du vielleicht mal
> verbal einen Gang zurück schalten.

Wer zu faul zum lesen ist, der kann auch keine vernünftigen Ratschläge 
geben. Insbesondere dann nicht, wenn die Frage bereits spätestens seit 
14.05.2016 16:47 geklärt ist...

: Bearbeitet durch User
von Peter D. (peda)


Lesenswert?

Uhu U. schrieb:
> Du kannst zwar schreiben, aber nicht lesen.

Nicht so grob bitte.
Du rückst ja nur mit mit mikroskopisch kleinen Häppchen mit wichtigen 
Einzelheiten heraus, bist also selber Schuld, wenn jemand etwas 
überliest.

Dein Problem ist schon ziemlich an den Haaren herbeigezogen. Ich finde 
eine Scheduler-Lösung viel besser und vor allem einfacher zu 
durchschauen.

von Uhu U. (uhu)


Lesenswert?

Peter D. schrieb:
> Du rückst ja nur mit mit mikroskopisch kleinen Häppchen mit wichtigen
> Einzelheiten heraus, bist also selber Schuld, wenn jemand etwas
> überliest.

Es handelt sich nicht um ein Implementierungsproblem - die kann ich 
durchaus schon selbst lösen - sondern um ein reines Sprachproblem mit 
C.

Im Eingangsposting steht alles über das Problem. Man muss nur lesen.

Wozu ich das letzten Endes benutze, ist völlig irrelevant. Das zu 
erfahren, befriedigt natürlich die Neugier - das geht auch in Ordnung. 
Nur Ratschläge in der Art: "Das ist alles Unsinn, mach das so und so..." 
sind nicht nur nutzlos und ärgerlich, sie signalisieren auch, dass der 
Ratgeber die Frage nicht im Entfernten verstanden hat.

> Dein Problem ist schon ziemlich an den Haaren herbeigezogen.

Das kann man so sehen, muss man aber nicht. Ich will einen 
wiederverwendbaren Scheduler, der völlig unabhängig von der jeweiligen 
Anwendung in eine Lib verpackt wird und dann nur noch eingebunden und 
aufgerufen wird. Um sowas sauber handhabbar zu machen, sollte man sich 
schon etwas genauer mit der Problematik auseinandersetzen - das habe ich 
gemacht.

> Ich finde eine Scheduler-Lösung viel besser und vor allem einfacher zu
> durchschauen.

Ich weiß nicht, was du unter einer "Scheduler-Lösung" verstehst. Ich 
finde, eine gut durchdachter Bibliotheksmodul ist allemal besser, als 
Einwegcode, den man jedesmal neu hinschreibt.

: Bearbeitet durch User
von Peter D. (peda)


Lesenswert?

Uhu U. schrieb:
> Ich weiß nicht, was du unter einer "Scheduler-Lösung" verstehst.

Z.B. diese Lib:
Beitrag "Wartezeiten effektiv (Scheduler)"

Ich nehme sie sogar für einfache Blink-LEDs.
Oder für Timeouts von beliebigen Protokollen.

von A. H. (ah8)


Lesenswert?

Peter D. schrieb:
> Dein Problem ist schon ziemlich an den Haaren herbeigezogen. Ich finde
> eine Scheduler-Lösung viel besser und vor allem einfacher zu
> durchschauen.

Ein Scheduler und ein DFA sind zwei unterschiedliche Konzepte, sie 
können sich aber sehr gut ergänzen.

Ein DFA schaltet immer auf Grund eines externen Ereignisses.(*) Das kann 
z.B. ein Interrupt sein, wenn ein neues Eingabesymbol vorliegt, aber 
auch der Ablauf einer bestimmten Zeit. (Das Eingabesymbol ist dann in 
der Regel Epsilon.) Um das Schalten nach einer bestimmten Zeit zu 
erreichen kann durchaus ein entsprechendes Ereignis im Scheduler 
eingestellt werden.

Ein Scheduler, auf der anderen Seite, sorgt dafür, dass zu einer 
bestimmten Zeit ein bestimmtes Ereignis getriggert wird. Das kann, muss 
aber nicht, auch das Weiterschalten eines DFA sein.

(*) Natürlich werden DFAs auch zu anderen Zwecken wie der Syntaxanalyse 
eingesetzt. Dann müssen sie in der Regel aber nicht ereignisgesteuert 
sein und es würden sich andere Implementierungen anbieten.

: Bearbeitet durch User
von Uhu U. (uhu)


Lesenswert?

Die Lösung mit dem Funktionspointer als Rückgabewert ist wohl deutlich 
kompakter, als Peters Modell: es fallen eine Menge Funktionsaufrufe in 
den Zustandsfunktionen ("handler") weg.

Zudem läßt sich das Interface sehr schlank gestalten, wenn man den 
Tick-Wert 0 für eine Zustandsfunktion als "lösche den Eintrag" 
interpretiert - was durchaus auf der Hand liegt.

Auch muss der Timeout für eine Kette von Zuständen bei vielen 
Anwendungen nicht jedesmal geändert werden - das spricht - neben der 
Eleganz - für die Lösung mit dem Funktionspointer als return-Wert. Das 
reduziert auch die Größe der Scheduler-Tabelle.

von Peter D. (peda)


Lesenswert?

Uhu U. schrieb:
> Die Lösung mit dem Funktionspointer als Rückgabewert ist wohl deutlich
> kompakter, als Peters Modell

Ich finde es im Gegenteil einfacher, wenn man für das Eintragen und 
Löschen eine timed Calls zentrale Lib-Funktionen hat, als wenn sich 
jeder selber um solchen Schrunz kümmern muß.
Auch kann man solche Funktionsaufrufe besser nachverfolgen, d.h. den 
Code einfacher verstehen.
Ich hab da keine untere Grenze, selbst einfache Funktionen (Einzeiler) 
zu modularisieren, wenn es sinnvoll ist.

von A. H. (ah8)


Lesenswert?

Vielleicht hätte ich mich oben etwas präziser ausdrücken sollen: Der 
Scheduler und der DFA sind zwei orthogonale Konzepte, die völlig 
unterschiedliche Probleme lösen. Die Diskussion, welches Konzept besser 
ist, ist müßig, weil es vom jeweiligen Problem abhängt. Das vom TO 
gepostete Problem ist dazu erst recht nicht geeignet, das es so trivial 
ist, dass es in der Praxis weder das eine, noch das andere benötigt. (Es 
sollte ja aber wohl auch nur als theoretisches Beispiel dienen.)

von Uhu U. (uhu)


Lesenswert?

Peter D. schrieb:
> Ich finde es im Gegenteil einfacher, wenn man für das Eintragen und
> Löschen eine timed Calls zentrale Lib-Funktionen hat, als wenn sich
> jeder selber um solchen Schrunz kümmern muß.

Na rat mal, warum ich mir dafür eine Lib schreibe...

von Michi (Gast)


Lesenswert?

Uhu U. schrieb:
> Na rat mal, warum ich mir dafür eine Lib schreibe...

Würdest du das Resultat dann hier vorstellen?

von Uhu U. (uhu)


Lesenswert?

A. H. schrieb:
> Der Scheduler und der DFA sind zwei orthogonale Konzepte, die völlig
> unterschiedliche Probleme lösen.

Das kommt auf den Abstraktionsgrad an, unter dem man das Ganze 
betrachtet - deswegen würde ich nicht sagen, die Konzepte seien 
orthogonal.

Beispiel: Rampe für einen PID-Regler abfahren. Der Prozess wird von 
außen durch eine Änderung des Sollwertes angestoßen - das ist eine 
Eingabe - und läuft dann timergesteuert ab, sozusagen als Echo auf die 
Eingabe.

von Uhu U. (uhu)


Lesenswert?

Michi schrieb:
> Würdest du das Resultat dann hier vorstellen?

Ja.

von A. H. (ah8)


Lesenswert?

Uhu U. schrieb:
> A. H. schrieb:
>> Der Scheduler und der DFA sind zwei orthogonale Konzepte, die völlig
>> unterschiedliche Probleme lösen.
>
> Das kommt auf den Abstraktionsgrad an, unter dem man das Ganze
> betrachtet - deswegen würde ich nicht sagen, die Konzepte seien
> orthogonal.

Das sehe ich nicht so. Egal auf welcher Abstraktionsebene ich mich 
bewege, auf ein und derselben Ebene sind Scheduler und DFA immer 
orthogonal zueinander.

Ein DFA schaltet immer auf Grund eines externen Ereignisses. Wo das 
Ereignis her kommt ist ihm egal, er kann es aber nicht selber erzeugen, 
denn ein DFA wechselt nie spontan seinen Zustand (zumindest nach der mir 
bekannten Definition des Models).

Ein Scheduler dagegen tut genau das, er erzeugt Ereignisse, mit denen er 
dann Aktionen in Form von Callback-Funktionen triggert. Da er genau das 
tut, was ein DFA nicht kann, kann er weder Teil des DFA sein noch ihn 
ersetzen.

Auf der anderen Seite ist weder der Scheduler auf den DFA als 
Ereignisempfänger, noch der DFA auf einen Scheduler als Ereignisquelle 
beschränkt. Es gibt immer alternative Quellen bzw. Senken. Beide 
Konzepte können also nicht die Funktion des jeweils anderen übernehmen, 
sind aber frei miteinander kombinierbar und also in diesem Sinne 
orthogonal zueinander.

Natürlich hat auch der Scheduler einen Zustand, der die aktuelle Zeit 
repräsentiert. Man kann also auch die Implementierung des Schedulers als 
DFA betrachten und das Voranschreiten der Zeit als Zustandsübergang. 
Dann befinde ich mich aber auf einer anderen, eigenen Abstraktionsebene. 
Auch auf dieser Ebene wird der DFA seinen Zustand nicht spontan ändern, 
sondern ist auf eine externe Ereignisquelle angewiesen, in diesem Fall 
einen Timer-Interrupt.

> Beispiel: Rampe für einen PID-Regler abfahren. Der Prozess wird von
> außen durch eine Änderung des Sollwertes angestoßen - das ist eine
> Eingabe - und läuft dann timergesteuert ab, sozusagen als Echo auf die
> Eingabe.

In diesem Fall könnte man einen DFA mit zwei Zuständen konstruieren: Der 
Initiale Zustand prüft, ob das Ereignis vom Timer oder von der Änderung 
des Sollwertes herrührt. Im ersten Fall verharrt er in seinem Zustand, 
im zweiten Fall geht er in den Ausgabezustand über. Hier zählt er bei 
jedem Timer-Tick eine Index weiter und gibt den entsprechenden 
Rampenwert aus. Nach n Ausgaben kehrt er in den Initialzustand zurück. 
(Mathematisch betrachtet handelt es sich natürlich um einen DFA mit n+1 
Zuständen, n Ausgabezustände plus einen Initialzustand. Aber niemand 
würde dafür n Zustandsfunktionen implementieren.) Wie ein Ausgabezustand 
auf eine weitere Änderung des Sollwertes regiert, bliebe noch zu 
definieren.

Wenn dabei ein Scheduler im Spiel ist – ein Timer-Interrupt ist genauso 
vorstellbar – dann übernimmt der die Rolle des Timers. Dann würde der 
DFA bei jedem Übergang in den Ausgabezustand – auch von sich selbst aus 
– das nächste Timer-Event registrieren. Anders als beim mathematischen 
Modell, das bei jedem Zustandsübergang immer auch ein – möglicherweise 
leeres – Ausgabesymbol ausgibt, ist es in der Praxis oft vorteilhafter, 
ggfs. eine Aktion auszuführen. Diese Aktion kann man als Teil der 
Ausgabefunktion betrachten. Das Registrieren eines neuen Timer-Events 
wäre eine solche Aktion. In jedem Fall aber bleiben Ereignisquelle und 
DFA sauber getrennt.

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.