В своем коде я пытаюсь использовать фиктивные объекты для реализации модульности в C.
На данный момент я указываю важную функцию, полезную для каждого объекта, с помощью указателей функций, таких как деструкторы, toString
, equals
следующим образом:
typedef void (*destructor)(const void* obj);
typedef void (*to_string)(void* obj, int bufferSize, const char* buffer);
typedef bool (*equals)(void* obj, const void* context);
Затем в моей кодовой базе я использую указатель на функцию, совместимый с заданным typedef
, для абстрактной обработки объектов, например:
struct Foo {
int a;
} Foo;
void destroyFoo1(const Foo* p) {
free((void*)p);
}
int main() {
//...
Foo* object_to_remove_from_heap = //instance of foo
destructor d = destroyFoo1;
//somewhere else
d(object_to_remove_from_heap, context);
}
Код компилируется и обычно генерирует только предупреждение (первым параметром деструктора должен быть const void*
, но вместо этого он const Foo*
).
Однако,
так как я включил -Werror
, «недопустимое приведение указателя» рассматривается как ошибка.
Чтобы решить эту проблему, мне нужно привести указатель функции следующим образом:
destructor d = (destructor)destroyFoo1;
Я знаю, что по стандарту const void*
и const Foo*
могут иметь разный размер памяти, но я предполагаю, что платформа, на которой развернут код, const void*
и const Foo*
размещены в одном и том же пространстве памяти и имеют одинаковый размер. В общем, я предполагаю, что приведение указателя на функцию, при котором хотя бы один аргумент указателя изменяется на какой-либо другой указатель, является безопасным приведением.
Это все хорошо, но подход показывает свою слабость, когда, например, мне нужно изменить подпись типа destructor
, например, добавив новый параметр const void* context
. Теперь интересное предупреждение отключено, а количество параметров в вызове указателя функции не совпадает:
//now destructor is
typedef void (*destructor)(const void* obj, const void* context);
void destroyFoo1(const Foo* p) {
free((void*)p);
}
destructor d = (destructor)destroyFoo1; //SILCENCED ERROR!!destroyFoo1 has invalid parameters number!!!!
//somewhere else
d(object_to_remove_from_heap, context); //may mess the stack
Мой вопрос: есть ли способ проверить, действительно ли указатель функции можно безопасно преобразовать в другой (и сгенерировать ошибку компиляции, если нет)?, что-то вроде:
destructor d = CHECK_IF_FUNCTION_RETURNS_VOID_AND_REQUIRE_2_VOID_POINTERS(destroyFoo1);
Что-то, что если мы проходим destroyFoo1
, то все нормально, но если мы проходим destroyFoo2
, компилятор жалуется.
Ниже код, который обобщает проблему
typedef void (*destructor)(const void* obj, const void* context);
typedef struct Foo {
int a;
} Foo;
void destroyFoo1(const Foo* p, const void* context) {
free((void*)p);
if (*((int*)context) == 0) {
printf("hello world\n");
}
}
void destroyFoo2(const Foo* p) {
free((void*)p);
}
int main() {
//this is(in my case) safe
destructor destructor = (destructor) destroyFoo1;
//this is really a severe error!
//destructor destructor = (destructor) destroyFoo2;
Foo* a = (Foo*) malloc(sizeof(Foo));
a->a = 3;
int context = 5;
if (a != NULL) {
//call a destructor: if destructor is destroyFoo2 this is a SEVERE ERROR!
//calling a function accepting a single parameter with 2 parameters!
destructor(a, &context);
}
}
Спасибо за любой ответ
Попробуйте -Wbad-function-cast
вариант
@NellieDanielyan Я посмотрел на вопрос, но не понимаю, как это решит проблему. Насколько я понял, цель макроса CALL_MAYBE
состоит в том, чтобы автоматически проверять нуль указателя на функцию, а не обрабатывать правильное/неправильное количество параметров.
@NellieDanielyan что касается -Wbad-function-cast
: я попробовал MWE, который я предоставил, и он не выдает ошибку (выдается предупреждение -Wincompatible-pointer-types
)
Вы можете удалить приведение и использовать директивы прагмы, чтобы отключить эту конкретную ошибку для этого файла.
Не будет ли это действительно небезопасно? Очень возможно, что мне все еще нужно предупреждение incompatible-pointer-types
в том же файле для совершенно не связанного кода (например, float* b = 5; int* a = b
).
вы все еще можете использовать его как предупреждение вместо ошибки, оставив -Werror включенным для остальных. #pragma GCC diagnostic warning "Wincompatible-pointer-types"
Прошло некоторое время, но не должен ли код для назначения указателя функции быть:
//this is okay
destructor destructor1 = &destructorFoo1;
//this should throw a compilation error!
destructor destructor2 = &destructorFoo2;
Обновлено:
Ладно, я ушел и посмотрел на это поближе.
Если я изменю объявление указателя функции на использование const Foo* p
, а не const void* obj
, чтобы мы не полагались на приведение, чтобы скрыть несовместимость между void*
и Foo*
, я получаю предупреждение с настройками компилятора по умолчанию.
Затем, приведя destroyFoo2 к (деструктору), вы затем скроете это предупреждение, заставив компилятор рассматривать функцию как этот тип.
Я предполагаю, что это подчеркивает подводные камни кастинга.
Я проверил это, используя следующий код:
typedef struct Foo
{
int a;
} Foo;
typedef void (*destructor)(const Foo* p, const void* context);
void destroyFoo1(const Foo* p, const void* context);
void destroyFoo1(const Foo* p, const void* context)
{
free((void*)p);
if (*((int*)context) == 0) {
printf("hello world\n");
}
}
void destroyFoo2(const Foo* p);
void destroyFoo2(const Foo* p)
{
free((void*)p);
}
int main(void)
{
//this is okay
destructor destructor1 = destroyFoo1;
//this triggers a warning
destructor destructor2 = destroyFoo2;
//This doesn't generate a warning
destructor destructor3 = (destructor)destroyFoo2;
}
Я не думаю, что это имеет значение: насколько я знаю, они уже являются указателями функций. Просто чтобы быть уверенным, я только что попробовал пример (с valgrind), и он ничего не меняет. И даже если это что-то изменило, к сожалению, это не решило проблему.
в C имя функции «распадается» до указателя на эту функцию. Следовательно, его можно присвоить переменной правильного типа функции без необходимости использования оператора адреса.
После РЕДАКТИРОВКИ: я думаю, вы неправильно поняли вопрос (извините за неясность): проблема не в удалении предупреждения (само приведение может решить проблему), а в том, чтобы определить, безопасно ли приведение или оно приведет к тяжелый УБ. с вашим destructor3
вы не решаете проблему, потому что после назначения destructor3
программа перейдет к destructor3(a, &context);
: там мы попытаемся вызвать destructor3
по 2 параметрам, в то время как destroyFoo2
имеет только 2 из них (жесткий UB происходит без компилятора, изменяющего вас!) .
Изменить тип первого параметра destructor
также нельзя: хотя Foo
— это всего лишь пример, в кодовой базе есть несколько разных структур (Bar
, Baz
и так далее). У каждого из них есть свой деструктор. Следовательно, единственным возможным типом первого параметра destructor
является void*
Хорошо. Возможно, я неправильно понял. Я не думаю, что существует безопасный способ автоматической проверки того, безопасно ли выполнять приведение или не использовать компилятор, поскольку приведение обходит проверку типов, которая в первую очередь выполняется с помощью определения типа.
Хорошо, я думаю, что понял это, но это не так просто.
Прежде всего, проблема заключается в том, что CHECK_IF_FUNCTION_RETURNS_VOID_AND_REQUIRE_2_VOID_POINTERS
необходимо сравнивать во время компиляции 2 подписи: входную (данную из указателя входной функции, например, destroyFoo1
) и базовую (то есть подпись типа destructor
): если мы реализуем метод это делает это, мы можем проверить, являются ли 2 подписи «совместимыми» или нет.
Мы делаем это, используя препроцессор C. Основная идея заключается в том, что для каждой функции, которую мы хотели бы использовать в качестве destructor
, определен макрос. CHECK_IF_FUNCTION_RETURNS_VOID_AND_REQUIRE_2_VOID_POINTERS
также будет макросом, который просто генерирует имя макроса на основе сигнатуры типа destructor
: если имя макроса, сгенерированное в CHECK_IF_FUNCTION_RETURNS_VOID_AND_REQUIRE_2_VOID_POINTERS
, существует, мы предполагаем, что указатель функции совместим с destructor
, и мы приводим к нему. В противном случае мы выдаем ошибку компиляции. Поскольку нам нужно определение макроса для каждой функции, которую мы хотим использовать в качестве деструктора, это может быть дорогостоящим решением в огромных кодовых базах.
Примечание. Реализация зависит от GCC (в ней используются варианты ##
и _Pragma
, но я думаю, что ее можно легко портировать и на некоторые другие компиляторы).
Так, например:
#define FUNCTION_POINTER_destructor_void_destroyFoo1_voidConstPtr_voidConstPtr 1
void destroyFoo1(const Foo* p, const void* context);
Значение макроса — это просто постоянное число. Что важно, так это имя макроса, которое имеет четко определенную структуру. Соглашение, которое вы используете, не имеет значения, просто выберите и придерживайтесь одного из них. Здесь я использовал следующее соглашение:
//macro (1)
"FUNCTION_POINTER_" typdefName "_" returnType "_" functionName "_" typeparam1 "_" typeparam2 ...
Теперь мы собираемся определить макрос, который проверяет, совпадают ли две подписи. Чтобы помочь нам, мы используем проект Р99. Мы будем использовать несколько макросов из проекта, поэтому вы можете реализовать такие макросы самостоятельно, если не хотите на них полагаться:
#define CHECK_IF_FUNCTION_RETURNS_VOID_AND_REQUIRE_2_VOID_POINTERS(functionName) \
_ENSURE_FUNCTION_POINTER(1, destructor, void, functionName, voidConstPtr, voidConstPtr)
#define _ENSURE_FUNCTION_POINTER(valueToCheck, castTo, expectedReturnValue, functionName, ...) \
P99_IF_EQ(valueToCheck, _GET_FUNCTION_POINTER_MACRO(castTo, expectedReturnValue, functionName, ## __VA_ARGS__)) \
((castTo)(functionName)) \
(COMPILE_ERROR())
#define COMPILE_ERROR() _Pragma("GCC error \"function pointer casting error!\"")
Входными данными макроса является значение макроса (1) для проверки (т. е. 1
в данном случае значение из макроса функции), typedef
, по которому мы хотим проверить (castTo
), тип возвращаемого значения, который мы ожидаем, что functionName
будет иметь и functionName
, переданный пользователем CHECK_IF_FUNCTION_RETURNS_VOID_AND_REQUIRE_2_VOID_POINTERS
(например, destroyFoo1
или destroyFoo2
). Variadic — это типы каждого параметра. Эти параметры должны быть такими же, как в (1): мы пишем voidConstPtr
, потому что мы не можем использовать const void*
в имени макроса.
_GET_FUNCTION_POINTER_MACRO
генерирует макрос, связанный с подписью, которую мы ожидаем от functionName
:
#define _DEFINE_FUNCTION_POINTER_OP(CONTEXT, INDEX, CURRENT, NEXT) P99_PASTE(CURRENT, NEXT)
#define _DEFINE_FUNCTION_POINTER_FUNC(CONTEXT, CURRENT, INDEX) P99_PASTE(_, CURRENT)
#define _GET_FUNCTION_POINTER_MACRO(functionPointerType, returnValue, functionName, ...) \
P99_PASTE(FUNCTION_POINTER, _, functionPointerType, _, returnValue, _, functionName, P99_FOR(, P99_NARG(__VA_ARGS__), _DEFINE_FUNCTION_POINTER_OP, _DEFINE_FUNCTION_POINTER_FUNC, ## __VA_ARGS__))
//example
_GET_FUNCTION_POINTER_MACRO(destructor, void, destroyFoo2, voidConstPtr, voidConstPtr)
//it generates
FUNCTION_POINTER_destructor_void_destroyFoo2_voidConstPtr_voidConstPtr
Так, например:
#define FUNCTION_POINTER_destructor_void_destroyFoo1_voidConstPtr_voidConstPtr 1
void destroyFoo1(const Foo* p, const void* context)
{
free((void*)p);
if (*((int*)context) == 0) {
printf("hello world\n");
}
}
void destroyFoo2(const Foo* p)
{
free((void*)p);
}
int main(void)
{
//this will work:
//FUNCTION_POINTER_destructor_void_destroyFoo1_voidConstPtr_voidConstPtr
//macro exist and is equal to 1
destructor destructor1 = CHECK_IF_FUNCTION_RETURNS_VOID_AND_REQUIRE_2_VOID_POINTERS(destroyFoo1);
//this raise a compile error:
//FUNCTION_POINTER_destructor_void_destroyFoo2_voidConstPtr_voidConstPtr
//does not exist (or exists but its value is not 1)
destructor destructor2 = CHECK_IF_FUNCTION_RETURNS_VOID_AND_REQUIRE_2_VOID_POINTERS(destroyFoo2);
}
на самом деле voidConstPtr
или даже void
в имени макроса — это просто строки. Все бы работало, даже если бы вы заменили void
на helloWorld
. Они просто следуют условности.
Последнее, что нужно понять, это условие, реализованное P99_IF_EQ
в _ENSURE_FUNCTION_POINTER
: если вывод _GET_FUNCTION_POINTER_MACRO
является существующим макросом, препроцессор автоматически заменит его своим значением, в противном случае имя макроса останется прежним; если макрос заменен на 1
(сгенерированный макрос _GET_FUNCTION_POINTER_MACRO
существует и равен 1), мы будем считать, что это достигается только потому, что макрос, определенный разработчиком (1), и мы предполагаем, что functionName
соответствует destructor
. В противном случае мы выдадим ошибку времени компиляции.
Возможный дубликат Указатель функции приведен к другой подписи