函数指针的转换

16

我正在编写一个函数,它接收一个指向比较函数的指针以及一个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*)。虽然它可以编译通过,但这是未定义行为(参见此stackoverflow问题)。

选项2

创建一个全局变量int (*global_comp)(const struct MyStruct *, const struct MyStruct *),并在myStructSort内设置global_comp=comp。然后创建一个函数:

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。这样做功能上是安全的,但是非常不好的做法。

有没有神奇完美的第四个选项?

编辑

我不能更改myStructSort的API,并且使用gcc c99 -Wall -Wextra -Wvla编译我的代码。


在这种情况下,像选项2中你想出来的包装函数是最佳方法。顺便说一句,老实说,我没有完全理解你提到的全局变量的概念。它们有什么作用? - HighPredator
HighPredator@,delegatingComp 需要知道调用哪个函数,但不能将其作为参数传递给它,因为它需要与 qsort 的参数匹配。 - Benjy Kessler
@BenjyKessler 你为什么认为需要一个全局变量?能解释一下吗? - autistic
@BenjyKessler 只有一个注释,我不想将其转换为答案,因为我认为这个设计很愚蠢,不赞成它,但是假设在您的选项2中,您使用第一个参数作为布尔值;如果它非空,则执行比较(通过调用函数),如果为空,则将static变量设置为第二个参数中的任何内容(您可以使用structunion传递函数指针)... 这将允许您将全局移动到函数中(作为static变量),但我确定这不是您想要的,而且它仍然非常丑陋! - autistic
是的,我也想过那样做,但我觉得这种实现方式的代码复杂度会更高,比起重写快排,所以我放弃了。 - Benjy Kessler
显示剩余12条评论
5个回答

10

选项2会破坏线程安全,因此我不会选择它。

选项3是完全错误的,正如你指出的那样。没有理由重新实现快速排序并潜在地犯错。

选项1是未定义行为,但它可以在任何合理的编译器上工作。如果您选择此选项,请务必添加注释。

我还会考虑:

选项4. 重新设计 myStructSort 接口以接受 int (*)(const void *, const void*) 或放弃它并直接调用 qsort。基本上将其发送回架构师,因为他做了一个糟糕的设计选择。


4
对于选项2,您可以将“global”放置在线程本地存储中。 - Colonel Thirty Two
可用选项的概述很好!对我来说,这使得只有选项1才是真正明智的选择,即简单地转换函数指针。其他所有选项都比病情更糟。 - fgp

5
以下方法仅适用于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标准。具体来说,最新的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) /* ... */
 }

没有旧的程序使用 gnu89,他们使用默认的 C11 跳过了 C89 - Jens Gustedt

4
如果您正在使用gcc编译器,那么自glibc 2.8版本以来,您可以使用qsort_r函数,并指定比较器函数及其附加的用户提供的参数。
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);
}

(在 ideone 上实时演示)

我认为你可以不用包装结构体。一个简单的指向函数的指针就足够了。它是一个数据指针,所以它可以通过 void * 完成往返传输。 - user2404501
@WumpusQ.Wumbley:这不是C标准所保证的,我也不知道gcc是否保证。 - rici
你关于@wumpus的观点是正确的;我误读了评论,对此我深表歉意。但是这个结构体并不会增加额外的开销。如果能提供一个指向posix要求的实际指针会更有帮助,但我可以在回家后自己搜索。 - rici
@rici POSIX:http://pubs.opengroup.org/onlinepubs/9699919799/functions/dlsym.html:应用使用部分的最后一段。 - zwol
@rici Windows: https://msdn.microsoft.com/en-us/library/windows/desktop/ms683212%28v=vs.85%29.aspx并没有明确说明这个要求,但是通过“GetProcAddress”能够检索到“函数或变量”的地址,这一点是暗示了的。(请注意,FARPROC是一个函数指针类型。) - zwol
显示剩余2条评论

3
正确的方法是在比较函数中将 void const * 强制转换为 MyStruct const *
对于第一个对象而言,这是明确定义的,因为传递到比较函数的指针是通过将 MyStruct const * 转换为 void const * 而创建的,而将指针转换回其原始类型是允许的(实际上这是唯一允许的)。
对于其他数组成员,假设将 void const * 转换为 char const *、加上通过将对象大小乘以对象在数组中的位置生成的对象偏移量,并将其转换回 void const *,则得到的指针可以转换回 MyStruct const *
这是个大胆的假设,但通常能够实现。可能存在某些极端情况下无法实现该假设,但一般来说编译器会将任何结构体 foo 填充到其对齐方式的倍数,以确保数组成员的起始地址之间的距离为 sizeof(struct foo)
强制转换函数指针通常是不安全的,需要避免,因为不同的数据类型可能具有不同的表示方式——例如,void * 必须能够表示可能从 char * 转换而来的每个可能的地址,而 MyStruct * 则保证了一些最低有效位清除为任何有效对象都将被对齐——因此这些类型的调用约定可能是不同的。

1

唯一的明智之选是重写你已经创建的界面,或者新建一个。

我在另一篇回答中使用冒泡排序做过非常类似的事情

简而言之,对于C语言,你需要让你的排序函数形式为:

void* bubbleSort(void* arr, int (*compareFcn)(void*, void*),
    size_t sizeOfElement, size_t numElements)

你的比较函数应该是以下形式:

int compareFunction(void *a, void *b);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接