qsort: Прехвърляне на самата функция за сравнение или на параметрите в тялото на функцията за сравнение?

Има няколко очевидни начина за използване на qsort: cast в сравнението:

int cmp(const void *v1, const void *v2) 
{
    const double *d1 = v1, *d2 = v2;
    ⋮
}

qsort(p, n, sizeof(double), cmp);

или хвърлете компаратора:

int cmp(const double *d1, const double *d2) 
{
    ⋮
}

qsort(p, n, sizeof(double), (int (*)(const void *, const void *))cmp);

Склонен съм да използвам първото, повече от естетически причини, отколкото от нещо друго. Има ли някакви технически причини за предпочитане на едното пред другото?


person jjg    schedule 27.02.2021    source източник


Отговори (3)


Трябва да избягвате последния случай, защото не е валиден.

За да бъдат съвместими два типа функции, връщаните типове трябва да са съвместими и съответните типове параметри трябва да са съвместими. const void * не е съвместим с const double *, следователно типовете функции не са съвместими. Извикването на функция чрез несъвместим тип указател води до недефинирано поведение.

Имайте предвид, че това, че два типа могат да бъдат косвено преобразувани, не означава, че са съвместими. Като вземем примера с const double * и const void *, преобразуването между двата типа може да се извърши без преобразуване, но не е необходимо представянето на двата типа да е едно и също.

Това означава, че начинът, по който const double * се предава на функция, може да е различен от начина, по който const void * се предава на функция. Така че чрез извикване на функция от тип int (*)(const double*, const double*), сякаш има тип int (*)(const void*, const void*), параметрите могат да бъдат предадени по неправилен начин.

Докато x64 и ARM системите обикновено ще използват едно и също представяне за всички типове указатели, може да се измъкнете с първото, но все още няма гаранция за това. Съвременните компилатори често приемат, че недефинирано поведение няма да се случи и извършват оптимизации въз основа на този факт.

Първият случай е правилният метод, тъй като сигнатурата на функцията е съвместима с това, което функцията qsort очаква.

person dbush    schedule 27.02.2021
comment
Интересно, благодаря -- виждате последното използване навсякъде, предположих, че е в съответствие поради тази причина. - person jjg; 27.02.2021
comment
@jjg: Броят на кода на местата, който се вижда, не е индикация за съответствието му с който и да е стандарт или спецификация. - person Eric Postpischil; 27.02.2021
comment
Това е отличен въпрос и отличен отговор. И си струва да го разберете добре, защото въпреки че методът на cast-the-comparator изглежда разумен в началото, ако мислите за кода, който компилаторът ще генерира (или вече го е генерирано) надолу в qsort, за да извикате действително функцията за сравнение, ще видите, че тя извиква функция с два указателя void *, така че това трябва да бъде вашата функция за сравнение. (Ако всички типове указатели на данни са еднакви -- както, разбира се, те са всички на всички популярни машини в наши дни -- грешният код ще работи, но все още е грешен.) - person Steve Summit; 27.02.2021
comment
Ако void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *)); беше void qsort(void *base, size_t nmemb, size_t size, int (*compar)();, подозирам, че и двете форми ще са правилни. тъй като непрототипните указатели на обекти се преобразуват в void * (може би). Важният въпрос е как се декларира compar. - person chux - Reinstate Monica; 27.02.2021
comment
@chux-ReinstateMonica Не мисля така, тъй като преобразуването към void * не е сред промоциите на аргумента по подразбиране. Ето защо указателите, предадени на printf, съответстващи на спецификатора на формат %p, трябва да бъдат изрично прехвърлени на void *. - person dbush; 27.02.2021
comment
@dbush Ах да. необходимо е изрично преобразуване/предаване. - person chux - Reinstate Monica; 27.02.2021
comment
Би било интересно, ако има пример от реалния живот, при който това се проваля или поради необичайни конвенции за извикване или представяне на указател, или поради агресивни оптимизации. - person Nate Eldredge; 27.02.2021
comment
@NateEldredge Въпреки че никога не съм използвал такъв, вярвам, че щеше да се провали на адресирани с думи машини като PR1ME, които имаха 16-битови указатели на думи, но (еквивалента на) 18-битови char и void указатели. Има информация за това в списъка с често задавани въпроси за C. - person Steve Summit; 27.02.2021
comment
@NateEldredge Със сигурност щеше да се провали сравняването на chars на ранните Crays, защото указателите на Cray адресираха 64-битови думи и съдържаха допълнителни вътрешни полета за обработка на char данни, опаковани по 8 байта на дума. - person alephzero; 28.02.2021

Като допълнение, има друга стратегия за извикване на qsort: създайте междинна qsort необходима прототипна функция, която извиква функция за сравнение с разрешен тип.

#include <stdlib.h>
#include <stdio.h>

static int double_cmp(const double *d1, const double *d2)
    { return (*d1 > *d2) - (*d2 > *d1); }

static int double_void_cmp(const void *v1, const void *v2)
    { return double_cmp(v1, v2); }

int main(void) {
    double p[] = { 2.18, 6.28, 3.14, 1.20, 2.72, 0.58, 4.67, 0.0, 1, 1.68 };
    const size_t n = sizeof p / sizeof *p;
    size_t i;
    qsort(p, n, sizeof *p, &double_void_cmp);
    for(i = 0; i < n; i++) printf("%s%.2f", i ? ", " : "", p[i]);
    fputs(".\n", stdout);
    return EXIT_SUCCESS;
}

Въпреки че това има своите проблеми, човек може да използва double_cmp като сравнение за други неща, които не са qsort. Освен това не изисква никакви кастинги или изрични присвоявания, според моето тълкуване на ISO 9899 6.3.2.3,

Указател към void може да бъде преобразуван във или от указател към всеки непълен или тип обект. . . и обратно.

person Neil    schedule 27.02.2021
comment
забележка: програмният жаргон за тази техника е thunk, което означава междинна функция, която прави някои малки корекции, така че несъвместими източник и дестинация да могат да се съберат - person M.M; 02.03.2021
comment
@M.M Мисля, че това ще се нарече обвивка, а не тънк. Thunk е използването на функция или затваряне за спиране на оценката на израз, предаване на кода като данни, за да се добави мързел към нетърпелив език. Това е често срещана техника в строго функционалните езици. Thunks обикновено не приемат аргументи и връщат стойност от посочения тип. - person Mario Carneiro; 04.03.2021

В допълнение към отличния отговор на dbush, трябва да се отбележи, че случаят на алтернативна функция за сравнение с прототип на int cmp(const char *s1, const char *s2), като strcmp, не е толкова ясен като този във въпроса. Стандартът C уточнява, че:

6.2.5 Типове

[...] Указател към void трябва да има същите изисквания за представяне и подравняване като указател към символен тип. По същия начин указателите към квалифицирани или неквалифицирани версии на съвместими типове трябва да имат еднакви изисквания за представяне и подравняване. Всички указатели към структурни типове трябва да имат еднакви изисквания за представяне и подравняване един към друг. Всички указатели към типове обединения трябва да имат еднакви изисквания за представяне и подравняване един към друг. Не е необходимо указателите към други типове да имат същите изисквания за представяне или подравняване.

Така че указателите към функции с прототипи int cmp(const void *v1, const void *v2) и int cmp(const char *v1, const char *v2) са несъвместими, но е малко вероятно последователността на извикване да е различна дори при тези изключително редки цели, където int cmp(const double *v1, const double *v2) би било проблематично (ранни Cray системи и процесори без байтова адресируемост) .


Вие не предоставяте кода за функциите за сравнение: често срещана грешка е просто да се върне разликата в стойностите (*d1 - *d2). Това не работи за стойности с плаваща запетая, нито пък за стойности int, тъй като изваждането може да препълни.

Ето една реализация за нарастващ ред, която работи за всички типове числа:

int cmp(const void *v1, const void *v2) {
    const int *p1 = v1, *p2 = v2;
    return (*p1 > *p2) - (*p1 < *p2);
}

За типове с плаваща запетая може да е необходима специална обработка на NaN стойности:

// sort by increasing values, with NaN after numbers
int cmp(const void *v1, const void *v2) {
    const double *p1 = v1, *p2 = v2;
    if (isnan(*p1)) {
        return isnan(*p2) ? 0 : 1;
    } else
    if (isnan(*p2)) {
        return -1;
    } else {
        return (*p1 > *p2) - (*p1 < *p2);
    }
}
person chqrlie    schedule 27.02.2021
comment
Харесва ми това double сравняване на обработката на NAN UV - като този добър отговор. Първо махнете тези NAN от пътя. - person chux - Reinstate Monica; 27.02.2021
comment
Това няма нищо общо с проблема, зададен във въпроса и трябва да бъде коментар или отделен въпрос. - person pipe; 28.02.2021
comment
@pipe: този отговор е наистина храна за размисъл във връзка с въпроса. OP не е публикувал кода на неговите функции за сравнение, но случайните читатели трябва да се научат как да пишат правилно тези функции за сравнение, извън проблема с прототипирането. - person chqrlie; 28.02.2021
comment
@chqrlie Затова напишете блог за това или задайте въпрос. Храна за размисъл е в най-добрия коментар. - person pipe; 01.03.2021
comment
@pipe: Може да е повече от коментар, но а) няма да работи добре като коментар поради дължината си и включването на код, и б) много ясно предоставя стойност на читателите на тази тема и по този начин помага за изграждането извън приетия отговор. Не виждам причина напр. изтрийте го като не е отговор. Ако има повод за дискретност при прегледа на отговорите, това със сигурност е един такъв случай; премахването му би направило лоша услуга на бъдещите читатели. - person Jeremy Caney; 01.03.2021
comment
@JeremyCaney: тази тема има проблема, че дизайнът на Stack Overflow изрично и умишлено не е табло за съобщения/форум с нишки. Дължините на коментарите са умишлено ограничени, за да се предотврати това объркване. (Вижте съответните мета SO дискусии) - person MSalters; 01.03.2021
comment
@MSalters: Може би. Въпреки че съм маркирал или прегледал над 7500 отговора и съм срещал само два или три „отговора с коментари“, които добавят достатъчно стойност, че не се чувствам комфортно да гласувам за изтриване. По-голямата част са много ясни. - person Jeremy Caney; 01.03.2021