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

89

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

Майк Веллер
источник
1
Я немного новичок, но что означает " указатель на функцию void ( ) (void )"? Это указатель на функцию, которая принимает void * в качестве аргумента и возвращает void
Digital Gal
2
@Myke: void (*func)(void *)означает, что funcэто указатель на функцию с сигнатурой типа, например void foo(void *arg). Так что да, ты прав.
mk12

Ответы:

122

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

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

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

Раздел 6.3.2.3, параграф 8 гласит:

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

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

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

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

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

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

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

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

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

Следовательно, поскольку a void* несовместимо с a 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, но помните, что это неопределенное поведение.
Адам Розенфилд
источник
18
Разве не все дело в void*том, что они совместимы с любым другим указателем? Не должно возникнуть проблем с приведением a struct my_struct*к a void*, на самом деле вам даже не нужно приводить , компилятор должен просто принять это. Например, если вы передаете a struct my_struct*функции, которая принимает a void*, приведение типов не требуется. Что мне здесь не хватает, что делает их несовместимыми?
brianmearns
2
В этом ответе есть ссылка на «Это, вероятно, будет нормально работать на x86 ...»: есть ли платформы, на которых это НЕ будет работать? У кого-нибудь есть опыт, когда это не удалось? qsort () для C кажется хорошим местом для приведения указателя на функцию, если это возможно.
kevinarpe
4
@KCArpe: Согласно диаграмме под заголовком «Реализации указателей функций-членов» в этой статье , 16-разрядный компилятор OpenWatcom иногда использует больший тип указателя функции (4 байта), чем тип указателя данных (2 байта) в определенных конфигурациях. Однако POSIX-совместимые системы должны использовать то же представление, void*что и для типов указателей на функции, см. Спецификацию .
Адам Розенфилд
3
Ссылка от @adam теперь относится к версии стандарта POSIX 2016 г., из которой удален соответствующий раздел 2.12.3. Вы все еще можете найти его в издании 2008 года .
Мартин Тренкманн
6
@brianmearns Нет, void *"совместим" с любым другим (нефункциональным) указателем только очень точно определенными способами (которые не связаны с тем, что стандарт C означает словом "совместимый" в данном случае). C позволяет a void *быть больше или меньше, чем a struct my_struct *, или иметь биты в другом порядке или отрицать, или что-то еще. Так void f(void *)и void f(struct my_struct *)может быть ABI-несовместимым . C при необходимости преобразует сами указатели за вас, но не может, а иногда и не может преобразовать указанную функцию, чтобы она могла принимать другой тип аргумента.
mtraceur
32

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

Поразмыслив глубже после написания этого ответа, я не удивлюсь, если в коде компиляторов C используется тот же трюк. А поскольку (большинство / все?) Современные компиляторы C загружаются, это означает, что трюк безопасен.

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

Kevinarpe
источник
10
Место, где это определенно не работает, - это компилятор Emscripten LLVM to Javascript. Подробности см. На github.com/kripken/emscripten/wiki/Asm-pointer-casts .
Бен Лингс,
2
Обновленная ссылка на Emscripten .
ysdx
4
Опубликованная ссылка @BenLings в ближайшее время не работает. Он официально переехал на kripken.github.io/emscripten-site/docs/porting/guidelines/…
Alex Reinking
9

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

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

MSalters
источник
Проблема в том, что это не общее решение. Это необходимо делать в индивидуальном порядке, зная функцию. Если у вас уже есть функция неправильного типа, вы застряли.
BeeOnRope
Все компиляторы, с my_callback_helperкоторыми я это тестировал, будут генерировать код , если он не всегда встроен. В этом определенно нет необходимости, так как единственное, что он обычно делает, - это jmp my_callback_function. Компилятор, вероятно, хочет удостовериться, что адреса для функций разные, но, к сожалению, он делает это, даже когда функция помечена C99 inline(т.е. «не заботится об адресе»).
yyny
Я не уверен, что это правильно. Другой комментарий из другого ответа выше (от @mtraceur) говорит, что a void *может быть даже другого размера, чем a struct *(я думаю, что это неправильно, потому что в противном случае он mallocбыл бы сломан, но этот комментарий имеет 5 голосов, поэтому я отдаю ему должное. Если @mtraceur прав, написанное вами решение не будет правильным.
cesss
@cesss: Совершенно неважно, если размер другой. Преобразование в и из void*все еще должно работать. Короче говоря, void*может быть больше бит, но если вы приведете struct*к void*этим дополнительным битам, они могут быть нулями, а при обратном приведении эти нули можно просто отбросить снова.
MSalters
@MSalters: Я действительно не знал, что void *(теоретически) может быть настолько отличным от struct *. Я реализую vtable в C, и я использую thisуказатель C ++ в качестве первого аргумента виртуальных функций. Очевидно, это thisдолжен быть указатель на «текущую» (производную) структуру. Итак, виртуальным функциям нужны разные прототипы в зависимости от структуры, в которой они реализованы. Я думал, что использование void *thisаргумента исправит все, но теперь я узнал, что это неопределенное поведение ...
cesss
6

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

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

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

Йоханнес Шауб - litb
источник
4

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

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

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

Стив Роу
источник
2
вы в безопасности, только если struct-поинтеры и void-поинтеры имеют совместимые битовые представления; это не гарантировано,
Кристоф
1
Компиляторы также могут передавать аргументы в регистрах. И нет ничего удивительного в использовании разных регистров для чисел с плавающей запятой, целых чисел или указателей.
MSalters
0

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

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

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

xslogic
источник
Это противоречит ответу Адама Розенфилда , см. Последний абзац и комментарии
пользователь
1
Это явно неверный ответ. Любой указатель может быть преобразован в указатель void, кроме указателей на функции.
marton78 05