Указатели функции каста
Я пишу функцию, которая получает указатель на функцию сравнения и массив MyStructs
и должна сортировать массив в соответствии с функцией сравнения:
void myStructSort(
struct MyStruct *arr,
int size,
int (*comp)(const struct MyStruct *, const struct MyStruct *)) {
qsort(arr, size, sizeof(struct MyStruct), comp);
}
К сожалению, это не скомпилируется, потому что qsort
ожидает, что компаратор получит аргументы void *
, а не const struct MyStruct *
. Я подумал о нескольких плохих решениях и задался вопросом, что такое правильное решение.
Вариант 1
Вставить comp
в int (*)(const void *, const void*)
. Это компилируется, но это поведение undefined (см. этот вопрос SO).
Вариант 2
Создайте глобальную переменную int (*global_comp)(const struct MyStruct *, const struct MyStruct *)
и установите global_comp=comp
внутри myStructSort
. Затем создайте функцию:
int delegatingComp(const void *a, const void *b) {
return globalComp((const struct MyStruct *)a, (const struct MyStruct *)b);
}
И в myStructSort
вызовите qsort(arr, size, sizeof(struct MyStruct), delegatingComp)
. Проблема с этим - нечеткая глобальная переменная.
Вариант 3
Повторить qsort
. Это функционально безопасная, но очень плохая практика.
Есть ли волшебный идеальный четвертый вариант?
Edit
Я не могу изменить API myStructSort
, и я компилирую свой код с помощью gcc c99 -Wall -Wextra -Wvla
.
Ответы
Ответ 1
Вариант 2 нарушает безопасность потоков, поэтому я бы не выбрал этот вариант.
Вариант 3 просто неверен, как вы указываете. Нет причин для повторной реализации quicksort и, возможно, ошибки.
Вариант 1 - UB, но он будет работать на любом компиляторе. Если вы выберете этот вариант, обязательно добавьте комментарий.
Я бы также подумал:
Вариант 4. Перепроектируйте интерфейс myStructSort
, чтобы взять int (*)(const void *, const void*)
или полностью отказаться от него и вызвать qsort
напрямую. В принципе отправьте его обратно в архитект, потому что он сделал плохой выбор дизайна.
Ответ 2
Следующий подход работает только для gcc
. Это часть расширения gnu. далее обратитесь к https://gcc.gnu.org/onlinedocs/gcc-4.8.5/gcc/Nested-Functions.html#Nested-Functions
сначала убедитесь, что прототип qsort
находится в такой форме:
void qsort(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *));
то вы можете:
void myStructSort(
struct MyStruct *arr,
int size,
int (*comp)(const struct MyStruct *, const struct MyStruct *)) {
int comparator(const void * a, const void *b) {
return comp((const struct MyStruct *)a, (const struct MyStruct *)b);
}
qsort(arr, size, sizeof *arr, comparator);
}
Но опять же, поскольку он использует расширение gnu, не ожидайте слишком большой переносимости.
О ВАШЕМ КОММЕНТАРИИ: для современных gcc
стандартом gnu является значение по умолчанию вместо iso. в частности, lastest gcc
должен использовать стандарт gnu11
. более старые используют gnu89
. поэтому я не знаю о ваших параметрах командной строки, но если -std
не установлен, это будет работать.
Ниже приведен пример из info gcc
, на всякий случай, когда ссылка мертва. он показывает закрытое использование вложенной функции:
bar (int *array, int offset, int size)
{
int access (int *array, int index)
{ return array[index + offset]; }
int i;
/* ... */
for (i = 0; i < size; i++)
/* ... */ access (array, i) /* ... */
}
Ответ 3
Если вы используете gcc, то вы можете использовать функцию qsort_r
в glibc с 2.8, что позволяет вам указать функцию компаратора с дополнительным аргументом, предоставленным пользователем:
void qsort_r(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *, void *),
void *arg);
Это, конечно, не переносится, и для этого требуется определить макрос функции:
#define _GNU_SOURCE
(На FreeBSD - и, предположительно, Mac OS X - существует аналогичный, но несовместимый qsort_r
; разница заключается в том, что предоставленный пользователем контекстный аргумент предоставляется в качестве первого аргумента функции сравнения, а не последний аргумент.)
Но если у вас есть это, это позволяет вам избежать глобального в опции 2:
/* This struct avoids the issue of casting a function pointer to
* a void*, which is not guaranteed to work. It might not be
* necessary, but I know of no guarantees.
*/
typedef struct CompContainer {
int (*comp_func)(const struct MyStruct *, const struct MyStruct *);
} CompContainer;
int delegatingComp(const void *a, const void *b, void* comp) {
return ((CompContainer*)comp)->comp_func((const struct MyStruct *)a,
(const struct MyStruct *)b);
}
void myStructSort(
struct MyStruct *arr,
int size,
int (*comp_func)(const struct MyStruct *,
const struct MyStruct *)) {
const CompContainer comp = {comp_func};
qsort_r(arr, size, sizeof(struct MyStruct), delegatingComp, &comp);
}
(Live on ideone)
Ответ 4
Правильный подход заключается в том, чтобы отбрасывать от void const *
до MyStruct const *
в функции сравнения.
Это хорошо определено для первого объекта, потому что указатель, который был передан в функцию сравнения, был создан с помощью от MyStruct const *
до void const *
и отбрасывает указатель на void
обратно в исходный тип (и это действительно единственное, что есть).
Для других членов массива предполагается, что отбрасывание void const *
до char const *
, добавление смещения объекта, сгенерированное путем умножения размера объекта на позицию объекта в массиве и отбрасывание его обратно void const *
даст указатель, который можно отбросить на MyStruct const *
.
Это смелое предположение, но обычно это получается. Там могут быть угловые случаи, когда это не работает, но в целом компиляторы накладывают любые struct foo
на кратное его выравнивание, чтобы гарантировать, что начальные адреса членов массива имеют расстояние sizeof(struct foo)
.
Указатели функции каста обычно небезопасны и их следует избегать, поскольку разные типы данных могут иметь разные представления - например, void *
должен иметь возможность выражать все возможные адреса, поскольку он мог быть преобразован из char *
, в то время как a MyStruct *
гарантированно имеет несколько наименее значимых битов, которые могут быть выровнены, поскольку любой действительный объект будет выровнен - так что вполне возможно, что соглашение вызова для этих типов может быть другим.
Ответ 5
Единственный разумный вариант - перезаписать созданный интерфейс или создать новый.
Я сделал что-то очень похожее на сортировку пузыря на другом ответе.
Короче говоря, с помощью C вы хотите, чтобы ваша функция сортировки имела вид:
void* bubbleSort(void* arr, int (*compareFcn)(void*, void*),
size_t sizeOfElement, size_t numElements)
И ваша функция сравнения будет иметь вид:
int compareFunction(void *a, void *b);