Приведение указателя функции к другому типу

предположим, у меня есть функция, которая принимает 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).
6.3.2.3, пункт 8 гласит:

указатель на функцию одного типа может быть преобразован в указатель на функцию другого тип и обратно; результат должен сравниваться равный исходному указатель. Если преобразованный указатель используется для вызова функции, тип которой не совместим с типом, указываемым на, поведение не определено.

другими словами, вы можете привести указатель функции к другому типу указателя функции, вернуть его еще раз, и позвоните, и все получится.

определение совместимость несколько усложняется. Его можно найти в разделе 6.7.5.3, пункт 15:

для совместимости двух типов функций оба должны указывать совместимые типы возврата127.

кроме того, списки типов параметров, если оба присутствуют, должны согласовываться в количестве параметры и в использовании Терминатора с многоточием; соответствующий параметры имеют совместимые типы. Если один тип имеет список типов параметров, а другой тип задается Декларатор функций, не являющийся частью определения функции и содержащий пустое список идентификаторов, список параметров не должен иметь многоточия и тип каждого параметр должен быть совместим с типом, который является результатом применения продвижение аргументов по умолчанию. Если один тип имеет список типов параметров, а другой тип указанные определением функции, содержащим (возможно, пустой) список идентификаторов, оба должны согласуйте количество параметров, и тип каждого параметра прототипа будет совместимость с типом, который является результатом применения аргумента по умолчанию акции типа соответствующий идентификатор. (При определении типа совместимость и составного типа, каждый параметр, объявленный в функции или массива тип принимается как имеющий скорректированный тип и каждый параметр, объявленный с квалифицированным типом принимается как имеющая неквалифицированную версию своего заявленного типа.)

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). Вызов указателя функции сводится к нажатию аргументов в стеке и выполнению косвенного перехода к цели указателя функции, и, очевидно, нет понятия типов на уровне машинного кода.

вещи, которые вы определенно не могу do:

  • приведение между указателями функций различных соглашений о вызовах. Вы испортите стек и в лучшем случае, аварии, в худшем случае, добиться успеха молча с огромной зияющей дырой безопасности. В программировании Windows вы часто передаете указатели функций. Win32 ожидает, что все функции обратного вызова будут использовать stdcall соглашение о вызове (что макрос CALLBACK, PASCAL и WINAPI все расширяться). Если вы передадите указатель функции, который использует стандартное соглашение о вызовах C (cdecl), результатом будет плохое.
  • в C++, приведение между указателями функций-членов класса и указателями регулярных функций. Это часто сбивает новичков с++. Функции-члены класса имеют скрытый this параметр, и если вы приведете функцию-член к регулярной функции, нет this объект для использования, и снова, много плохого приведет.

еще одна плохая идея, которая может иногда работать, но и неопределенному поведению:

  • отливки между указателями функций и обычными указателями (например, литье void (*)(void) до void*). Указатели функций не обязательно имеют тот же размер, что и обычные указатели, поскольку в некоторых архитектурах они могут содержать дополнительную контекстную информацию. Это вероятно, будет работать нормально на x86, но помните, что это неопределенное поведение.

Я спросил об этой же самой проблеме относительно некоторого кода в GLib недавно. (Глеб-это базовая библиотека для проекта GNOME и написана на C.) мне сказали, что все слоты-Н-'signals рамки зависит от него.

на протяжении всего кода существует множество экземпляров литья от типа (1) до (2):

  1. typedef int (*CompareFunc) (const void *a, const void *b)
  2. 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). Черт бы побрал эти стандарты.

Я провел некоторое время, думая об этих ответах. Вот мой вывод:

  1. если вы пишете библиотеку обратного вызова, это может быть нормально. Будьте бдительны, используйте на свой страх и риск.
  2. иначе, не делай этого.

думая глубже после написания этого ответа, я не удивлюсь, если код для компиляторов 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 две структуры были совместимы, если в остальном они были идентичны, но только их имя было другим. С99, кажется, изменились, что. Цитата из в обоснование документ (настоятельно рекомендуется к прочтению, кстати!):

объявления типа структуры, объединения или перечисления в двух разных единицах перевода формально не объявляют один и тот же тип, даже если текст этих объявлений происходит из одного и того же файла include, так как сами единицы перевода непересекаются. Таким образом, стандарт определяет дополнительные правила совместимости для таких типов, так что если два таких объявления достаточно похожи, они совместимый.

что сказал-Да, строго это неопределенное поведение, потому что ваша функция 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 может быть преобразован в или из указателя на любой неполный или тип объекта