Указатель на функцию приведен к другой подписи
Я использую структуру указателей функций для реализации интерфейса для разных бэкэндов. Сигнатуры очень разные, но возвращаемые значения почти все void, void * или int.
struct my_interface {
void (*func_a)(int i);
void *(*func_b)(const char *bla);
...
int (*func_z)(char foo);
};
Но не обязательно, чтобы бэкенд поддерживал функции для каждой функции интерфейса. Таким образом, у меня есть две возможности, во-первых, перед каждым вызовом проверять, является ли указатель неравным NULL. Мне это не очень нравится из-за читабельности и из-за боязни влияния на производительность (однако я не измерял его). Другой вариант - использовать фиктивную функцию, в редких случаях интерфейсной функции не существует.
Поэтому мне понадобится фиктивная функция для каждой сигнатуры. Интересно, возможно ли иметь только одну для разных возвращаемых значений? И наложить его на данную подпись.
#include <stdio.h>
int nothing(void) {return 0;}
typedef int (*cb_t)(int);
int main(void)
{
cb_t func;
int i;
func = (cb_t) nothing;
i = func(1);
printf("%d\n", i);
return 0;
}
Я тестировал этот код с помощью gcc, и он работает. Но так ли это? Или это может повредить стек или может вызвать другие проблемы?
РЕДАКТИРОВАТЬ: Благодаря всем ответам, я теперь узнал много о соглашениях о вызовах, после некоторого дальнейшего чтения. И теперь я понимаю, что происходит под капотом.
7 ответов
Согласно спецификации C, приведение указателя на функцию приводит к неопределенному поведению. Фактически, в течение некоторого времени предварительные версии GCC 4.3 возвращали NULL всякий раз, когда вы приводили указатель на функцию, что полностью соответствовало спецификации, но они отменяли это изменение перед выпуском, потому что оно ломало множество программ.
Предполагая, что GCC продолжает делать то же, что и сейчас, он будет нормально работать с соглашением о вызовах по умолчанию x86 (и большинством соглашений о вызовах на большинстве архитектур), но я бы не стал зависеть от этого. Тестирование указателя функции на NULL на каждом месте вызова не намного дороже, чем вызов функции. Если вы действительно хотите, вы можете написать макрос:
#define CALL_MAYBE (func, args...) do {if (func) (func) (## args);} while (0)
Или вы могли бы иметь разные фиктивные функции для каждой подписи, но я понимаю, что вы хотели бы избежать этого.
редактировать
Чарльз Бейли позвал меня по этому поводу, поэтому я пошел и посмотрел детали (вместо того, чтобы полагаться на свою дырявую память). Спецификация C говорит
766 Указатель на функцию одного типа может быть преобразован в указатель на функцию другого типа и обратно;
767 результат должен сравниваться равным исходному указателю.
768 Если преобразованный указатель используется для вызова функции, тип которой не совместим с указанным типом, поведение не определено.
и предварительные версии GCC 4.2 (это было решено до 4.3) следовали этим правилам: приведение указателя функции, как я писал, не приводило к NULL, но пыталось вызвать функцию через несовместимый тип, т.е.
func = (cb_t)nothing;
func(1);
из вашего примера, приведет к abort
, Они вернулись к поведению 4.1 (разрешить, но предупредить), отчасти потому, что это изменение нарушило OpenSSL, но OpenSSL тем временем был исправлен, и это неопределенное поведение, которое компилятор может изменять в любое время.
OpenSSL только приводил указатели на функции к другим типам функций, получая и возвращая одинаковое количество значений одинакового точного размера, и это (при условии, что вы не имеете дело с плавающей точкой) безопасно для всех платформ и соглашений о вызовах. знать о. Однако все остальное потенциально небезопасно.
Я подозреваю, что вы получите неопределенное поведение.
Вы можете назначить (при правильном приведении) указатель на функцию другому указателю на функцию с другой сигнатурой, но при ее вызове могут произойти странные вещи.
Ваш nothing()
Функция не принимает аргументов, для компилятора это может означать, что он может оптимизировать использование стека, поскольку там не будет аргументов. Но здесь вы называете это аргументом, это неожиданная ситуация, которая может привести к сбою.
Я не могу найти правильную точку в стандарте, но я помню, он говорит, что вы можете приводить указатели на функции, но когда вы вызываете результирующую функцию, вы должны делать с правильным прототипом, в противном случае поведение не определено.
В качестве примечания, вы не должны сравнивать указатель функции с указателем данных (например, NULL), так как указатели могут принадлежать отдельным адресным пространствам. В стандарте C99 есть приложение, которое допускает этот конкретный случай, но я не думаю, что оно широко реализовано. Тем не менее, в архитектуре, где есть только одно адресное пространство, приведение указателя функции к указателю данных или сравнение его с NULL, обычно работает.
Вы рискуете вызвать повреждение стека. Сказав это, если вы объявите функции с extern "C"
связь (и / или __cdecl
в зависимости от вашего компилятора), вы можете сойти с рук. Это было бы похоже на то, как функция, такая как printf()
может принимать переменное количество аргументов по усмотрению звонящего.
Работает ли это в вашей текущей ситуации или нет, может также зависеть от того, какие параметры компилятора вы используете. Если вы используете MSVC, параметры компиляции отладки и выпуска могут иметь большое значение.
Это не сработает, если вы не используете специфичные для реализации / платформенные вещи для принудительного применения правильного соглашения о вызовах. Для некоторых соглашений о вызовах вызываемая функция отвечает за очистку стека, поэтому они должны знать, что было добавлено.
Я бы пошел на проверку на NULL, а затем позвонил - я не могу себе представить, что это повлияет на производительность.
Компьютеры могут проверять NULL так же быстро, как и все, что они делают.
До тех пор, пока вы можете гарантировать, что вы делаете вызов, используя метод, в котором вызывающий абонент сбалансирует стек, а не вызываемый (__cdecl). Если вы не указали соглашение о вызовах, глобальное соглашение может быть установлено на что-то другое. (__stdcall или __fastcall) И то, и другое может привести к повреждению стека.
Это должно быть хорошо. Поскольку вызывающий отвечает за очистку стека после вызова, он не должен оставлять в стеке ничего лишнего. Вызываемый объект (в этом случае ничего) в порядке, поскольку он не будет пытаться использовать какие-либо параметры в стеке.
РЕДАКТИРОВАТЬ: это предполагает соглашения о вызовах cdecl, который обычно используется по умолчанию для C.
Приведение указателя функции к NULL явно не поддерживается стандартом C. Вы находитесь во власти автора компилятора. Это работает хорошо на многих компиляторах.
Это одно из самых больших неудобств C, что для указателей на функции нет эквивалента NULL или void*.
Если вы действительно хотите, чтобы ваш код был пуленепробиваемым, вы можете объявить свои собственные значения NULL, но вам нужен один для каждого типа функции. Например,
void void_int_NULL(int n) { (void)n; abort(); }
и тогда вы можете проверить
if (my_thing->func_a != void_int_NULL) my_thing->func_a(99);
Уродливый, не так ли?