Хорошо, думаю, я понял это, но это не так просто.
Прежде всего, проблема заключается в том, что CHECK_IF_FUNCTION_RETURNS_VOID_AND_REQUIRE_2_VOID_POINTERS
необходимо сравнить во время компиляции 2 сигнатуры: входную (полученную из указателя входной функции, например, destroyFoo1
) и базовую (т.е. сигнатуру * 1005). * type): если мы реализуем метод, который делает это, мы можем проверить, являются ли 2 подписи «совместимыми» или нет.
Мы делаем это, используя препроцессор Си. Основная идея состоит в том, что каждая функция, которую мы хотели бы использовать как destructor
, имеет определенный макрос. CHECK_IF_FUNCTION_RETURNS_VOID_AND_REQUIRE_2_VOID_POINTERS
также будет макросом, который просто генерирует имя макроса на основе сигнатуры типа destructor
: если имя макроса, сгенерированное в CHECK_IF_FUNCTION_RETURNS_VOID_AND_REQUIRE_2_VOID_POINTERS
, существует, то мы предполагаем, что functionPointer совместим с 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 ...
Теперь мы собираемся определить макрос, который проверяет, совпадают ли 2 подписи. Чтобы помочь нам, мы используем P99 проект . Мы собираемся использовать несколько макросов из проекта, поэтому вы можете реализовать такие макросы самостоятельно, если не хотите на них полагаться:
#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
. В противном случае мы выдадим ошибку времени компиляции.