Forum: PC-Programmierung C/C++ Funktionspointer


Announcement: there is an English version of this forum on EmbDev.net. Posts you create there will be displayed on Mikrocontroller.net and EmbDev.net.
von Jörg W. (Gast)


Bewertung
0 lesenswert
nicht lesenswert
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
typedef byte(*tPtr1)(byte)
kann das nicht und zickt, wenn die Funktion nicht genau einen Parameter 
hat.

von Felix U. (ubfx)


Bewertung
1 lesenswert
nicht lesenswert
Wenn du keinen Prototyp für die Funktion festlegen willst, ist es kein 
Funktionspointer. Um die Adresse zu speichern nimmst du void*

von g457 (Gast)


Bewertung
0 lesenswert
nicht lesenswert
> es geht nur darum, die Adressen bestimmter Funktionen zu speichern

Wozu soll das gut sein?

von Jim M. (turboj)


Bewertung
-1 lesenswert
nicht lesenswert
Man muss denn Compiler auf neueres C++ einstellen, aber
1
typedef void (*foo_t)(...);

Compiliert hier:
1
#include <stdio.h>
2
3
typedef void (*foo_t) (...);
4
5
int main()
6
{
7
    foo_t foo = (foo_t) printf;
8
    foo("Hello World");
9
10
    return 0;
11
}

von Jim M. (turboj)


Bewertung
2 lesenswert
nicht lesenswert
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).

von mh (Gast)


Bewertung
1 lesenswert
nicht lesenswert
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.

von Jörg W. (Gast)


Bewertung
0 lesenswert
nicht lesenswert
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.

von Sven B. (scummos)


Bewertung
2 lesenswert
nicht lesenswert
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.

von A. S. (achs)


Bewertung
0 lesenswert
nicht lesenswert
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.

: Bearbeitet durch User
von Rolf M. (rmagnus)


Bewertung
1 lesenswert
nicht lesenswert
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.

von Daniel A. (daniel-a)


Bewertung
1 lesenswert
nicht lesenswert
Folgendes ist gültiges c99:
1
typedef void(*noprotofunc)();
2
3
void test(int a, int b){}
4
5
noprotofunc x = test;

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.

von mh (Gast)


Bewertung
2 lesenswert
nicht lesenswert
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.

von x^2 (Gast)


Bewertung
0 lesenswert
nicht lesenswert
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*".

von Jörg W. (Gast)


Bewertung
0 lesenswert
nicht lesenswert
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  :-)

von Jörg W. (Gast)


Bewertung
0 lesenswert
nicht lesenswert
So. Ich habe in meinem o.g. Code
1
typedef byte(*tPtr1)(byte)
einfach 'byte' durch '...' ersetzt
1
typedef byte(*tPtr1)(...)
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?

von x^2 (Gast)


Bewertung
0 lesenswert
nicht lesenswert
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.

von Jörg W. (Gast)


Bewertung
0 lesenswert
nicht lesenswert
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.

von Kaj (Gast)


Bewertung
0 lesenswert
nicht lesenswert
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
typedef struct {
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++?

von Dr. Sommer (Gast)


Bewertung
0 lesenswert
nicht lesenswert
1
using tPtr1 = byte (*) (std::initializer_list<byte> l);
2
3
byte fun1 (std::initializer_list<byte> l) {
4
  for (byte b : l) {
5
    std::cout << b;
6
  }
7
  return 42;
8
}
9
10
int main () {
11
  tPtr1 f = fun1;
12
  f ({1, 2, 3, 4});
13
}

von Dr. Sommer (Gast)


Angehängte Dateien:

Bewertung
0 lesenswert
nicht lesenswert
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?

von Keiner N. (nichtgast)


Bewertung
0 lesenswert
nicht lesenswert
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.

: Bearbeitet durch User
von Sven B. (scummos)


Bewertung
0 lesenswert
nicht lesenswert
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.

von M.K. B. (mkbit)


Bewertung
0 lesenswert
nicht lesenswert
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
class Timer {
2
   // Der Timer sieht nur einen Funktion ohne Parameter
3
   std::function<void()> m_func;
4
}
5
6
void foo(int i);
7
8
Timer t;
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.
11
t.m_func = std::bind(foo, 1);

von Dr. Sommer (Gast)


Bewertung
0 lesenswert
nicht lesenswert
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?

von Sven B. (scummos)


Bewertung
0 lesenswert
nicht lesenswert
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.

von Dr. Sommer (Gast)


Bewertung
0 lesenswert
nicht lesenswert
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 :-)

von Sven B. (scummos)


Bewertung
0 lesenswert
nicht lesenswert
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.

: Bearbeitet durch User
von Dr. Sommer (Gast)


Bewertung
0 lesenswert
nicht lesenswert
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?

von Sven B. (scummos)


Bewertung
0 lesenswert
nicht lesenswert
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.

von Dr. Sommer (Gast)


Bewertung
0 lesenswert
nicht lesenswert
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...

von Sven B. (scummos)


Bewertung
0 lesenswert
nicht lesenswert
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.

von Dr. Sommer (Gast)


Bewertung
0 lesenswert
nicht lesenswert
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:
1
#include <map>
2
#include <string>
3
4
class InvokerBase {
5
  public:
6
    virtual void invoke (std::string command) = 0;
7
};
8
9
template <typename T, T Fun>
10
class Invoker;
11
12
template <typename R, typename... Args, R (*Fun) (Args...)>
13
class Invoker<R (*) (Args...), Fun> : public InvokerBase {
14
  public:
15
    virtual void invoke (std::string command) {
16
      // Args aus command deserialisieren
17
      std::tuple<Args...> args = deserialize<Args...> (command);
18
      std::apply (Fun, std::move (args));
19
    }
20
};
21
22
void FooFun (int, char, float);
23
24
Invoker<decltype(&FooFun), &FooFun> i1;
25
26
std::map<std::string, InvokerBase*> commands {
27
  {"Foo", &i1 }
28
};

Der spannende Teil kommt dann in der noch fehlenden deserialize Funktion 
:-)

von Sven B. (scummos)


Bewertung
0 lesenswert
nicht lesenswert
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)
1
template<typename First, typename ... Args>
2
std::tuple<typename std::decay<First>::type, typename std::decay<Args>::type...> deserialize(std::vector<string> remainArgs)
3
{
4
  auto first = fromString(remainArgs.takeFirst());
5
  auto firstTuple = std::tuple<std::decay<First>::type>(first);
6
  if constexpr ( sizeof...(Args) == 0 ) {
7
    return firstTuple;
8
  }
9
10
  return std::tuple_cat(firstTuple, deserialize<Args...>(remainArgs));
11
}

Meine alte Variante macht es mit Function Template specialization, da 
ist es murksig.

: Bearbeitet durch User
von Dr. Sommer (Gast)


Bewertung
0 lesenswert
nicht lesenswert
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.

von Sven B. (scummos)


Bewertung
0 lesenswert
nicht lesenswert
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 ;)

von A. S. (achs)


Bewertung
0 lesenswert
nicht lesenswert
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.

Antwort schreiben

Die Angabe einer E-Mail-Adresse ist freiwillig. Wenn Sie automatisch per E-Mail über Antworten auf Ihren Beitrag informiert werden möchten, melden Sie sich bitte an.

Wichtige Regeln - erst lesen, dann posten!

  • Groß- und Kleinschreibung verwenden
  • Längeren Sourcecode nicht im Text einfügen, sondern als Dateianhang

Formatierung (mehr Informationen...)

  • [c]C-Code[/c]
  • [code]Code in anderen Sprachen, ASCII-Zeichnungen[/code]
  • [math]Formel in LaTeX-Syntax[/math]
  • [[Titel]] - Link zu Artikel
  • Verweis auf anderen Beitrag einfügen: Rechtsklick auf Beitragstitel,
    "Adresse kopieren", und in den Text einfügen




Bild automatisch verkleinern, falls nötig
Bitte das JPG-Format nur für Fotos und Scans verwenden!
Zeichnungen und Screenshots im PNG- oder
GIF-Format hochladen. Siehe Bildformate.
Hinweis: der ursprüngliche Beitrag ist mehr als 6 Monate alt.
Bitte hier nur auf die ursprüngliche Frage antworten,
für neue Fragen einen neuen Beitrag erstellen.

Mit dem Abschicken bestätigst du, die Nutzungsbedingungen anzuerkennen.