【问题标题】:qsort: Cast the comparator function itself or the parameters in the body of comparator function?qsort:转换比较器函数本身还是比较器函数体中的参数?
【发布时间】:2021-05-29 17:00:26
【问题描述】:

有几种明显的方法可以使用qsort:在比较器中强制转换:

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);

我倾向于使用前者,更多是出于审美原因。是否有任何技术上的原因可以让您选择其中一个而不是另一个?

【问题讨论】:

    标签: c qsort


    【解决方案1】:

    你应该避免后一种情况,因为它是无效的。

    两个函数类型要兼容,返回类型必须兼容,对应的参数类型必须兼容。 const void *const double * 不兼容,因此函数类型不兼容。通过不兼容的指针类型调用函数会导致undefined behavior

    请注意,仅仅因为两种类型可以隐式转换并不意味着它们是兼容。以const double *const void * 为例,两种类型之间的转换无需强制转换即可执行,但两种类型的表示不必相同。

    这意味着const double * 传递给函数的方式可能与const void * 传递给函数的方式不同。因此,通过调用int (*)(const double*, const double*) 类型的函数,就好像它具有int (*)(const void*, const void*) 类型一样,参数可能会以不正确的方式传递。

    虽然 x64 和 ARM 系统通常对所有指针类型使用相同的表示形式,但您可能不使用前者,但仍不能保证这一点。现代编译器通常会假设未定义的行为不会发生,并根据该事实执行优化。

    前一种情况是正确的方法,因为函数的签名与 qsort 函数所期望的兼容。

    【讨论】:

    • @jjg:看到代码的位置数量并不表示其符合任何标准或规范。
    • 这是一个很好的问题和一个很好的答案。并且值得好好理解,因为虽然cast-the-comarator方法一开始看起来是合理的,但如果你考虑一下编译器将要生成的代码(或者已经生成)在qsort 中,要实际调用比较器函数,您会看到它正在调用一个带有两个void * 指针的函数,所以这就是您的比较器函数必须。 (如果所有数据指针类型都是“相同的”——当然它们都在所有流行的机器上——错误的代码会起作用,但它仍然是错误的。)
    • @chux-ReinstateMonica 我不这么认为,因为转换为void * 不属于默认参数提升。这就是为什么传递给与%p 格式说明符对应的printf 的指针必须显式转换为void *
    • @NateEldredge 虽然我从未使用过,但我相信它在诸如 PR1ME 之类的字寻址机器上会失败,它有 16 位字指针但(相当于)18 位charvoid 指针。 C FAQ list 中有一些相关信息。
    • @NateEldredge 在早期的 Cray 上比较 chars 肯定会失败,因为 Cray 指针寻址 64 位字并包含额外的内部字段来处理 char 数据打包 8 个字节到一个字.
    【解决方案2】:

    除了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 系统和缺乏字节寻址能力的 CPU)。


    您没有提供比较函数的代码:简单地返回值的差异是一个常见的错误 (*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);
        }
    }
    

    【讨论】:

    • 我喜欢这个double 比较处理NAN UV - 喜欢这个good answer。先把那些NANs 移开。
    • 这与问题中提出的问题无关,应该是评论或单独的问题。
    • @pipe:这个答案真的是与问题有关的思考。 OP 没有发布他的比较函数的代码,但是普通读者应该学习如何正确编写这些比较函数,而不是原型问题。
    • @chqrlie 所以写一篇关于它的博客或者问一个问题。 “深思熟虑”充其量是一条评论。
    • @pipe:它可能更像是评论,但是a)由于它的长度和包含代码,它不能很好地作为评论,并且b)它非常清楚地为读者提供了价值该线程的,因此有助于建立已接受的答案。我认为没有理由将其删除为不是答案。如果在审查答案时有裁量权的情况,这肯定是这样一种情况;删除它会损害未来的读者。
    【解决方案3】:

    作为附录,还有另一种调用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 事物的比较器。此外,它不需要任何强制转换或显式分配,per 我对ISO 9899 6.3.2.3 的解释,

    指向 void 的指针可以转换为指向任何 不完整或对象类型。 . .然后再回来。

    【讨论】:

    • 注意:这种技术的编程术语是 thunk,意思是一个中间函数,它进行一些细微的调整,以便不兼容的源和目标可以结合在一起
    • @M.M 我认为这将被称为“包装器”,而不是 thunk。 thunk 是使用函数或闭包来“暂停”对表达式的求值,将代码传递给类似数据,从而为急切的语言增加惰性。这是严格函数式语言中的常用技术。 Thunks 通常不带参数并返回指定类型的值。
    猜你喜欢
    • 2018-03-26
    • 2014-10-10
    • 1970-01-01
    • 2018-09-14
    • 2021-08-04
    • 2012-07-06
    • 2018-10-09
    • 1970-01-01
    相关资源
    最近更新 更多