转换函数指针

Posted

技术标签:

【中文标题】转换函数指针【英文标题】:Casting function pointers 【发布时间】:2015-11-03 17:48:02 【问题描述】:

我正在编写一个函数,它接收一个指向比较函数的指针和一个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*)。这可以编译但未定义行为(请参阅this SO question)。

选项 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)。问题在于 icky 全局变量。

选项 3

重新实现qsort。这在功能上是安全的,但却是非常糟糕的做法。

有没有神奇的完美第四选择?

编辑

我无法更改 myStructSort 的 API,我正在使用 gcc c99 -Wall -Wextra -Wvla 编译我的代码。

【问题讨论】:

在这种情况下,像您在选项 2 中提出的包装函数是最好的方法。顺便说一句,我不太了解您提到的全局变量。它们是干什么用的? HighPredator@, delegatingComp 需要知道调用哪个函数并且不能作为参数传递给它,因为它需要匹配qsort 的参数。 如果您使用gcc,gnu 扩展允许您在函数内部定义子函数。它模拟基于堆栈的闭包。如果您不介意便携性损坏,可以尝试一下。 @BenjyKessler,啊,现在我明白了。 qsort API基本坏了,这个问题没有好的解决办法。 【参考方案1】:

如果你使用 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;不同之处在于用户提供的上下文参数作为 first 参数提供到比较函数,而不是最后一个参数。)

但如果你有它,它可以让你避免选项 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)

【讨论】:

我认为你可以不用包装结构。一个简单的指向函数的指针就足够了。它是一个数据指针,因此它可以通过void * @WumpusQ.Wumbley:C 标准不保证这一点,我也不知道 gcc 是否保证。 @rici POSIX 和 Windows(它耗尽了您在实践中可能遇到的 ABI 集)实际上都保证可以将指向函数的指针转换为 void * 并返回而不会丢失信息,以便不必为对象和函数符号提供dlsym / GetProcAddress 的两个变体。另外,我相信 Wumpus Q. Wumbley 是正确的:to 指向函数的指针是数据指针,因此可以通过标准 C 中的void * 往返。 您对@wumpus 的看法是正确的;我误读了评论,对此我深表歉意。不过,该结构不会增加任何开销。一个指向 posix 需求的实际指针会很有帮助,但我可以在回家时搜索它。 @rici POSIX:pubs.opengroup.org/onlinepubs/9699919799/functions/dlsym.html:应用程序使用部分的最后一段。【参考方案2】:

正确的做法是在比较函数中将void const *转换成MyStruct const *

这对于第一个对象是明确定义的,因为传递给比较函数的指针是通过从 MyStruct const *void const * 的转换创建的,并将指向 void 的指针转换回其原始类型是允许的(而且它确实是唯一的)。

对于其他数组成员,假设将void const *转换为char const *,加上对象的偏移量,通过将对象大小乘以对象在数组中的位置生成,并将其转换回void const * 将给出一个指针,该指针可以转换回 MyStruct const *

这是一个大胆的假设,但通常会奏效。可能在某些极端情况下这不起作用,但通常编译器会将任何struct foo 填充到其对齐的倍数,以确保数组成员的起始地址具有sizeof(struct foo) 的距离。

转换函数指针通常是不安全的,需要避免,因为不同的数据类型可能有不同的表示形式——例如,void * 必须能够表达每个可能的地址,因为它可以从 @987654333 转换而来@,而 MyStruct * 保证会清除一些最低有效位,因为任何有效对象都会对齐——因此完全有可能这些类型的调用约定可能不同。

【讨论】:

【参考方案3】:

唯一明智的选择是重新编写您创建的界面,或者创建一个新界面。

I've done something very similar with bubble sort on another answer of mine.

简而言之,对于 C,您希望排序函数具有以下形式:

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

你的比较函数是这样的:

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

【讨论】:

【参考方案4】:

以下方法仅适用于gcc。它是 gnu 扩展的一部分。进一步请参考https://gcc.gnu.org/onlinedocs/gcc-4.8.5/gcc/Nested-Functions.html#Nested-Functions

首先让我们确保qsort的原型在such a form中:

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而不是gnu99,他们默认从C89跳到C11。【参考方案5】:

选项 2 破坏了线程安全,所以我不会选择那个。

正如您所指出的,选项 3 完全是错误的。没有理由重新实现快速排序并可能犯错误。

选项 1 是 UB,但它适用于任何健全的编译器。如果您选择此选项,请务必添加评论。

我也会考虑:

方案4. 重新设计myStructSort的接口,取用int (*)(const void *, const void*)或完全废弃,直接调用qsort。基本上把它寄回给建筑师,因为他做了一个糟糕的设计选择。

【讨论】:

对于选项 2,您可以将“全局”放在线程本地存储中。 很好地概述了可用选项!对我来说,这清楚地表明唯一真正理智的选项是选项 1,即简单地转换函数指针。其他一切都是比疾病更糟糕的治疗方法。

以上是关于转换函数指针的主要内容,如果未能解决你的问题,请参考以下文章

转换函数指针

向上转换函数指针是不是安全?

用于成员函数指针往返转换的 void(*)() 类似物

将 void 指针(数据)转换为函数指针

c++ 成员函数 普通函数指针转换

如何合法地将函数指针转换为方法指针?