Frage

Lassen Sie uns sagen, ich habe eine Funktion, die einen void (*)(void*) Funktionszeiger für die Verwendung als Rückruf akzeptiert:

void do_stuff(void (*callback_fp)(void*), void* callback_arg);

Nun, wenn ich eine Funktion wie folgt aus:

void my_callback_function(struct my_struct* arg);

Kann ich das sicher?

do_stuff((void (*)(void*)) &my_callback_function, NULL);

Ich habe unter diese Frage sah und ich habe sah einige C-Normen, die sagen, Sie zu ‚kompatiblen Funktionszeiger‘ werfen können, aber ich kann keine Definition dessen, was ‚kompatiblen Funktionszeiger‘ bedeutet.

finden
War es hilfreich?

Lösung

Was der C-Standard betrifft, wenn Sie einen Funktionszeiger auf einen Funktionszeiger eines anderen Typs gegossen und dann so nennen, ist es nicht definiertes Verhalten . Siehe Anhang J.2 (informativ):

  

Das Verhalten wird in den folgenden Fällen nicht definiert:

     
      
  • Ein Zeiger verwendet wird, eine Funktion, deren Art zu nennen, ist nicht kompatibel mit dem spitzen-to   Art (6.3.2.3).
  •   

Abschnitt 6.3.2.3 Absatz 8 lautet:

  

Ein Zeiger auf eine Funktion eines Typs kann auf einen Zeiger auf eine Funktion eines anderen umgewandelt werden,   Typ und wieder zurück; das Ergebnis wird auf den ursprünglichen Zeiger gleich vergleichen. Wenn ein umgebautes   Zeiger wird verwendet, um eine Funktion, deren Ausführung mit dem spitzen zulauf-to kompatibel zu nennen, ist nicht ein,   das Verhalten ist nicht definiert.

Also mit anderen Worten, Sie einen Funktionszeiger auf einen anderen Funktion Zeigertyp umwandeln können, es wieder zurückgeworfen, und nennen Sie es, und die Dinge funktionieren.

Die Definition von kompatibel ist etwas kompliziert. Es kann in Abschnitt 6.7.5.3 Absatz 15 zu finden:

  

Für zwei Funktionstypen kompatibel zu sein, beide kompatibel Rückgabetypen geben wird 127 .

     

Darüber hinaus sind die Parameter Typenlisten, wenn beide vorhanden sind, wird in der Anzahl der zustimmen   Parameter und bei der Verwendung der Ellipsen-Terminator; entsprechende Parameter werden müssen   kompatible Typen. Wenn ein Typ hat eine Parametertyp-Liste und der andere Typ wird durch eine spezifizierte   Funktion declarator das nicht eine Funktion Definitionsteil und enthält eine leere   Bezeichnerliste, die Parameterliste ist nicht ein Auslassungszeichen Terminator hat und die Art der jeweils   Parameter werden mit dem Typ kompatibel sein, die sich aus der Anwendung der Ergebnisse   Standardargument Aktionen. Wenn ein Typ hat eine Parametertyp-Liste und der andere Typ ist   durch eine Funktionsdefinition spezifiziert, die eine (möglicherweise leere) Liste-Identifizierer enthält, sind beide   stimmen in der Anzahl der Parameter, und der Typ jedes Prototyp Parameter wird sein   kompatibel mit dem Typ, der sich aus der Anwendung des Standardargument führt   Aktionen auf den Typ der entsprechenden Kennung. (Bei der Bestimmung des Typs   Kompatibilität und eines zusammengesetzten Typs, jeder Parameter mit Funktion oder Array deklariert   Typ wird als mit der eingestellten Art entnommen und jeder Parameter mit qualifiziertem Typ deklarierte   genommen wird, um die uneingeschränkte Version seines deklarierten Typ haben.)

     

127) Wenn beiden Funktionstypen ‚alten Stil‘ ‚sind‘, Parametertypen werden nicht verglichen.

Die Regeln zur Bestimmung, ob zwei Typen kompatibel sind, sind in Abschnitt 6.2.7, und ich werde sie hier nicht zitieren, da sie ziemlich lang sind, aber Sie können sie auf der Entwurf des C99-Standard (PDF) .

Die entsprechende Regel hier ist in Abschnitt 6.7.5.1, Absatz 2:

  

Für zwei Zeigertypen kompatibel zu sein, beide sind gleich qualifiziert sein und beide sind Zeiger auf kompatible Typen sein.

Da somit ein void* nicht kompatibel ist mit einem struct my_struct*, void (*)(void*) ein Funktionszeiger vom Typ ist mit einem Funktionszeiger vom Typ void (*)(struct my_struct*) nicht kompatibel, so dass diese Gießen von Funktionszeiger ist technisch nicht definiertes Verhalten.

In der Praxis aber können Sie sicher mit Gussfunktionszeigern in einigen Fällen weg. In der x86-Aufrufkonvention, werden die Argumente auf dem Stapel abgelegt, und alle Zeiger die gleiche Größe haben (4 Bytes in x86 oder 8 Byte in x86_64). einen Funktionszeiger aufrufen läuft darauf hinaus, drückt die Argumente auf dem Stack und dabei einen indirekten Sprung in die Funktion nach untenZeiger Ziel, und es gibt offensichtlich keine Ahnung von Typen auf Maschinencode-Ebene.

Was Sie auf jeden Fall kann nicht zu tun:

  • Besetzung zwischen Funktionszeiger verschiedener Aufrufkonventionen. Sie werden die Stapel vermasseln und bestenfalls, Absturz, im schlimmsten Fall gelingt es still mit einem riesigen Sicherheitsloch klaffenden. In der Windows-Programmierung, übergeben Sie oft Funktionszeiger um. Win32 erwartet, dass alle Callback-Funktionen die stdcall Aufrufkonvention verwenden (die Makros CALLBACK, PASCAL und WINAPI alle erweitern). Wenn Sie einen Funktionszeiger übergeben, die die Standard-C-Aufrufkonvention (cdecl) verwendet, Schlechtigkeit wird zur Folge haben.
  • In C ++, gegossen zwischen Klasse Mitglied Funktionszeiger und regelmäßige Funktionszeiger. Diese oft Reisen bis C ++ Anfänger. Klasse Member-Funktionen haben einen versteckten this Parameter, und wenn Sie eine Elementfunktion in eine regulären Funktion werfen, gibt es kein this Objekt zu verwenden, und wieder wird viel Schlechtigkeit führen.

Eine weitere schlechte Idee, die manchmal funktionieren kann, ist aber auch nicht definiertes Verhalten:

  • Casting zwischen Funktionszeigern und regelmäßigen Zeigern (zum Beispiel eines void (*)(void) zu einem void* Gießen). Funktionszeiger sind nicht notwendigerweise die gleiche Größe wie normale Zeiger, da auf einigen Architekturen sie zusätzliche Kontextinformationen enthalten könnten. Dies wird wahrscheinlich ok arbeitet auf x86, aber denken Sie daran, dass es nicht definiertes Verhalten ist.

Andere Tipps

, fragte ich über dieses genau die gleiche Frage vor kurzem einige Code in GLib in Bezug auf. (Glib ist eine zentrale Bibliothek für das GNOME-Projekt und in C geschrieben) wurde mir gesagt, die gesamte slots'n'signals Rahmen hängt davon ab.

Während des gesamten Code gibt es zahlreiche Fälle von Typ Gießen (1) bis (2):

  1. typedef int (*CompareFunc) (const void *a, const void *b)
  2. typedef int (*CompareDataFunc) (const void *b, const void *b, void *user_data)

Es ist üblich, Ketten-thru mit Anrufen wie folgt aus:

int stuff_equal (GStuff      *a,
                 GStuff      *b,
                 CompareFunc  compare_func)
{
    return stuff_equal_with_data(a, b, (CompareDataFunc) compare_func, NULL);
}

int stuff_equal_with_data (GStuff          *a,
                           GStuff          *b,
                           CompareDataFunc  compare_func,
                           void            *user_data)
{
    int result;
    /* do some work here */
    result = compare_func (data1, data2, user_data);
    return result;
}

Sehen Sie selbst, hier in g_array_sort(): http: //git.gnome .org / browse / glib / Baum / glib / garray.c

Die Antworten oben sind detailliert und wahrscheinlich richtig - , wenn Sie sitzen auf dem Normenausschuss. Adam und Johannes verdienen Anerkennung für ihre gut recherchierten Antworten. Doch in der Wildnis, werden Sie feststellen, dieser Code funktioniert gut. Umstritten? Ja. Bedenken Sie: GLib kompiliert / Werke / Tests auf einer Vielzahl von Plattformen (Linux / Solaris / Windows / OS X) mit einer Vielzahl von Compiler / Linker / kernel Lader (GCC / Clang / MSVC). Standards verdammt sein, glaube ich.

verbrachte ich über diese Antworten einige Zeit zu denken. Hier ist meine Schlussfolgerung:

  1. Wenn Sie eine Rückruf Bibliothek schreiben, dies könnte in Ordnung sein. Caveat emptor -. Einsatz erfolgt auf eigene Gefahr
  2. Else, tut es nicht.

Denken tiefer nach dieser Antwort zu schreiben, würde ich nicht überrascht, wenn der Code für C-Compiler diesen gleichen Trick verwendet. Und da (die meisten / alle?) Moderne C-Compiler Bootstrap werden, würde dies bedeuten, der Trick sicher ist.

Eine weitere wichtige Frage für die Forschung: jemand eine Plattform / Compiler / Linker / loader gefunden, wo dieser Trick funktioniert nicht Arbeit? Die wichtigsten Pluspunkte für diesen einen. Ich wette, es gibt einige Embedded-Prozessoren / Systeme, die es nicht mögen. Doch für Desktop-Computing (und wahrscheinlich auch mobil / Tablette), dieser Trick wahrscheinlich noch funktioniert.

Der Punkt ist wirklich nicht, ob Sie können. Die triviale Lösung ist

void my_callback_function(struct my_struct* arg);
void my_callback_helper(void* pv)
{
    my_callback_function((struct my_struct*)pv);
}
do_stuff(&my_callback_helper);

Ein guter Compiler wird nur Code für my_callback_helper erzeugen, wenn es wirklich nötig ist, in dem Fall, dass Sie froh sein, würde es getan hat.

Sie haben einen kompatiblen Funktionstyp, wenn der Rückgabetyp und Parametertypen kompatibel sind - im Allgemeinen (es ist in Wirklichkeit komplizierter ist :)). Kompatibilität ist das gleiche wie „gleiche Art“ nur mehr lax zu ermöglichen verschiedene Typen zu haben, aber immer noch eine Form haben zu sagen: „Diese Typen sind fast gleich“. In C89 zum Beispiel waren zwei structs kompatibel, wenn sie ansonsten identisch waren, aber nur ihr Name war anders. C99 scheinen, dass sich geändert zu haben. Zitiert aus der c Begründung Dokument (hoch Lesen empfohlen, btw!):

  

Struktur, Vereinigung oder Aufzählungstyp Erklärungen in zwei verschiedenen Übersetzungseinheiten erklären formal nicht die gleiche Art, auch wenn der Text dieser Erklärungen aus dem gleichen kommt Include-Datei, da die Übersetzungseinheiten selbst disjunkt sind. Der Standard-so gibt zusätzliche Kompatibilitätsregeln für solche Typen, so dass, wenn zwei solche Erklärungen ausreichend ähnlich sind sie kompatibel sind.

Das heißt - ja genau dieses undefinierten Verhalten ist, weil Ihre do_stuff Funktion oder jemand anderes Ihre Funktion mit einem Funktionszeiger mit void* als Parameter aufrufen, aber Ihre Funktion hat eine inkompatible Parameter. Aber dennoch erwarte ich, dass alle Compiler es ohne Jammer zu kompilieren und auszuführen. Aber man kann sauberer machen eine andere Funktion, indem eine void* nehmen (und Registrierung dass als Callback-Funktion), die nur Ihre eigentliche Funktion aufrufen, wird dann.

Als C-Code kompiliert Anweisung, die überhaupt nicht über Zeigertypen kümmern, dann ist es ganz in Ordnung, den Code zu verwenden, die Sie erwähnen. Sie würden auf Probleme stoßen, wenn Sie do_stuff mit Ihrer Callback-Funktion und Zeiger auf etwas anderes dann my_struct Struktur als Argument führen würde.

Ich hoffe, ich kann es klarer zeigen, was nicht funktionieren würde:

int my_number = 14;
do_stuff((void (*)(void*)) &my_callback_function, &my_number);
// my_callback_function will try to access int as struct my_struct
// and go nuts

oder ...

void another_callback_function(struct my_struct* arg, int arg2) { something }
do_stuff((void (*)(void*)) &another_callback_function, NULL);
// another_callback_function will look for non-existing second argument
// on the stack and go nuts

Grundsätzlich können Sie werfen Zeiger auf, was auch immer Sie mögen, solange die Daten weiterhin Sinn zur Laufzeit machen.

Wenn Sie über die Art und Weise denken Funktion Arbeit in C / C ++ ruft, schieben sie bestimmte Elemente auf dem Stapel, springen auf den neuen Code Lage, Ausführen, dann Pop der Stapel auf Rückkehr. Wenn Ihre Funktionszeiger Funktionen mit dem gleichen Rückgabetyp und die gleiche Anzahl / Größe der Argumente beschreiben, sollten Sie in Ordnung sein.

So, ich denke, Sie sollten so sicher tun können.

Void-Pointer sind kompatibel mit anderen Arten von Zeigern. Es ist das Rückgrat, wie malloc und die mem-Funktionen (memcpy, memcmp) Arbeit. Typischerweise wird in C (eher als C ++) NULL ist ein Makro, wie ((void *)0) definiert.

Sehen Sie in 6.3.2.3 (Punkt 1) in C99:

  

Ein Zeiger für ungültig zu erklären kann zu oder von einem Zeiger auf jeden unvollständigen oder Objekttyp umgewandelt werden

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top