Скажем, у меня есть функция, которая принимает void (*)(void*)
указатель на функцию для использования в качестве обратного вызова:
void do_stuff(void (*callback_fp)(void*), void* callback_arg);
Теперь, если у меня есть такая функция:
void my_callback_function(struct my_struct* arg);
Могу ли я сделать это безопасно?
do_stuff((void (*)(void*)) &my_callback_function, NULL);
Я рассмотрел этот вопрос, и я просмотрел некоторые стандарты C, в которых говорится, что вы можете приводить к «указателям совместимых функций», но я не могу найти определения того, что означает «указатель совместимых функций».
c
function-pointers
Майк Веллер
источник
источник
void (*func)(void *)
означает, чтоfunc
это указатель на функцию с сигнатурой типа, напримерvoid foo(void *arg)
. Так что да, ты прав.Ответы:
Что касается стандарта C, то если вы приведете указатель функции к указателю функции другого типа, а затем вызовете его, это будет неопределенное поведение . См. Приложение J.2 (справочное):
Раздел 6.3.2.3, параграф 8 гласит:
Другими словами, вы можете привести указатель на функцию к другому типу указателя функции, вернуть его обратно и вызвать его, и все заработает.
Определение совместимости довольно сложно. Его можно найти в разделе 6.7.5.3, параграф 15:
Правила определения совместимости двух типов описаны в разделе 6.2.7, и я не буду их здесь цитировать, поскольку они довольно длинные, но вы можете прочитать их в черновике стандарта C99 (PDF) .
Соответствующее правило здесь находится в разделе 6.7.5.1, параграф 2:
Следовательно, поскольку a
void*
несовместимо с astruct my_struct*
, указатель на функцию типаvoid (*)(void*)
несовместим с указателем на функцию типаvoid (*)(struct my_struct*)
, поэтому это приведение указателей на функции является технически неопределенным поведением.Однако на практике в некоторых случаях можно спокойно обойтись без приведения указателей функций. В соглашении о вызовах x86 аргументы помещаются в стек, и все указатели имеют одинаковый размер (4 байта в x86 или 8 байтов в x86_64). Вызов указателя функции сводится к помещению аргументов в стек и косвенному переходу к целевому указателю функции, и, очевидно, нет понятия типов на уровне машинного кода.
То, что вы точно не можете делать:
stdcall
соглашение о вызовах (которые макросыCALLBACK
,PASCAL
иWINAPI
все расширяться). Если вы передадите указатель на функцию, который использует стандартное соглашение о вызовах C (cdecl
), результатом будет плохой результат.this
параметр, и если преобразовать функцию-член в обычную функцию, не будетthis
объекта для использования, и, опять же, это приведет к большому ущербу.Еще одна плохая идея, которая иногда может работать, но также имеет неопределенное поведение:
void (*)(void)
в avoid*
). Указатели функций не обязательно того же размера, что и обычные указатели, поскольку на некоторых архитектурах они могут содержать дополнительную контекстную информацию. Это, вероятно, будет работать на x86, но помните, что это неопределенное поведение.источник
void*
том, что они совместимы с любым другим указателем? Не должно возникнуть проблем с приведением astruct my_struct*
к avoid*
, на самом деле вам даже не нужно приводить , компилятор должен просто принять это. Например, если вы передаете astruct my_struct*
функции, которая принимает avoid*
, приведение типов не требуется. Что мне здесь не хватает, что делает их несовместимыми?void*
что и для типов указателей на функции, см. Спецификацию .void *
"совместим" с любым другим (нефункциональным) указателем только очень точно определенными способами (которые не связаны с тем, что стандарт C означает словом "совместимый" в данном случае). C позволяет avoid *
быть больше или меньше, чем astruct my_struct *
, или иметь биты в другом порядке или отрицать, или что-то еще. Такvoid f(void *)
иvoid f(struct my_struct *)
может быть ABI-несовместимым . C при необходимости преобразует сами указатели за вас, но не может, а иногда и не может преобразовать указанную функцию, чтобы она могла принимать другой тип аргумента.Недавно я спросил об этой же проблеме, касающейся некоторого кода в GLib. (GLib - это основная библиотека для проекта GNOME, написанная на C.) Мне сказали, что от нее зависит вся структура slots'n'signals.
В коде есть множество примеров приведения типов (1) к (2):
typedef int (*CompareFunc) (const void *a, const void *b)
typedef int (*CompareDataFunc) (const void *b, const void *b, void *user_data)
Обычно сквозное соединение с такими вызовами:
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; }
Убедитесь сами здесь
g_array_sort()
: http://git.gnome.org/browse/glib/tree/glib/garray.cПриведенные выше ответы подробны и, вероятно, верны - если вы входите в комитет по стандартам. Адам и Йоханнес заслуживают похвалы за их хорошо исследованные ответы. Однако в природе вы обнаружите, что этот код отлично работает. Спорный? Да. Учтите это: GLib компилирует / работает / тестирует на большом количестве платформ (Linux / Solaris / Windows / OS X) с большим количеством компиляторов / компоновщиков / загрузчиков ядра (GCC / CLang / MSVC). Будь прокляты стандарты, я думаю.
Я некоторое время размышлял над этими ответами. Вот мой вывод:
Поразмыслив глубже после написания этого ответа, я не удивлюсь, если в коде компиляторов C используется тот же трюк. А поскольку (большинство / все?) Современные компиляторы C загружаются, это означает, что трюк безопасен.
Более важный вопрос для исследования: может ли кто-нибудь найти платформу / компилятор / компоновщик / загрузчик, где этот трюк не работает? Основные моменты для этого. Бьюсь об заклад, есть некоторые встроенные процессоры / системы, которым это не нравится. Однако для настольных компьютеров (и, возможно, мобильных / планшетов) этот трюк, вероятно, все еще работает.
источник
Дело не в том, можете ли вы. Тривиальное решение
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);
Хороший компилятор будет генерировать код для my_callback_helper только в том случае, если он действительно нужен, и в этом случае вы были бы рады, что он это сделал.
источник
my_callback_helper
которыми я это тестировал, будут генерировать код , если он не всегда встроен. В этом определенно нет необходимости, так как единственное, что он обычно делает, - этоjmp my_callback_function
. Компилятор, вероятно, хочет удостовериться, что адреса для функций разные, но, к сожалению, он делает это, даже когда функция помечена C99inline
(т.е. «не заботится об адресе»).void *
может быть даже другого размера, чем astruct *
(я думаю, что это неправильно, потому что в противном случае онmalloc
был бы сломан, но этот комментарий имеет 5 голосов, поэтому я отдаю ему должное. Если @mtraceur прав, написанное вами решение не будет правильным.void*
все еще должно работать. Короче говоря,void*
может быть больше бит, но если вы приведетеstruct*
кvoid*
этим дополнительным битам, они могут быть нулями, а при обратном приведении эти нули можно просто отбросить снова.void *
(теоретически) может быть настолько отличным отstruct *
. Я реализую vtable в C, и я используюthis
указатель C ++ в качестве первого аргумента виртуальных функций. Очевидно, этоthis
должен быть указатель на «текущую» (производную) структуру. Итак, виртуальным функциям нужны разные прототипы в зависимости от структуры, в которой они реализованы. Я думал, что использованиеvoid *this
аргумента исправит все, но теперь я узнал, что это неопределенное поведение ...У вас есть совместимый тип функции, если тип возвращаемого значения и типы параметров совместимы - в основном (на самом деле это сложнее :)). Совместимость - это то же самое, что и «один и тот же тип», только более слабая, позволяющая иметь разные типы, но все же иметь некоторую форму выражения «эти типы почти одинаковы». В C89, например, две структуры были совместимы, если в остальном они были идентичны, но отличалось только их имя. C99, кажется, изменил это. Цитата из основного документа (настоятельно рекомендуется прочитать, кстати!):
Тем не менее, да, строго говоря, это неопределенное поведение, потому что ваша функция do_stuff или кто-то другой вызовет вашу функцию с указателем функции, имеющим
void*
параметр as, но ваша функция имеет несовместимый параметр. Но, тем не менее, я ожидаю, что все компиляторы скомпилируют и запустят его без жалоб. Но вы можете сделать чище, имея другую функцию, которая принимаетvoid*
(и регистрирует ее как функцию обратного вызова), которая затем просто вызывает вашу фактическую функцию.источник
Поскольку код C компилируется в инструкции, которые совершенно не заботятся о типах указателей, вполне нормально использовать упомянутый код. Вы столкнетесь с проблемами, если запустите do_stuff со своей функцией обратного вызова и указателем на что-то еще, кроме структуры my_struct в качестве аргумента.
Я надеюсь, что смогу прояснить ситуацию, показав, что не сработает:
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
или...
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
По сути, вы можете приводить указатели к чему угодно, если данные сохраняют смысл во время выполнения.
источник
Если вы думаете о том, как вызовы функций работают в C / C ++, они помещают определенные элементы в стек, переходят в новое место кода, выполняются, а затем возвращают стек при возврате. Если указатели на функции описывают функции с одним и тем же типом возвращаемого значения и одинаковым количеством / размером аргументов, все должно быть в порядке.
Таким образом, я думаю, вы сможете сделать это безопасно.
источник
struct
-поинтеры иvoid
-поинтеры имеют совместимые битовые представления; это не гарантировано,Указатели пустоты совместимы с другими типами указателей. Это основа работы malloc и функций mem (
memcpy
,memcmp
). Обычно в C (а не в C ++)NULL
макрос определяется как((void *)0)
.Посмотрите на 6.3.2.3 (пункт 1) в C99:
источник