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


допустим, у меня есть функция, которая принимает 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 66

7 ответов:

что касается стандарта C, если вы приведете указатель функции к указателю функции другого типа, а затем вызовете его, это неопределенное поведение. См. приложение J. 2 (информативное):

поведение не определено в следующих обстоятельствах:

  • указатель используется для вызова функции, тип которой не совместим с указанным тип (6.3.2.3).
6.3.2.3, пункт 8 гласит:

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

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

определение совместимость несколько усложняется. Его можно найти в разделе 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):

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