Jeśli chodzi o standard C, rzutowanie wskaźnika funkcji na wskaźnik funkcji innego typu, a następnie wywołanie go, oznacza niezdefiniowane zachowanie . Patrz załącznik J.2 (informacyjny):
Zachowanie jest niezdefiniowane w następujących okolicznościach:
- Wskaźnik służy do wywołania funkcji, której typ nie jest zgodny z typem wskazywanym (6.3.2.3).
Sekcja 6.3.2.3, akapit 8 brzmi:
Wskaźnik do funkcji jednego typu można przekształcić we wskaźnik do funkcji innego typu iz powrotem; wynik jest równy pierwotnemu wskaźnikowi. Jeśli przekonwertowany wskaźnik jest używany do wywołania funkcji, której typ nie jest zgodny z typem wskazanym, zachowanie jest niezdefiniowane.
Innymi słowy, możesz rzutować wskaźnik funkcji na inny typ wskaźnika funkcji, rzutować go z powrotem i wywoływać, a wszystko będzie działać.
Definicja zgodności jest nieco skomplikowana. Można go znaleźć w sekcji 6.7.5.3, akapit 15:
Aby dwa typy funkcji były zgodne, oba powinny określać zgodne typy zwracane 127 .
Ponadto listy typów parametrów, jeśli są obecne, powinny zgadzać się co do liczby parametrów i użycia terminatora wielokropka; odpowiednie parametry mają zgodne typy. Jeśli jeden typ ma listę typów parametrów, a drugi typ jest określony przez deklarator funkcji, który nie jest częścią definicji funkcji i który zawiera pustą listę identyfikatorów, lista parametrów nie powinna mieć zakończenia wielokropka, a typ każdego parametru powinien być zgodne z typem wynikającym z zastosowania domyślnych promocji argumentów. Jeśli jeden typ ma listę typów parametrów, a drugi typ jest określony przez definicję funkcji zawierającą (prawdopodobnie pustą) listę identyfikatorów, oba powinny się zgadzać co do liczby parametrów, a typ każdego parametru prototypu będzie zgodny z typem wynikającym z zastosowania promocji argumentów domyślnych do typu odpowiedniego identyfikatora. (Przy określaniu zgodności typu i typu złożonego każdy parametr zadeklarowany z typem funkcji lub tablicy jest traktowany jako mający dostosowany typ, a każdy parametr zadeklarowany z typem kwalifikowanym jest traktowany jako mający niekwalifikowaną wersję swojego zadeklarowanego typu).
127) Jeśli oba typy funkcji są w „starym stylu”, typy parametrów nie są porównywane.
Zasady określania, czy dwa typy są kompatybilne, są opisane w sekcji 6.2.7 i nie będę ich tutaj cytować, ponieważ są dość obszerne, ale można je przeczytać w wersji roboczej standardu C99 (PDF) .
Odpowiedni przepis znajduje się w sekcji 6.7.5.1, ustęp 2:
Aby dwa typy wskaźników były kompatybilne, oba powinny być identycznie kwalifikowane i oba powinny być wskaźnikami do kompatybilnych typów.
W związku z tym, ponieważ a void*
nie jest kompatybilny z a struct my_struct*
, wskaźnik funkcji typu void (*)(void*)
nie jest zgodny ze wskaźnikiem funkcji typu void (*)(struct my_struct*)
, więc rzutowanie wskaźników funkcji jest technicznie niezdefiniowanym zachowaniem.
W praktyce jednak w niektórych przypadkach można bezpiecznie uciec od rzutowania wskaźników funkcji. W konwencji wywoływania x86 argumenty są umieszczane na stosie, a wszystkie wskaźniki mają ten sam rozmiar (4 bajty w x86 lub 8 bajtów w x86_64). Wywołanie wskaźnika funkcji sprowadza się do umieszczenia argumentów na stosie i wykonania pośredniego skoku do celu wskaźnika funkcji, a na poziomie kodu maszynowego nie ma oczywiście pojęcia o typach.
Rzeczy, których zdecydowanie nie możesz zrobić:
- Rzutowanie między wskaźnikami funkcji o różnych konwencjach wywoływania. Zepsujesz stos iw najlepszym wypadku rozbijesz się po cichu z wielką, ziejącą luką w zabezpieczeniach. W programowaniu w systemie Windows często przekazuje się wskaźniki funkcji. Win32 oczekuje, że wszystkie funkcje zwrotne używać
stdcall
konwencji wzywającą (który makra CALLBACK
, PASCAL
i WINAPI
wszystko rozwijać się). Jeśli przekażesz wskaźnik do funkcji, który używa standardowej konwencji wywoływania języka C ( cdecl
), wyniknie to źle.
- W C ++ rzutowanie między wskaźnikami funkcji składowych klasy a zwykłymi wskaźnikami funkcji. To często wyzwala początkujących C ++. Funkcje składowe klasy mają ukryty
this
parametr, a jeśli rzutujesz funkcję składową na zwykłą funkcję, nie ma this
obiektu do użycia i znowu spowoduje to wiele zła.
Kolejny zły pomysł, który czasami może działać, ale jest także niezdefiniowanym zachowaniem:
- Rzutowanie między wskaźnikami funkcji a zwykłymi wskaźnikami (np. Rzutowanie a
void (*)(void)
do a void*
). Wskaźniki funkcji niekoniecznie są tego samego rozmiaru co zwykłe wskaźniki, ponieważ na niektórych architekturach mogą zawierać dodatkowe informacje kontekstowe. Prawdopodobnie zadziała to dobrze na x86, ale pamiętaj, że jest to niezdefiniowane zachowanie.