Приведение указателя функции к другому типу
допустим, у меня есть функция, которая принимает 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, которые говорят, что вы можете использовать "совместимые указатели функций", но я не могу найти определение того, что означает "совместимый указатель функций".
7 ответов:
что касается стандарта C, если вы приведете указатель функции к указателю функции другого типа, а затем вызовете его, это неопределенное поведение. См. приложение J. 2 (информативное):
6.3.2.3, пункт 8 гласит:поведение не определено в следующих обстоятельствах:
- указатель используется для вызова функции, тип которой не совместим с указанным тип (6.3.2.3).
указатель на функцию одного типа может быть преобразован в указатель на функцию другого тип и обратно; результат должен сравниваться равный исходному указатель. Если преобразованный указатель используется для вызова функции, тип которой не совместим с указанным типом, поведение не определено.
другими словами, вы можете привести указатель функции к другому типу указателя функции, вернуть его обратно опять же, и назовите его, и все будет работать.
определение совместимость несколько усложняется. Его можно найти в разделе 6.7.5.3, пункт 15:
чтобы два типа функций были совместимы, оба должны указывать совместимые возвращаемые типы127.
кроме того, списки типов параметров, если они присутствуют, должны совпадать по количеству параметры и в пользе Терминатора многоточия; соответствовать параметры должны иметь совместимые типы. Если один тип имеет список типов параметров, а другой тип задается a Декларатор функций, не являющийся частью определения функции и содержащий пустое значение список идентификаторов, список параметров не должен иметь многоточие Терминатор и тип каждого из них параметр должен быть совместим с типом, который является результатом применения продвижение аргументов по умолчанию. Если один тип имеет список типов параметров, а другой тип указанные в определении функции, содержащем (возможно, пустой) список идентификаторов, оба должны согласуйте количество параметров, и тип каждого параметра прототипа должен быть совместим с типом, который является результатом применения аргумента по умолчанию продвижение к типу соответствующего идентификатора. (При определении типа совместимость и составного типа, каждый параметр, объявленный в функции или массива тип принимается как имеющий скорректированный тип и каждый параметр, объявленный с квалифицированным типом принимается как имеющая неквалифицированную версию своего объявленного типа.)
127) если оба типа функций являются ‘старыми’, типы параметров не сравниваются.
правила определения совместимости двух типов описаны в разделе 6.2.7, и я не буду цитировать их здесь, поскольку они довольно длинные, но вы можете прочитать их на проект стандарта C99 (PDF).
в соответствующее правило приведено в пункте 2 раздела 6.7.5.1:
для совместимости двух типов указателей оба должны иметь одинаковую квалификацию и оба должны быть указателями на совместимые типы.
следовательно, с
void*
не совместим сstruct my_struct*
указатель на функцию типаvoid (*)(void*)
не совместим с указателем функции типаvoid (*)(struct my_struct*)
, Так что это приведение указателей функций является технически неопределенным поведением.на практике, хотя, в некоторых случаях вы можете спокойно уйти с указателями функций кастинга. В соглашении о вызове x86 аргументы помещаются в стек, и все указатели имеют одинаковый размер (4 байта в x86 или 8 байт в x86_64). Вызов указателя функции сводится к нажатию аргументов в стеке и выполнению косвенного перехода к цели указателя функции, и, очевидно, нет понятия типов на уровне машинного кода.
вещи, которые вы определенно не могу делать:
- приведение между указателями функций различных соглашений о вызовах. Вы будете испортить стек и в лучшем случае, аварии, в худшем случае, добиться успеха молча с огромной зияющей дырой безопасности. В программировании Windows вы часто передаете указатели функций. Win32 ожидает, что все функции обратного вызова будут использовать
stdcall
соглашение о вызове (что макросCALLBACK
,PASCAL
иWINAPI
все расширяться). Если вы передаете указатель на функцию, которая использует стандартное соглашение о вызове c (cdecl
), плохо будет результат.- в C++ приведение между указателями функций-членов класса и указателями обычных функций. Это часто отключает новичков C++. Функции-члены класса имеют скрытый
this
параметр, и если вы приведете функцию-член к обычной функции, нетthis
объект для использования, и снова, много плохого приведет.еще одна плохая идея, которая иногда может работать, но также неопределенное поведение:
- отливки между указателями функций и обычными указателями (например, приведение a
void (*)(void)
доvoid*
). Указатели функций не обязательно имеют тот же размер, что и обычные указатели, поскольку на некоторых архитектурах они могут содержать дополнительную контекстную информацию. Это вероятно, будет работать нормально на x86, но помните, что это неопределенное поведение.
Я спросил об этой точно такой же проблеме относительно некоторого кода в GLib недавно. (Глеб-это базовая библиотека для проекта GNOME и написана на C.) мне сказали, что все слоты-Н-'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, если это действительно необходимо, и в этом случае вы были бы рады, что это так.
у вас есть совместимый тип функции, если тип возвращаемого значения и типы параметров совместимы - в основном (это сложнее на самом деле :)). Совместимость такая же, как и "тот же тип", просто более слабая, чтобы позволить иметь разные типы, но все же иметь некоторую форму выражения "эти типы почти одинаковы". Например, в C89 две структуры были совместимы, если они были идентичны, но только их имя было другим. C99, похоже, изменил это. Цитирование из в обоснование документ (настоятельно рекомендуется к прочтению, кстати!):
что сказал-Да строго это неопределенное поведение, потому что ваша функция do_stuff или кто-то другой вызовет вашу функцию с указателем функции, имеющим
void*
как параметр, но ваша функция имеет несовместимый параметр. Но тем не менее, я ожидаю, что все компиляторы будут компилировать и запускать его без стонов. Но вы можете сделать чище, имея другую функцию, принимаяvoid*
(и регистрируя это как функцию обратного вызова) , которая просто вызовет ваш фактический тогда действуй.
поскольку код C компилируется в инструкции, которые вообще не заботятся о типах указателей, вполне нормально использовать код, который вы упоминаете. Вы столкнетесь с проблемами, когда вы запустите do_stuff с вашей функцией обратного вызова и указателем на что-то еще, чем my_struct structure в качестве аргумента.
Я надеюсь, что смогу сделать это яснее, показав, что не будет работать:
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++, они нажимают определенные элементы в стеке, переходят в новое местоположение кода, выполняют, а затем открывают стек при возврате. Если указатели на функции описывают функции с одинаковым типом возвращаемого значения и одинаковым числом/размером аргументов, вы должны быть в порядке.
таким образом, я думаю, вы должны быть в состоянии сделать это безопасно.
указатели Void совместимы с другими типами указателей. Это основа того, как malloc и функции mem (
memcpy
,memcmp
) работы. Как правило, в C (а не C++)NULL
- это макрос, определенный как((void *)0)
.посмотрите на 6.3.2.3 (пункт 1) в C99:
указатель на void может быть преобразован в или из указателя на любой неполный или тип объекта