Rzutowanie wskaźnika funkcji na inny typ

Załóżmy, że mam funkcję, która akceptuje void (*)(void*) Wskaźnik funkcji do użycia jako wywołanie zwrotne:

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

Teraz, jeśli mam taką funkcję:

void my_callback_function(struct my_struct* arg);
Czy Mogę to zrobić bezpiecznie?
do_stuff((void (*)(void*)) &my_callback_function, NULL);

Spojrzałem na to pytanie i spojrzałem na kilka standardów C, które mówią, że można rzucać na "kompatybilne Wskaźniki funkcji", ale nie mogę znaleźć definicji tego, co oznacza "kompatybilny wskaźnik funkcji".

Author: Community, 2009-02-18

7 answers

Jeśli chodzi o standard C, jeśli rzucisz wskaźnik funkcji do wskaźnika funkcji innego typu, a następnie wywołasz go, jest to niezdefiniowane zachowanie. Zob. Załącznik J. 2 (informacyjny):

Zachowanie jest nieokreślone w następujących okolicznościach:

  • wskaźnik jest używany do wywołania funkcji, której Typ nie jest zgodny z pointed-to Typ (6.3.2.3).

Sekcja 6.3.2.3, paragraf 8 brzmi:

Wskaźnik do funkcji jednego typu może być zamieniony na wskaźnik do funkcji innego typu typ i back again; wynik jest równy oryginalnemu wskaźnikowi. Jeśli Przelicznik wskaźnik służy do wywołania funkcji, której Typ nie jest zgodny z typem wskazanym, zachowanie jest nieokreślone.

Innymi słowy, możesz rzucić wskaźnik funkcji na inny typ wskaźnika funkcji, oddać go ponownie i wywołać, a wszystko będzie działać.

The definicja zgodnego jest nieco skomplikowana. Można go znaleźć w sekcji 6.7.5.3, pkt 15:

Aby dwa typy funkcji były kompatybilne, oba muszą określać kompatybilne typy zwrotne127.

Ponadto listy typu parametru, jeśli oba są obecne, zgadzają się w liczbie parametrów oraz w użyciu terminatora elipsy; odpowiednie parametry mają kompatybilne typy. Jeśli jeden typ ma listę typów parametrów i inny typ jest określony przez deklarator funkcji, który nie jest częścią definicji funkcji i który zawiera pusty lista identyfikatorów, Lista parametrów nie może mieć terminatora elipsy i typu każdego parametr musi być zgodny z typem wynikającym z zastosowania domyślne promocje argumentów. Jeśli jeden typ ma listę typów parametrów, a drugi typ jest określone przez definicję funkcji, która zawiera (ewentualnie pustą) listę identyfikatorów, obie uzgodnić liczbę parametrów, A Typ każdego parametru prototypu powinien być zgodny z typem wynikającym z zastosowania argumentu domyślnego promocje do rodzaju odpowiedniego identyfikatora. (W określeniu typu kompatybilności i typu kompozytowego, każdy parametr deklarowany za pomocą funkcji lub tablicy typ jest brany jako typ dopasowany, a każdy parametr zadeklarowany z typem kwalifikowanym jest traktowana jako posiadająca niewykwalifikowaną wersję jego deklarowany Typ.)

127) jeśli oba typy funkcji są "starym stylem", typy parametrów nie są porównywane.

Zasady ustalania, czy dwa typy są kompatybilne, są opisane w sekcji 6.2.7 i nie będę ich tutaj cytował, ponieważ są dość długie, ale możesz je przeczytać na szkicu standardu C99 (PDF) .

Odpowiednia zasada znajduje się w sekcji 6.7.5.1, ust. 2:

Aby dwa typy wskaźników były kompatybilne, oba są identycznie kwalifikowane i oba są wskaźnikami do kompatybilnych typów.

Stąd, ponieważ void* nie jest kompatybilny z struct my_struct*, Wskaźnik funkcji typu {[2] } nie jest kompatybilny ze wskaźnikiem funkcji typu void (*)(struct my_struct*), więc to rzucanie wskaźników funkcji jest technicznie niezdefiniowanym zachowaniem.

W praktyce jednak w niektórych przypadkach można bezpiecznie uciec od wskaźników funkcji rzucania. W konwencji wywołującej x86 argumenty są wypychane na stos, a wszystkie wskaźniki są tego samego rozmiaru (4 bajty w x86 lub 8 bajtów w x86_64). Wywołanie wskaźnika funkcji sprowadza się do wypchnięcia 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 typów.

Rzeczy, których zdecydowanie nie możesz zrobić:

  • rzucane pomiędzy wskaźnikami funkcji o różnych konwencjach wywołania. Będziesz bałagan stos i w najlepszym wypadku, awarii, w najgorszym, sukces cicho z ogromnym dziura bezpieczeństwa. W programowaniu Windows często przekazujesz Wskaźniki funkcji. Win32 oczekuje, że wszystkie funkcje wywołania zwrotnego będą używać konwencji wywołania stdcall (którą makra CALLBACK, PASCAL, i WINAPI wszystkie rozszerzają się do). Jeśli przekażesz wskaźnik funkcji, który używa standardowej konwencji wywołania C (cdecl), spowoduje to błąd.
  • W C++, rzucane między wskaźnikami funkcji członka klasy a zwykłymi wskaźnikami funkcji. To często wyjeżdża w górę C++ newbies. Funkcje należące do klasy mają ukryty this parametr, a jeśli rzucisz funkcję member do zwykłej funkcji, nie ma this obiektu do użycia, i znowu, wiele złego spowoduje.

Kolejny zły pomysł, który może czasami działać, ale jest również niezdefiniowanym zachowaniem:

  • Rzutowanie pomiędzy wskaźnikami funkcyjnymi a wskaźnikami regularnymi(np. rzutowanie a void (*)(void) na a void*). Wskaźniki funkcji niekoniecznie są tej samej wielkości co zwykłe wskaźniki, ponieważ na niektórych architekturach mogą zawierać dodatkowe informacje kontekstowe. To prawdopodobnie będzie działać ok na x86, ale pamiętaj, że to nieokreślone zachowanie.
 104
Author: Adam Rosenfield,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-12-01 15:10:43

Zapytałem ostatnio o ten sam problem dotyczący kodu w GLib. (GLib jest podstawową biblioteką projektu GNOME i jest napisany w C.) powiedziano mi, że cały framework slots ' N ' signals od niego zależy.

W całym kodzie występują liczne przypadki odlewania od typu (1) do (2):

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

To jest wspólne dla łańcucha thru z wywołaniami takimi jak:

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;
}

Przekonaj się sam tutaj w g_array_sort(): http://git.gnome.org/browse/glib/tree/glib/garray.c

Powyższe odpowiedzi są szczegółowe i prawdopodobnie poprawne -- jeśli zasiądziesz w Komitecie Normalizacyjnym. Adam i Johannes zasługują na uznanie za dobrze zbadane odpowiedzi. Jednak, na wolności, znajdziesz ten kod działa dobrze. Kontrowersyjne? Tak. Rozważ to: GLib kompiluje/działa / testuje na dużej liczbie platform (Linux / Solaris / Windows / OS X) z szeroką gamą kompilatorów / linkerów/ładowarek jądra (GCC / CLang / MSVC). Standardy niech będą przeklęte.

Spędziłem trochę czasu myśląc o tych odpowiedziach. Oto mój wniosek:
  1. Jeśli piszesz bibliotekę zwrotną, może to być w porządku. Caveat emptor-używaj na własne ryzyko.
  2. Else, nie rób tego.

Myśląc głębiej po napisaniu tej odpowiedzi, nie zdziwiłbym się, gdyby kod kompilatorów C używał tej samej sztuczki. A ponieważ (większość / wszystkie?) współczesne kompilatory C są bootstrapowane, oznaczałoby to trick jest bezpieczny.

Ważniejsze pytanie do badań: czy ktoś może znaleźć platformę / kompilator/linker / loader gdzie ta sztuczka działa a nie? Duże punkty brownie za to. Założę się, że są pewne wbudowane procesory / systemy, które tego nie lubią. Jednak w przypadku komputerów stacjonarnych (i prawdopodobnie urządzeń mobilnych / tabletów) Ta sztuczka prawdopodobnie nadal działa.

 25
Author: kevinarpe,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-11-29 16:55:32

Nie chodzi o to, czy potrafisz. Trywialnym rozwiązaniem jest

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);

Dobry kompilator wygeneruje kod dla my_callback_helper tylko wtedy, gdy jest naprawdę potrzebny.

 7
Author: MSalters,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-02-18 13:27:30

Masz zgodny typ funkcji, jeśli Typ zwracania i typy parametrów są kompatybilne-w zasadzie (w rzeczywistości jest to bardziej skomplikowane :)). Kompatybilność jest taka sama jak "ten sam typ" tylko bardziej luźne, aby umożliwić różne typy, ale nadal mają jakąś formę powiedzenia"te typy są prawie takie same". Na przykład w C89 dwie struktury były kompatybilne, jeśli były identyczne, ale tylko ich nazwa była inna. Wydaje się, że C99 to zmienił. Cytowanie z c document (Gorąco polecam lekturę, btw!):

Deklaracje typu struktura, Unia lub enumeracja w dwóch różnych jednostkach tłumaczeniowych nie deklarują formalnie tego samego typu, nawet jeśli tekst tych deklaracji pochodzi z tego samego pliku include, ponieważ jednostki tłumaczeniowe same w sobie nie są wspólne. Norma określa zatem dodatkowe zasady zgodności dla takich typów, tak że jeśli dwie takie deklaracje są wystarczająco podobne, są one zgodne.

Że said-yeah ściśle jest to nieokreślone zachowanie, ponieważ twoja funkcja do_stuff lub ktoś inny wywoła twoją funkcję ze wskaźnikiem funkcji posiadającym void* jako parametr, ale twoja funkcja ma niezgodny parametr. Niemniej jednak oczekuję, że wszystkie Kompilatory skompilują i uruchomią go bez jęków. Ale możesz zrobić czystsze, mając inną funkcję, biorąc void* (i rejestrując to jako funkcję zwrotną), która po prostu wywoła Twoją rzeczywistą funkcję.

 4
Author: Johannes Schaub - litb,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-02-18 03:14:48

Ponieważ kod C kompiluje się do instrukcji, które w ogóle nie dbają o typy wskaźników, całkiem dobrze jest użyć kodu, o którym wspomniałeś. Napotkasz problemy, gdy uruchomisz do_stuff ze swoją funkcją zwrotną i wskaźnikiem do czegoś innego niż struktura my_struct jako argument.

Mam nadzieję, że wyjaśnię to, pokazując, co nie zadziała:

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

Lub...

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

Zasadniczo, można rzucać wskaźniki do czego chcesz, tak długo, jak dane nadal mają sens w czas trwania.

 2
Author: che,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-02-18 02:19:10

Jeśli myślisz o sposobie działania wywołań funkcji w C / C++, wypychają one określone elementy na stos, przeskakują do nowej lokalizacji kodu, wykonują, a następnie pop stos po powrocie. Jeśli Twoje wskaźniki funkcji opisują funkcje z tym samym typem zwrotu i tą samą liczbą/rozmiarem argumentów, powinieneś być w porządku.

Dlatego myślę, że powinieneś być w stanie to zrobić bezpiecznie.

 0
Author: Steve Rowe,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-02-18 02:21:16

Wskaźniki Void są kompatybilne z innymi typami wskaźników. To podstawa funkcjonowania malloc i mem (memcpy, memcmp) Praca. Zazwyczaj w C (a nie c++) NULL jest makro zdefiniowane jako ((void *)0).

Spójrz na 6.3.2.3 (Pozycja 1) w C99:

Wskaźnik do void może być zamieniony na lub ze wskaźnika na dowolny niekompletny lub obiekt typu

 0
Author: xslogic,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-12-23 14:45:56