Указатель функций, отлитый от другой подписи

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

EDIT: благодаря всем ответам, я узнал теперь много о вызывать соглашения, после немного дальнейшего чтения. И теперь гораздо лучше понять, что происходит под капотом.

Ответ 1

По спецификации C, приведение указателя функции приводит к поведению undefined. На самом деле, некоторое время, предварительные версии GCC 4.3 вернут NULL всякий раз, когда вы набрасываете указатель на функцию, совершенно верный спецификации, но они отменили это изменение перед выпуском, потому что оно разбило много программ.

Предполагая, что GCC продолжает делать то, что он делает сейчас, он отлично справится с назначением по умолчанию x86 (и большинством вызовов на большинстве архитектур), но я не буду зависеть от него. Тестирование указателя функции на NULL на каждом call-сайте не намного дороже вызова функции. Если вы действительно хотите, вы можете написать макрос:

#define CALL_MAYBE(func, args...) do {if (func) (func)(## args);} while (0)

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

Изменить

Чарльз Бейли позвал меня на это, поэтому я пошел и посмотрел подробности (вместо того, чтобы полагаться на мою дырявую память). спецификация C говорит

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

и предварительные версии GCC 4.2 (это было решено до 4.3) выполняли следующие правила: приведение указателя функции не приводило к NULL, как я писал, но пытаюсь вызвать функцию через несовместимый тип, т.е.

func = (cb_t)nothing;
func(1);

из вашего примера приведет к abort. Они изменились на поведение 4.1 (разрешить, но предупредить), отчасти потому, что это изменение нарушило OpenSSL, но OpenSSL был исправлен в то же время, и это поведение undefined, которое компилятор может свободно изменять в любое время.

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

Ответ 2

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

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

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

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

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

Ответ 3

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

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

Ответ 4

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

EDIT: это предполагает соглашения о вызовах cdecl, которые обычно являются значениями по умолчанию для C.

Ответ 5

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

Ответ 6

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

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

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

Ответ 7

Приведение указателя функции в NULL явно не поддерживается стандартом C. Вы во власти писателя-компилятора. Он работает нормально во многих компиляторах.

Это одно из величайших раздражений C, что для указателей функций нет эквивалента NULL или void *.

Если вы действительно хотите, чтобы ваш код был пуленепробиваемым, вы можете объявить свои собственные нули, но вам нужен один для каждого типа функции. Например,

void void_int_NULL(int n) { (void)n; abort(); }

а затем вы можете протестировать

if (my_thing->func_a != void_int_NULL) my_thing->func_a(99);

Уродливый, неподходящий?