Guten Tag.
Kann man in C oder C++ einen "allgemeinen Funktionspointer" erzeugen?
Der Pointer soll flexibel sein. Mit flexibel meine ich, dass die Anzahl
der Funktionsparameter egal sein soll (es geht nur darum, die Adressen
bestimmter Funktionen zu speichern).
Die von mir verwendete Variante
1
typedefbyte(*tPtr1)(byte)
kann das nicht und zickt, wenn die Funktion nicht genau einen Parameter
hat.
Felix U. schrieb:> Um die Adresse zu speichern nimmst du void*
Das knallt gerne mal auf ARM Cortex-M Plattformen, da dort die Addresse
des Funktionsaufrufs != Funktions Adresse ist (wegen dem Thumb Bit).
Jim M. schrieb:> Felix U. schrieb:>> Um die Adresse zu speichern nimmst du void*>> Das knallt gerne mal auf ARM Cortex-M Plattformen, da dort die Addresse> des Funktionsaufrufs != Funktions Adresse ist (wegen dem Thumb Bit).
Wie das? Einen void* kann man nicht "aufrufen". Man muss den im void*
gespeicherten Wert erst wieder in den korrekten Funktionspointertyp
konvertieren.
mh schrieb:> Wie das? Einen void* kann man nicht "aufrufen". Man muss den im void*> gespeicherten Wert erst wieder in den korrekten Funktionspointertyp> konvertieren.
Ja genau. Meine Idee war, erst beim "Aufruf" des Pointers die jeweiligen
Parameter zuzufügen, die die aufgerufene Funktion braucht. Ich wußte
nicht, ob ich dem Compiler damit zuviel zumute.
Mit variadic templates geht das schon, wie oben bereits erwähnt. Wo du
die Argumentliste herbekommst, ist hingegen ein anderes Problem.
std::apply könnte aber hilfreich sein.
Du kannst eine Union nehmen, mit allen Variationen, die du brauchst.
Oder meinst du genau eine variadic-funktion?
Dem Compiler ist es übrigens prinzipiell beim Aufruf egal. Wenn kein
Prototyp existiert, versucht er es ja auch so gut er kann, kann aber
nicht immer richtig raten. Er kann z.b. nicht ahnen, dass der übergebe
float ein int werden muss.
Aber aufräumen z.b. tut er wieder das, was er angerichtet hat.
Felix U. schrieb:> Wenn du keinen Prototyp für die Funktion festlegen willst, ist es kein> Funktionspointer. Um die Adresse zu speichern nimmst du void*
void* ist für einen Funktionspointer eigentlich nicht geeignet.
Da wäre eher void (*)(void) geeignet. Vor dem Aufruf muss man sowieso in
den richtigen Typ casten.
mh schrieb:> Jim M. schrieb:>> Felix U. schrieb:>>> Um die Adresse zu speichern nimmst du void*>>>> Das knallt gerne mal auf ARM Cortex-M Plattformen, da dort die Addresse>> des Funktionsaufrufs != Funktions Adresse ist (wegen dem Thumb Bit).>> Wie das? Einen void* kann man nicht "aufrufen". Man muss den im void*> gespeicherten Wert erst wieder in den korrekten Funktionspointertyp> konvertieren.
Ein void* muss nicht unbedingt zu einem Funktionszeiger kompatibel sein.
Hier wird ausgenutzt, dass keine Parameter ein Spezialfall für eine
Funktion ohne Prototyp ist, also beliebig viele Parameter haben könnte.
Eine Funktion mit Prototyp, die tatsächlich keine Parameter hat,
bräuchte void als Parameter.
Vor der Verwendung sollte man den Pointer aber wieder richtig casten,
sonst kann man in die witzigsten Probleme laufen.
Oh, und natürlich geht der c Syntax unter c++ nicht und umgekehrt.
Rolf M. schrieb:> mh schrieb:>> ... blubber ...> Ein void* muss nicht unbedingt zu einem Funktionszeiger kompatibel sein.
Du kannst das "muss nicht unbedingt ... sein" durch ein "ist nicht ..."
ersetzen. Es ist vom Standard nicht erlaubt, einen Funktionspointer in
einen void* zu speichern.
Der Cast zwischen Funktionszeigern ist nach Standard nicht definiertes
Verhalten. Gesetzt den Fall, man kann ausschließen eine Funktion mit
falscher Signatur aufzurufen, kann eine union funktionieren, zum
Beispiel:
1
union FUNC_U
2
{
3
int (*f)(int x);
4
float (*g)(float x);
5
double (*h)(double x);
6
};
7
typedef union FUNC_T FUNC;
Je nach Anwendung kann man auch nur die abweichenden Argumente in einer
union platzieren und diese als Argument übergeben. Die Funktionen haben
so immer die gleiche Signatur. Dies setzt Kontrolle über die
aufzurufenden Funktionen voraus.
1
union PARA_U
2
{
3
int x;
4
float y;
5
double z;
6
};
7
struct PARA_ST
8
{
9
int type;
10
union PARA_U para;
11
};
12
typedef struct PARA_ST PARA;
13
14
// some common arguments x and y + variable argument
15
void f(int x, int y, PARA* arg);
Dieses Pattern ist dann interessant, wenn man z. B. const und
nicht-const Parameter hat (z. B. eine read und write Funktion per
Funktionszeiger). So ist z. B. "const int*" ein anderer Typ als "int*".
g457 schrieb:> Wozu soll das gut sein?
Spielerei. Timer-unterstützt will ich mir sowas bauen:
After <Zeit> call <Adresse>
bzw.
Every <Zeit> call <Adresse>.
Vor 100 J. hatte ich das mal in BASIC und war ganz nützlich für alles
mögliche.
Eure Beispiele funktionieren übrigens. Bin hellauf begeistert :-)
wie es aussieht, kann damit jede Funktion beliebiger Parameter bedient
werde. Jedenfalls die bei mir vorkommenden Funktionen haben alle
funktioniert. Ein echter Fortschrit. Bisher mussten alle Funktionen, die
per Pointer aufgerufen werden sollen, einem bestimmten Prototyp genügen.
Das ist jetzt nicht mehr nötig. Super.
Ähm ... so ganz geheuer ist mir das noch nicht. Muss ich mit
Überraschungen rechnen?
Jörg W. schrieb:> Ähm ... so ganz geheuer ist mir das noch nicht. Muss ich mit> Überraschungen rechnen?
Ja. Das ist Mist und hochgradig außerhalb des C Standards.
Es ist lang nicht das einzige Problem, aber in Bezug auf implizite
Typkonvertierungen unterscheidet sich das Verhalten. Nimm mal eine
Funktion mit einem double Argument. Jetzt definiere eine int Variable,
weise etwas sinnvolles zu und rufe damit die Funktion über deinen
Funktionszeigertrick mit variabler Argumentenliste auf. Damit der
Compiler es nicht "gesund-optimiert" am besten in getrennten Modulen
damit er den Umweg über den Zeiger beim Aufruf nehmen muss und den
Prototyp wirklich nicht kennt. Und, geht das immer noch?
Ein direkter Aufruf der Funktion mit einer int Variablen in ein double
Argument führt normalerweise zu einem impliziten Cast int->double. Bei
deiner Variante kennt der Compiler den wahren Prototyp aber nicht und
macht nur Standardkonvertierungen (z. B. integral promotion). Du hast
schon einen int gegeben, also ist für den Compiler alles in Ordnung und
er macht gar nichts. Die aufgerufene Funktion braucht aber ein double.
Peng. Bei ARM Cortex M4F zum Beispiel erfolgt die Übergabe von
float/double in FPU Registern (S0, ...) und die Übergabe von int in CPU
Registern (R0, ...). Die aufgerufene Funktion benutzt in diesem Fall
also einfach ein Register mit undefiniertem Inhalt und macht was schönes
draus.
x^2 schrieb:> Ein direkter Aufruf der Funktion mit einer int Variablen in ein double> Argument führt normalerweise zu einem impliziten Cast int->double.
Das habe ich nicht vor, es leuchtet aber ein, dass das Probleme geben
-könnte. Der Variablentyp meiner Funktionen ist bei jeder gleich (byte).
Mir ging es darum, mit der Anzahl der Parameter etwas flexibler zu
werden. Wobei dann wohlmöglich auch zu vermeiden ist, eine Funktion mit
mehr Parametern aufzurufen als sie hat.
Jörg W. schrieb:> Der Variablentyp meiner Funktionen ist bei jeder gleich (byte).> Mir ging es darum, mit der Anzahl der Parameter etwas flexibler zu> werden.
Wenn der Typ immer gleich ist, dann uebergib doch einfach ein Array und
die groesse des Array, bzw. die Anzahl der Parameter, wenn es weniger
Parameter sind als das Array gross ist.
Jörg W. schrieb:> Wobei dann wohlmöglich auch zu vermeiden ist, eine Funktion mit> mehr Parametern aufzurufen als sie hat.
Du koenntest dir ein struct bauen das den Funktionszeiger, ein Array
fuer die Parameter, und einen Zaehler fuer die Anzahl der Parameter
enthaelt.
Pseudocode:
1
typedefstruct{
2
function_pointer;
3
param_array[MAX_PARAM_COUNT];
4
actual_param_cnt;
5
}foo_t;
Dann musst du das nur noch sauber initialisieren. Dann hast du immer die
Funktion und die dazu gehoerenden Parameter zusammen.
Damit wuerde man zwar immer etwas Speicher verschwenden, aber solange
der Speicher reicht... fuer nicht benutzten Speicher gibt es kein Geld
zurueck.
In C++ wuerde man dafuer wohl eine Klasse schreiben, das ist aber nicht
mein Gebiet, da sind andere besser drin.
Es sollte also erstmal geklaert werden, welche Sprache du denn nun
konkret einsetzen willst:
C oder C++?
Im Anhang in der test.cc das vervollständigte Beispiel von eben. Da
wurde aber etwas getrickst - es ist nicht ein Funktionszeiger für
beliebige Funktionen die aber jeweils eine fixe Argumentzahl haben,
sondern die Funktionen haben nur 1 Argument, welches eine beliebige
Anzahl an Elementen enthalten kann. Das erfordert, dass man die
Funktionen anpassen kann.
Wenn man wirklich unbedingt einen Funktionszeiger haben möchte, welcher
auf verschiedene Funktionen, die unterschiedliche aber jeweils fixe
Argumentzahlen haben sollen, wird es kompliziert. Beim Aufruf muss man
ja wissen, wie viele Argumente die jeweils referenzierte Funktion
braucht, und muss diese in die Klammer schreiben, und die muss auch
passen. Tatsächlich braucht man also verschiedene Funktionszeiger-Typen
(einen für jede Argumentzahl), und muss diese in einen Variant-Typ
speichern, welcher jeweils einen der verschiedenen Zeiger ablegt. Beim
Aufruf muss man dann prüfen, um welchen es sich handelt, und die
richtige Anzahl an Parametern übergeben.
In C macht man das traditionell mit einer union, aber in C++ gibt es
dafür std::variant. Dafür ist im Anhang in der test2.cc ein Beispiel.
Dort werden erst 4 Typen für Funktionspointer für die Argumentzahlen 0-3
definiert. Mit VariantFPtr wird dann ein varianter Typ definiert,
welcher einen dieser Funktionszeiger aufnehmen kann. Diesem Variant wird
dann in der main()-Funktion jeweils eine Funktion zugewiesen. Um den
Pointer aufzurufen, wird std::visit verwendet, welches prüft welche Art
von Funktionszeiger jetzt tatsächlich vorhanden ist. Es wird die
entsprechende Funktion im Funktional "ApplyFPtr" aufgerufen, welches
dann die richtige Argument-Zahl übergibt.
Das ist natürlich total umständlich. Wenn's etwas generischer sein darf,
siehe test3.cc. Dort wird erst ein Typ-Alias FixedFPtr<N> für einen
Funktionspointer mit N Argumenten von Typ std::byte definiert.
VariantFPtr<N> ist dann ein varianter Typ für Funktionspointer mit 0-N
Argumenten. ApplyFPtr kann mit beliebigen FixedFPtr<N> umgehen und ruft
die Funktion mit den Werten 0...N-1 auf. Somit können hier Funktionen
mit beliebig vielen Argumenten verwendet werden, es muss nur das N groß
genug gewählt werden.
Das ist natürlich immer noch nicht wirklich schön. Der Knackpunkt ist
wie gesagt, dass man beim Aufruf die genaue Anzahl wissen muss. Da
führt kein Weg dran vorbei. Der Umweg über variadische Funktionszeiger
(mit ...) wie von Jim vorgeschlagen hat nur manchmal den Anschein zu
funktionieren, wenn die Funktion selbst variadisch ist. Am Beispiel von
ARM wird dies deutlich: Die ersten 4 Argumente werden in den Registern
r0-r3 übergeben, wenn sie in 32bit passen. Die Argumente variadischer
Funktionen (also mit "..." in der Argumentliste) werden aber immer auf
dem Stack übergeben. Eine Funktion z.B. 2 Argumenten an einen
Funktionspointer mit "..." zuzuweisen kann also nicht funktionieren,
weil hier beim Aufruf die Argumente auf den Stack gelegt werden, aber
die Funktion sie in den Registern erwartet!
Rolf M. schrieb:> Ein void* muss nicht unbedingt zu einem Funktionszeiger kompatibel sein.
Genau, unter POSIX/Unix ist dies zwar garantiert, aber nicht auf allen
Plattformen.
Jörg W. schrieb:> Wobei dann wohlmöglich auch zu vermeiden ist, eine Funktion mit> mehr Parametern aufzurufen als sie hat.
Problematischer sind eher zu wenig Parameter. Zu viele vegetieren halt
nur ungenutzt im Speicher herum. Fehlerhaft ist aber beides.
Jörg W. schrieb:> Spielerei. Timer-unterstützt will ich mir sowas bauen:
Und woher soll der Timer wissen, wie viele und was für Argumente die
Funktion jetzt braucht?
Moin,
was mir an der ganzen Diskussion verborgen bleibt ist der Sinn dahinter.
Spätestens beim Aufruf muss ich dann doch wieder die Argumente kennen.
Die sind also schon beim Kompilieren bekannt.
Dann habe ich einen Funktionszeiger, den ich für alles her nehmen kann.
Das bedeutet aber, dass der Name dessen nur generisch sein kann und im
Grunde nichtssagend sein muss. Damit wird der Quellcode wieder schwerer
lesbar.
Dann doch lieber passende Zeiger mit entsprechendem Namen.
Keiner N. schrieb:> was mir an der ganzen Diskussion verborgen bleibt ist der Sinn dahinter.>> Spätestens beim Aufruf muss ich dann doch wieder die Argumente kennen.> Die sind also schon beim Kompilieren bekannt.
Nicht unbedingt. Ich habe sowas mal für eine Art RPC verwendet, wo man
irgendwie "FUNC 1 2 foo 3.7" in ASCII hinschicken konnte, und dann wurde
eine bestimmte Funktion aufgerufen mit diesen Argumenten.
Jörg W. schrieb:> Spielerei. Timer-unterstützt will ich mir sowas bauen:> After <Zeit> call <Adresse>> bzw.> Every <Zeit> call <Adresse>.
Wenn ich das richtig verstehe, dann willst du dem Timer eine Funktion
mitgeben können, die dieser nach Ablauf der Zeit aufrufen soll. Woher
bekommt dann die Funktion aber ihre Argumente?
Hier könnte std::function helfen.
1
classTimer{
2
// Der Timer sieht nur einen Funktion ohne Parameter
3
std::function<void()>m_func;
4
}
5
6
voidfoo(inti);
7
8
Timert;
9
// Der Integer 1 wird in bind verpackt und dem Timer übergeben.
10
// Ruft der Timer die void Funktion auf, dann ruft diese f(1) auf.
Sven B. schrieb:> und dann wurde> eine bestimmte Funktion aufgerufen mit diesen Argumenten.
Und wie genau sieht der Aufruf der Funktion aus? Was passiert, wenn man
im ASCII-Text unpassende Argumente mitgegeben hat?
Dr. Sommer schrieb:> Sven B. schrieb:>> und dann wurde>> eine bestimmte Funktion aufgerufen mit diesen Argumenten.>> Und wie genau sieht der Aufruf der Funktion aus? Was passiert, wenn man> im ASCII-Text unpassende Argumente mitgegeben hat?
Dann gibt der Code einen Fehler zurück, dass er die Argumentliste nicht
in die erwartete Argumentliste der Funktion konvertieren kann. Der
Aufruf der Funktion passiert mit std::apply und vorher wird aus der
(String-)Argumentliste per Template-Rekursion über die erwarteten
Funktionsargument-Typen ein std::tuple generiert.
Den Code poste ich glaube ich nicht, ist kein so gutes Beispiel weil es
noch C++11 unterstützen muss, mit C++17 geht das viel kürzer.
Sven B. schrieb:> er> Aufruf der Funktion passiert mit std::apply und vorher wird aus der> (String-)Argumentliste per Template-Rekursion über die erwarteten> Funktionsargument-Typen ein std::tuple generiert.
Ah, dann wird aber bestimmt nicht mit "generischen" Funktionszeigern
gearbeitet :-)
Dr. Sommer schrieb:> Sven B. schrieb:>> er>> Aufruf der Funktion passiert mit std::apply und vorher wird aus der>> (String-)Argumentliste per Template-Rekursion über die erwarteten>> Funktionsargument-Typen ein std::tuple generiert.>> Ah, dann wird aber bestimmt nicht mit "generischen" Funktionszeigern> gearbeitet :-)
Doch, klar, wieso denn nicht? Du kannst eine Funktion zu einer Map
hinzufügen, im Stil von "add_command("FOO", &func)", und das
instanziiert intern das Template, was die Funktionsparameter für die
Signatur von func aus einem String generiert. Ich denke das ist auch die
abstrakteste Variante, wie man das machen kann, an irgend*einer* Stelle
muss die Funktion mal explizit erwähnt werden.
Sven B. schrieb:> Du kannst eine Funktion zu einer Map> hinzufügen
Und von welchem Typ ist die map? std::map<string, void (*) (...)>?
Werden da Funktionszeiger umgecastet?
Dr. Sommer schrieb:> Sven B. schrieb:>> Du kannst eine Funktion zu einer Map>> hinzufügen>> Und von welchem Typ ist die map? std::map<string, void (*) (...)>?> Werden da Funktionszeiger umgecastet?
Die Map ist tatsächlich sowas wie map<string,
std::function<void(string)>>, aber die Elemente der Map sind nicht
direkt die Funktionen, sondern da ist ein Lambda als Wrapper drum rum
was das Template richtig instanziiert, das die Funktionsargumente
entpackt.
Sven B. schrieb:> aber die Elemente der Map sind nicht> direkt die Funktionen, sondern da ist ein Lambda als Wrapper drum rum> was das Template richtig instanziiert, das die Funktionsargumente> entpackt.
Ja, so sollte man es machen. Die Lambdas haben vermutlich alle die
gleiche Signatur und implementieren hier somit die Type-Erasure. Das ist
was ganz anderes, als irgendwie Funktionszeiger so umzucasten dass man
sie mit beliebigen Argumenten aufrufen kann...
Dr. Sommer schrieb:> Sven B. schrieb:>> aber die Elemente der Map sind nicht>> direkt die Funktionen, sondern da ist ein Lambda als Wrapper drum rum>> was das Template richtig instanziiert, das die Funktionsargumente>> entpackt.>> Ja, so sollte man es machen. Die Lambdas haben vermutlich alle die> gleiche Signatur und implementieren hier somit die Type-Erasure. Das ist> was ganz anderes, als irgendwie Funktionszeiger so umzucasten dass man> sie mit beliebigen Argumenten aufrufen kann...
Jo, so kann man das auch formulieren. Ich glaube nicht, dass es anders
typsicher geht.
Sven B. schrieb:> Ich glaube nicht, dass es anders> typsicher geht.
Es gibt noch ein paar Varianten mit Polymorphie, die aber letzlich auf
das gleiche hinaus laufen; std::function ist typischerweise so in der
Art implementiert. So ala:
Dr. Sommer schrieb:> Sven B. schrieb:>> Ich glaube nicht, dass es anders>> typsicher geht.>> Es gibt noch ein paar Varianten mit Polymorphie, die aber letzlich auf> das gleiche hinaus laufen; std::function ist typischerweise so in der> Art implementiert. So ala:
Hm joa, das ist im Wesentlichen std::function nachgebaut, oder? Vom
Konzept her ;)
> Der spannende Teil kommt dann in der noch fehlenden deserialize Funktion> :-)
Das fand ich gar nicht das schwierigste daran, mit dem if constexpr ist
das glaube ich sogar sehr einfach. Müsste irgendwie so gehen
(Pseudocode)
Ja so gehts, ist aber leider rekursiv und dürfte auch mit tuple_cat
nicht besonders schnell kompilieren. Mit geschickt angewendeter fold
expression bei überladenem Operator könnte es etwas besser gehen, ist
aber nicht wirklich schön. Eine modifizierende "pop"-artige Operation
auf dem vector ist zur Laufzeit ggf. ineffizient; clever wäre es, per
template-Parameter den Index des nächsten Parameters zu übergeben, und
einfach nur auf das entsprechende Element zuzugreifen. Noch besser ist
es, nur einen String zu übergeben und den Offset im String per Parameter
mitzugeben.
Hmja. Das würde ich erstmal profilen, bevor ich da über-optimiere.
Typischerweise ist ein Element aus einem Vektor mit 7 Einträgen zu
entfernen keine Operation die der Rede wert ist. Limitierend für den
Einsatz ist hier glaube ich oft eher die Lesbarkeit des Quellcodes ;)
Jörg W. schrieb:> Mir ging es darum, mit der Anzahl der Parameter etwas flexibler zu> werden. Wobei dann wohlmöglich auch zu vermeiden ist, eine Funktion mit> mehr Parametern aufzurufen als sie hat.
Wir alle kennen printf mit variablen Argumenten, wobei das erste
Argument bestimmt, wieviele und welche Parameter es braucht.
Sowas ist es bei Dir ja nicht und es wäre mit oder ohne Pointer gleich.
Dann kennen die meisten hier irgendwelche Frameworks, bei denen eine
Funktion (z.B. SioSend(..)) als Ptr ausgeführt ist, damit man
verschiedene Implementierungen (mit DMA oder ohne, mit Fifo oder intern,
...) dahinter laufen lassen kann.
Das ist es bei Dir ja auch nicht.
Dann kennen die meisten hier aus C++ Überladungen, so dass man SioSend()
mal mit 2, 3 oder 4 Parametern aufruft.
Das ist es bei Dir ja auch nicht.
Kannst Du mal ein Beispiel geben, mit 2 leicht unterschiedlichen
Funktionen? Also byte f1b(byte); und byte f2b(byte, byte);. die beide
hinter byte (*fb)(...) stecken können.
Wer ruft fb nun auf, und wie erkennt er, ob es 1 oder 2 Parameter sind?
Dann gibt es auch typsichere Lösungen in plain C.