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

Asked
Viewd83764

87

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

121

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • Преобразование между указателями функций и обычными указателями (например, преобразование void (*)(void) в void*). Указатели функций не обязательно того же размера, что и обычные указатели, поскольку на некоторых архитектурах они могут содержать дополнительную контекстную информацию. Это, вероятно, будет работать на x86, но помните, что это неопределенное поведение.
  • This answer makes reference to “This will probably work ok on x86…”: Are there any platforms where this will NOT work? Does anyone have experience when this failed? qsort() for C seems like a nice place to cast a function pointer if possible.

    kevinarpe08 декабря 2012, 17:02
  • As the question-asker pointed out, this only disallows an “incompatible type.” It seems like a function type taking a void* should be compatible with a function type taking any other kind of pointer in C, but the question is what the standard says.

    Chuck18 февраля 2009, 03:04
  • The link from @adam now refers to the 2016 edition of the POSIX standard where the relevant section 2.12.3 has been removed. You can still find it in the 2008 edition.

    Martin Trenkmann08 декабря 2016, 01:43
  • @adam, i see you also did your research :) chuck, void* and his struct type* are not compatible. so it’s undefined behavior to call it. but anyway, undefined behavior is not really always all that bad. if the compiler does it fine, why worry about it. but in this case, there exist a clean solution.

    Johannes Schaub - litb18 февраля 2009, 03:10
  • @KCArpe: According to the chart under the heading “Implementations of Member Function Pointers” in this article, the 16-bit OpenWatcom compiler sometimes uses a larger function pointer type (4 bytes) than data pointer type (2 bytes) in certain configurations. However, POSIX-conforming systems must use the same representation for void* as for function pointer types, see the spec.

    Adam Rosenfield09 декабря 2012, 05:42
  • Isn’t the whole point of void* is that they are compatible with any other pointer? There should be no problem casting a struct my_struct* to a void*, in fact you shouldn’t even have to cast, the compiler should just accept it. For instance, if you pass a struct my_struct* to a function that takes a void*, no casting required. What am I missing here that makes these incompatible?

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

  • The problem is this isn’t a general solution. It needs to be done on a case by case basis with knowledge of the function. If you already have a function of the wrong type, you are stuck.

    BeeOnRope24 октября 2017, 02:55
0

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

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

  • Compilers also can pass arguments in registers. And it’s not unheard of to use different registers for floats, ints or pointers.

    MSalters18 февраля 2009, 13:24
  • you’re only safe as long as struct-pointers and void-pointers have compatible bit-representations; that’s not guaranteed to be the case

    Christoph18 февраля 2009, 10:49
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
 

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

6

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

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

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