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

StackOverflow https://stackoverflow.com/questions/559581

  •  05-09-2019
  •  | 
  •  

Вопрос

Допустим, у меня есть функция, которая принимает 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, то если вы приведете указатель на функцию к указателю на функцию другого типа, а затем вызовете его, то это неопределенное поведение.См. Приложение J.2 (информативное):

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

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

Пункт 8 раздела 6.3.2.3 гласит:

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

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

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

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

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

127) Если оба типа функций имеют ‘старый стиль’, типы параметров не сравниваются.

Правила определения совместимости двух типов описаны в разделе 6.2.7, и я не буду цитировать их здесь, поскольку они довольно пространные, но вы можете прочитать их на проект стандарта C99 (PDF).

Соответствующее правило здесь содержится в разделе 6.7.5.1, параграф 2:

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

Следовательно, поскольку a 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) к a void*).Указатели на функции не обязательно имеют тот же размер, что и обычные указатели, поскольку на некоторых архитектурах они могут содержать дополнительную контекстуальную информацию.Вероятно, это будет нормально работать на x86, но помните, что это неопределенное поведение.

Другие советы

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

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

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

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

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

Тем не менее - да, строго говоря, это неопределенное поведение, потому что ваша функция do_stuff или кто-то другой вызовет вашу функцию с указателем на функцию, имеющим void* как параметр, но ваша функция имеет несовместимый параметр.Но, тем не менее, я ожидаю, что все компиляторы скомпилируют и запустят его без стонов.Но вы можете сделать чище, используя другую функцию, выполняющую 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 ++, они помещают определенные элементы в стек, переходят к новому расположению кода, выполняются, а затем извлекают стек при возврате.Если ваши указатели на функции описывают функции с одинаковым типом возвращаемого значения и одинаковым количеством / размером аргументов, все должно быть в порядке.

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top