Angenommen, ich habe eine Funktion, die einen void (*)(void*)
Funktionszeiger zur Verwendung als Rückruf akzeptiert :
void do_stuff(void (*callback_fp)(void*), void* callback_arg);
Nun, wenn ich eine Funktion wie diese habe:
void my_callback_function(struct my_struct* arg);
Kann ich das sicher machen?
do_stuff((void (*)(void*)) &my_callback_function, NULL);
Ich habe mir diese Frage und einige C-Standards angesehen, die besagen, dass Sie in "kompatible Funktionszeiger" umwandeln können, aber ich kann keine Definition finden, was "kompatibler Funktionszeiger" bedeutet.
c
function-pointers
Mike Weller
quelle
quelle
void (*func)(void *)
bedeutet, dass diesfunc
ein Zeiger auf eine Funktion mit einer Typensignatur wie zvoid foo(void *arg)
. Also ja, du hast recht.Antworten:
Wenn Sie für den C-Standard einen Funktionszeiger auf einen Funktionszeiger eines anderen Typs umwandeln und diesen dann aufrufen, handelt es sich um ein undefiniertes Verhalten . Siehe Anhang J.2 (informativ):
Abschnitt 6.3.2.3, Absatz 8 lautet:
Mit anderen Worten, Sie können einen Funktionszeiger auf einen anderen Funktionszeigertyp umwandeln, ihn wieder zurücksetzen und aufrufen, und die Dinge werden funktionieren.
Die Definition von kompatibel ist etwas kompliziert. Es ist in Abschnitt 6.7.5.3, Absatz 15 zu finden:
Die Regeln zum Bestimmen, ob zwei Typen kompatibel sind, sind in Abschnitt 6.2.7 beschrieben, und ich werde sie hier nicht zitieren, da sie ziemlich lang sind, aber Sie können sie im Entwurf des C99-Standards (PDF) lesen .
Die relevante Regel hier ist in Abschnitt 6.7.5.1, Absatz 2:
Da a
void*
nicht mit a kompatibel iststruct my_struct*
, ist ein Funktionszeiger vom Typvoid (*)(void*)
nicht mit einem Funktionszeiger vom Typ kompatibelvoid (*)(struct my_struct*)
, so dass diese Umwandlung von Funktionszeigern ein technisch undefiniertes Verhalten ist.In der Praxis können Sie jedoch in einigen Fällen sicher mit Casting-Funktionszeigern davonkommen. In der x86-Aufrufkonvention werden Argumente auf den Stapel übertragen, und alle Zeiger haben dieselbe Größe (4 Byte in x86 oder 8 Byte in x86_64). Das Aufrufen eines Funktionszeigers läuft darauf hinaus, die Argumente auf dem Stapel zu verschieben und einen indirekten Sprung zum Ziel des Funktionszeigers zu machen, und es gibt offensichtlich keine Vorstellung von Typen auf der Ebene des Maschinencodes.
Dinge, die Sie definitiv nicht tun können:
stdcall
Aufrufkonvention (welche die MakrosCALLBACK
,PASCAL
undWINAPI
alle erweitern). Wenn Sie einen Funktionszeiger übergeben, der die Standard-C-Aufrufkonvention (cdecl
) verwendet, führt dies zu einer Unrichtigkeit.this
Parameter, und wenn Sie eine Elementfunktion in eine reguläre Funktionthis
umwandeln , gibt es kein zu verwendendes Objekt, und wiederum führt dies zu einer großen Beeinträchtigung.Eine andere schlechte Idee, die manchmal funktioniert, aber auch undefiniertes Verhalten ist:
void (*)(void)
nach avoid*
). Funktionszeiger haben nicht unbedingt die gleiche Größe wie normale Zeiger, da sie auf einigen Architekturen möglicherweise zusätzliche Kontextinformationen enthalten. Dies wird unter x86 wahrscheinlich in Ordnung sein, aber denken Sie daran, dass es sich um ein undefiniertes Verhalten handelt.quelle
void*
dass sie mit jedem anderen Zeiger kompatibel sind? Es sollte kein Problem geben, astruct my_struct*
in a umzuwandelnvoid*
, tatsächlich sollten Sie nicht einmal umwandeln müssen, der Compiler sollte es einfach akzeptieren. Wenn Sie beispielsweise astruct my_struct*
an eine Funktion übergeben, die a übernimmtvoid*
, ist kein Casting erforderlich. Was fehlt mir hier, was diese inkompatibel macht?void*
wie für Funktionszeigertypen, siehe Spezifikation .void *
ist nur auf sehr genau definierte Weise mit jedem anderen (nicht funktionsfähigen) Zeiger "kompatibel" (was in diesem Fall nichts mit dem zu tun hat, was der C-Standard mit dem Wort "kompatibel" bedeutet). C erlaubt es avoid *
, größer oder kleiner als a zu seinstruct my_struct *
oder die Bits in unterschiedlicher Reihenfolge oder negiert zu haben oder was auch immer. Alsovoid f(void *)
undvoid f(struct my_struct *)
kann ABI-inkompatibel sein . C konvertiert die Zeiger bei Bedarf selbst für Sie, kann jedoch eine Funktion, auf die verwiesen wird, nicht und manchmal auch nicht konvertieren, um einen möglicherweise anderen Argumenttyp zu verwenden.Ich habe kürzlich nach genau demselben Problem bezüglich eines Codes in GLib gefragt. (GLib ist eine Kernbibliothek für das GNOME-Projekt und in C geschrieben.) Mir wurde gesagt, dass das gesamte Slots'n'signals-Framework davon abhängt.
Im gesamten Code gibt es zahlreiche Fälle von Casting von Typ (1) bis (2):
typedef int (*CompareFunc) (const void *a, const void *b)
typedef int (*CompareDataFunc) (const void *b, const void *b, void *user_data)
Es ist üblich, bei Anrufen wie diesen eine Kette durchzuketten:
Überzeugen Sie sich hier unter
g_array_sort()
: http://git.gnome.org/browse/glib/tree/glib/garray.cDie obigen Antworten sind detailliert und wahrscheinlich richtig - wenn Sie im Normungsausschuss sitzen. Adam und Johannes verdienen Anerkennung für ihre gut recherchierten Antworten. In freier Wildbahn werden Sie jedoch feststellen, dass dieser Code einwandfrei funktioniert. Umstritten? Ja. Beachten Sie Folgendes: GLib kompiliert / arbeitet / testet auf einer großen Anzahl von Plattformen (Linux / Solaris / Windows / OS X) mit einer Vielzahl von Compilern / Linkern / Kernel-Loadern (GCC / CLang / MSVC). Standards sind verdammt, denke ich.
Ich habe einige Zeit damit verbracht, über diese Antworten nachzudenken. Hier ist meine Schlussfolgerung:
Wenn ich nach dem Schreiben dieser Antwort genauer nachdenke, wäre ich nicht überrascht, wenn der Code für C-Compiler denselben Trick verwendet. Und da (die meisten / alle?) Moderne C-Compiler gebootet sind, würde dies bedeuten, dass der Trick sicher ist.
Eine wichtigere Frage für die Forschung: Kann jemand eine Plattform / Compiler / Linker / Loader finden, bei der dieser Trick nicht funktioniert? Wichtige Brownie-Punkte für diesen. Ich wette, es gibt einige eingebettete Prozessoren / Systeme, die es nicht mögen. Für Desktop-Computer (und wahrscheinlich für Mobilgeräte / Tablets) funktioniert dieser Trick jedoch wahrscheinlich immer noch.
quelle
Der Punkt ist wirklich nicht, ob Sie können. Die triviale Lösung ist
Ein guter Compiler generiert nur dann Code für my_callback_helper, wenn er wirklich benötigt wird. In diesem Fall wären Sie froh, dass dies der Fall ist.
quelle
my_callback_helper
, es sei denn, er ist immer inline. Dies ist definitiv nicht notwendig, da es nur dazu neigtjmp my_callback_function
. Der Compiler möchte wahrscheinlich sicherstellen, dass die Adressen für die Funktionen unterschiedlich sind, tut dies jedoch leider auch dann, wenn die Funktion mit C99 markiert istinline
(dh "die Adresse ist mir egal").Sie haben einen kompatiblen Funktionstyp, wenn der Rückgabetyp und die Parametertypen kompatibel sind - im Grunde genommen (in der Realität ist dies komplizierter :)). Die Kompatibilität ist die gleiche wie bei "gleichem Typ", nur lockerer, um unterschiedliche Typen zuzulassen, aber es gibt immer noch die Form "diese Typen sind fast gleich". In C89 waren beispielsweise zwei Strukturen kompatibel, wenn sie ansonsten identisch waren, aber nur ihr Name unterschiedlich war. C99 scheint das geändert zu haben. Zitat aus dem Begründungsdokument (sehr zu empfehlen, übrigens!):
Das heißt - ja, streng genommen ist dies ein undefiniertes Verhalten, da Ihre do_stuff-Funktion oder eine andere Person Ihre Funktion mit einem Funktionszeiger
void*
als Parameter aufruft , Ihre Funktion jedoch einen inkompatiblen Parameter hat. Trotzdem erwarte ich von allen Compilern, dass sie es kompilieren und ausführen, ohne zu stöhnen. Sie können jedoch sauberer arbeiten, indem Sie eine andere Funktion verwendenvoid*
(und diese als Rückruffunktion registrieren), die dann nur Ihre eigentliche Funktion aufruft.quelle
Da C-Code zu Anweisungen kompiliert wird, die sich überhaupt nicht um Zeigertypen kümmern, ist es in Ordnung, den von Ihnen erwähnten Code zu verwenden. Sie würden auf Probleme stoßen, wenn Sie do_stuff mit Ihrer Rückruffunktion ausführen und auf etwas anderes als die my_struct-Struktur als Argument verweisen würden.
Ich hoffe, ich kann es klarer machen, indem ich zeige, was nicht funktionieren würde:
oder...
Grundsätzlich können Sie Zeiger auf beliebige Elemente setzen, solange die Daten zur Laufzeit weiterhin sinnvoll sind.
quelle
Wenn Sie über die Funktionsweise von Funktionsaufrufen in C / C ++ nachdenken, werden bestimmte Elemente auf dem Stapel verschoben, zum neuen Code-Speicherort gesprungen, ausgeführt und der Stapel bei der Rückkehr eingeblendet. Wenn Ihre Funktionszeiger Funktionen mit demselben Rückgabetyp und derselben Anzahl / Größe von Argumenten beschreiben, sollten Sie in Ordnung sein.
Daher denke ich, dass Sie dies sicher tun sollten.
quelle
struct
Zeiger undvoid
Zeiger kompatible Bitdarstellungen haben. Das ist nicht garantiert der FallLeere Zeiger sind mit anderen Zeigertypen kompatibel. Es ist das Rückgrat der Funktionsweise von malloc und mem (
memcpy
(memcmp
)). In der Regel ist in C (anstelle von C ++)NULL
ein Makro definiert als((void *)0)
.Siehe 6.3.2.3 (Punkt 1) in C99:
quelle