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

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

9 ответов

Решение

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

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

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

Тем не менее, да, строго это неопределенное поведение, потому что ваша функция 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

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

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

      void print_data(void *data)
{
    // ...
}


((void (*)(char *)) &print_data)("hello");

Более чистым способом было бы создание функции typedef.

      typedef void(*t_print_str)(char *);
((t_print_str) &print_data)("hello");

Это неопределенное поведение, как объясняли некоторые другие ответы/комментарии.

Но это будет четко определено, если вы замените на

      void my_callback_function(void *arg)
{
   struct my_struct* p = arg;
   ... // use p
}

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

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

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

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

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

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

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

Другие вопросы по тегам