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

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

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

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
17
0
23 175
7
Перейти к ответу Данный вопрос помечен как решенный

Ответы 7

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

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

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

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

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

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

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

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

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

BS ISO / IEC 9899: 1999 6.3.2.3 параграф 8 И хотя вы не можете сравнивать данные и типы указателей функций, 0, (void *) 0 и NULL могут быть законно преобразованы в тип указателя функции и привести к соответствующему нулю. указатель этого типа.

CB Bailey 11.10.2008 15:20

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

Ответ принят как подходящий

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

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

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

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

Редактировать

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

766 A pointer to a function of one type may be converted to a pointer to a function of another type and back again;
767 the result shall compare equal to the original pointer.
768 If a converted pointer is used to call a function whose type is not compatible with the pointed-to type, the behavior is undefined.

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

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

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

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

Кстати, уловка do {} while (0) заключается в том, чтобы избежать неожиданного поведения, если вы напишете что-то вроде: if (foo) CALL_MAYBE (func, arg); else {do_something;} Работает только как утверждение; если вам это нужно как выражение, GCC поддерживает выражения операторов: ({if (func) ...; 0;})

ephemient 10.10.2008 00:47

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

CB Bailey 11.10.2008 15:03

Неправильный начальный абзац. Приведение указателя на функцию - это нормально. Вызов функции через указатель функции неправильного типа не определен.

M.M 14.06.2018 08:28

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

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

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

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

Уродливо, черт?

Есть NULL для работы указателей, и это NULL. Это также значение, которым указатели функций инициализируются 0 или внутри структуры с {0}. Проверьте 6.5.9 для сравнения указателей с NULL и 6.5.15.1 для назначения указателей на NULL, где указано pointer и было бы указано pointer to object, если бы он не включал указатели на функции.

u0b34a0f6ae 13.11.2011 03:18

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