转换函数指针
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,即简单地转换函数指针。其他一切都是比疾病更糟糕的治疗方法。以上是关于转换函数指针的主要内容,如果未能解决你的问题,请参考以下文章