Приведение указателя на функцию другого типа - PullRequest
75 голосов
/ 18 февраля 2009

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

113 голосов
/ 18 февраля 2009

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

Вещи, которые вы определенно не можете делать:

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

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

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

Я недавно спросил об этой же проблеме, касающейся некоторого кода в GLib. (GLib - это базовая библиотека для проекта GNOME, написанная на C.) Мне сказали, что от этого зависит вся структура slots'n'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 загружаются, это означает, что хитрость безопасна.

Более важный вопрос для исследования: может ли кто-нибудь найти платформу / компилятор / компоновщик / загрузчик, где этот трюк не работает? Основные очки брауни за это. Бьюсь об заклад, есть некоторые встроенные процессоры / системы, которым это не нравится. Однако для настольных компьютеров (и, вероятно, для мобильных устройств / планшетов) этот прием, вероятно, все еще работает.

7 голосов
/ 18 февраля 2009

Дело не в том, можете ли вы. Тривиальное решение

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, только если он действительно нужен, и в этом случае вы будете рады, что он это сделал.

5 голосов
/ 18 февраля 2009

У вас есть совместимый тип функции, если возвращаемый тип и типы параметров совместимы - в основном (в реальности это сложнее :)). Совместимость такая же, как у «одного типа», только более слабая, что позволяет иметь разные типы, но все же имеет некоторую форму выражения «эти типы почти одинаковы» В C89, например, две структуры были совместимы, если они были идентичны, но только их имя было другим. C99, похоже, изменил это. Цитата из обоснования документа (настоятельно рекомендуется к прочтению, кстати!):

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

Тем не менее - да, строго говоря, это неопределенное поведение, потому что ваша функция do_stuff или кто-то другой будет вызывать вашу функцию с указателем функции, имеющим void* в качестве параметра, но ваша функция имеет несовместимый параметр. Но тем не менее, я ожидаю, что все компиляторы скомпилируют и запустят его без стонов. Но вы можете сделать это чище, если бы другая функция взяла void* (и зарегистрировала это как функцию обратного вызова), которая просто вызовет вашу действительную функцию.

3 голосов
/ 18 февраля 2009

Поскольку код 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

По сути, вы можете приводить указатели к тому, что вам нравится, если данные продолжают иметь смысл во время выполнения.

0 голосов
/ 19 января 2011

Пустые указатели совместимы с другими типами указателей. Это основа работы функций malloc и mem (memcpy, memcmp). Обычно в C (а не в C ++) NULL - это макрос, определяемый как ((void *)0).

Посмотрите на 6.3.2.3 (пункт 1) в C99:

Указатель на void может быть преобразован в или из указателя на любой неполный или тип объекта

0 голосов
/ 18 февраля 2009

Если вы думаете о том, как вызовы функций работают в C / C ++, они помещают определенные элементы в стек, переходят к новому расположению кода, выполняют, а затем выталкивают стек по возвращении. Если ваши указатели на функции описывают функции с одинаковым типом возвращаемого значения и одинаковым числом / размером аргументов, все будет в порядке.

Таким образом, я думаю, вы должны быть в состоянии сделать это безопасно.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...