Указатель на функцию приведен к другой подписи - PullRequest
16 голосов
/ 09 октября 2008

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

13 голосов
/ 10 октября 2008

По спецификации 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 только приводил указатели функций на другие типы функций, получая и возвращая одинаковое количество значений одинакового точного размера, и это (при условии, что вы не имеете дело с плавающей запятой) оказывается безопасным на всех платформах и соглашения о вызовах, о которых я знаю. Однако все остальное потенциально небезопасно.

2 голосов
/ 09 октября 2008

Я подозреваю, что вы получите неопределенное поведение.

Вы можете назначить (при правильном приведении) указатель на функцию другому указателю на функцию с другой сигнатурой, но при вызове ее могут произойти странные вещи.

Ваша nothing() функция не принимает аргументов, для компилятора это может означать, что он может оптимизировать использование стека, поскольку там не будет аргументов. Но здесь вы называете это аргументом, это неожиданная ситуация, которая может привести к сбою.

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

В качестве примечания не следует сравнивать указатель функции с указателем данных (например, NULL), так как указатели могут принадлежать отдельным адресным пространствам. В стандарте C99 есть приложение, которое допускает этот конкретный случай, но я не думаю, что оно широко реализовано. Тем не менее, в архитектуре, где есть только одно адресное пространство, приведение указателя функции к указателю данных или сравнение его с NULL, обычно будет работать.

1 голос
/ 09 октября 2008

Вы рискуете вызвать повреждение стека. Тем не менее, если вы объявите функции со связью extern "C" (и / или __cdecl в зависимости от вашего компилятора), вы можете обойтись без этого. Тогда это будет аналогично тому, как такая функция, как printf(), может принимать переменное число аргументов по усмотрению вызывающего.

Работает ли это в вашей текущей ситуации или нет, может также зависеть от того, какие параметры компилятора вы используете. Если вы используете MSVC, параметры компиляции «отладка против выпуска» могут иметь большое значение.

0 голосов
/ 06 декабря 2008

Преобразование указателя функции в 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);

Ужасно, не так ли?

0 голосов
/ 10 октября 2008

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

Я бы пошел на проверку на NULL, а затем позвонил - я не могу себе представить, что это повлияет на производительность.

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

0 голосов
/ 09 октября 2008

Пока вы можете гарантировать, что вы делаете вызов, используя метод, в котором вызывающий абонент сбалансирует стек, а не вызываемый (__cdecl). Если вы не указали соглашение о вызовах, глобальное соглашение может быть установлено на что-то другое. (__stdcall или __fastcall) И то, и другое может привести к повреждению стека.

0 голосов
/ 09 октября 2008

Все должно быть в порядке. Поскольку вызывающий отвечает за очистку стека после вызова, он не должен оставлять в стеке ничего лишнего. Вызываемый объект (в этом случае ничто ()) в порядке, поскольку он не будет пытаться использовать какие-либо параметры в стеке.

РЕДАКТИРОВАТЬ: здесь подразумеваются соглашения о вызовах cdecl, которые обычно используются по умолчанию для C.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...