我有一个数据结构,为每个节点存储一个通用void *
,并在适当的时候将其转换为正确的类型。在这个对象的清理函数中,我想提供一个回调,以便这个通用对象也可以被清理。"
struct foo {
void *data;
// ...
};
void foo_cleanup(struct foo *foo, void (*data_cleanup)(void *data)) {
data_cleanup(foo->data);
// ...
}
// ...
void bar_cleanup(void *data) {
struct bar *bar = (struct bar *)data;
// ...
}
这样可行,但如果bar_cleanup
的签名直接引用bar
,而不是void *
,我会更喜欢这样做:
void bar_cleanup(struct bar *bar)
当然,替换该代码会导致"参数类型不匹配"警告。有没有办法直接实现我想要做的事情,如果不是类似的方法来实现相同的清理任务?
答案 0 :(得分:1)
你现在正在做的是处理它的正确方法。您希望使用指针清除特定类型的函数会违反C11中的(严格)规则(和C99,可能是C90,但我还没有正式检查过C90)。
[§6.3]转化次数
§6.3.2.3 Pointers
¶8指向一种类型函数的指针可以转换为指向另一种类型函数的指针,然后再返回;结果应该等于原始指针。如果转换的指针用于调用类型与引用类型不兼容的函数,则行为未定义。
您现有的代码是:
struct foo {
void *data;
// ...
};
void foo_cleanup(struct foo *foo, void (*data_cleanup)(void *data)) {
data_cleanup(foo->data);
// ...
}
void bar_cleanup(void *data) {
struct bar *bar = (struct bar *)data;
// ...
}
此代码干净且遵守规则。指向bar
清理函数的指针具有与void (*)(void *)
使用的指针匹配的签名foo_cleanup()
。 bar_cleanup()
中的强制转换是可选的但是明确的。即使您省略了演员表示法,当C自动从void *
转换为struct bar *
时,也会发生转换。
如果您尝试使用清理功能:
void bar_cleanup(struct bar *bar);
您必须拨打相当于:
的电话struct foo foo37;
…code initializing foo37…
foo_cleanup(&foo37, (void (*)(void *))bar_cleanup);
这会将函数的类型强制转换为不同的指针类型。除非foo_cleanup()
内的代码以某种方式知道(如何?)指针需要使用带有签名void (*)(struct bar *)
的函数并在调用清理函数之前更改它,否则它将违反§6.3.2.3中的规则
foo_cleanup(struct foo *foo, void (*data_cleanup)(void *data))
{
(*data_cleanup)(foo->data); // Undefined behaviour
if (data->…)
(*(void (*)(struct bar *))data_cleanup)(foo->data); // OK, but…
…
}
无条件调用是错误的,因为实际函数指针的类型和参数类型声明的类型不同。条件调用是干净的,因为它在调用函数之前将指针强制转换回其真实类型。 (这是C;从void *
到struct bar *
的转换是自动且有效的。)。但是,必须知道在foo_cleanup()
函数中将指针转换为函数的方法,首先会失败使用指针到函数的目的。还不清楚foo_cleanup()
如何确定哪个强制转换是正确的,如果添加新类型,则必须再次更改代码以支持新类型。
所有这些都意味着使用void bar_cleanup(struct bar *bar)
的解决方案并不是真的可以接受。
如果您遵循标准规定的严格规则并且仍然想要调用void bar_cleanup(struct bar *)
,则必须编写可怕的,不可维护的,不灵活的代码。
如果您想要绝对可靠的代码,您将遵循这些规则并保留现有代码(void bar_cleanup(void *data)
)。它具有避免痛苦转换的有益副作用 - 函数指针转换并不漂亮 - 并且无论foo_cleanup()
成员data
中存储了多少不同的指针类型,struct foo
函数都保持不变只要调用代码知道哪个是正确的类型(并且如果调用代码不知道,那么无论如何都是“放弃希望所有进入这里的人”)。
在实践中,问题有多严重?实际上,你现在可能会侥幸逃脱。但它正在调用未定义的行为,编译器总是急于识别和利用未定义的行为来“优化”它们生成的代码。你可以在foo_cleanup()
中没有演员的情况下按照自己的意愿去做,但是你可以
通过保留当前的代码来承担可以简单无痛地避免的风险。
请注意,这适用于传递给标准库中qsort()
或bsearch()
的比较器函数。应编写这些函数以获取两个const void *
参数并返回int
。否则违反§6.3.2.3。
在其他受尊重的C书中有一些例子没有遵守这些严格的规则。
答案 1 :(得分:-1)
typedef void (*CLEANUP_FUNC)(void *);
void foo_cleanup(struct foo *foo, CLEANUP_FUNC *data_cleanup) {
data_cleanup(foo->data);
// ...
}
void bar_cleanup(struct bar *data) {
// ...
}
foo_cleanup(foo, (CLEANUP_FUNC *)bar_cleanup);
以下是一个完整的示例,其中包含通用堆栈Stack
和一些Foo
类型的元素。
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
// ---
typedef void (STACK_DEALLOCATOR)(void *);
typedef struct Stack {
STACK_DEALLOCATOR *element_deallocator;
size_t num_elements;
size_t num_allocated;
void **elements;
} Stack;
Stack *Stack_New(STACK_DEALLOCATOR *element_deallocator) {
Stack *this = malloc(sizeof(Stack));
if (this == NULL)
goto ERROR;
this->element_deallocator = element_deallocator;
this->num_elements = 0;
this->num_allocated = 4;
this->elements = malloc(sizeof(void *) * this->num_allocated);
if (this->elements == NULL)
goto ERROR2;
return this;
ERROR2: free(this);
ERROR: return NULL;
}
int Stack_Push(Stack *this, void *element) {
if (this->num_elements == this->num_allocated) {
// ...
}
this->elements[ this->num_elements++ ] = element;
return 1;
}
void Stack_Destroy(Stack *this) {
void **element = this->elements;
for (size_t i=this->num_elements; i--; ) {
this->element_deallocator(*(element++));
}
free(this->elements);
free(this);
}
// ---
typedef struct Foo {
int data;
// ....
} Foo;
Foo *Foo_New(int data) {
Foo *this = malloc(sizeof(Foo));
if (this == NULL)
return NULL;
this->data = data;
return this;
}
void Foo_Destroy(Foo *this) {
free(this);
}
// ---
int main(void) {
Stack *stack = Stack_New((STACK_DEALLOCATOR *)Foo_Destroy);
if (stack == NULL) {
perror("Stack_New");
goto ERROR;
}
Foo *foo = Foo_New(123);
if (foo == NULL) {
perror("Foo_New");
goto ERROR2;
}
if (!Stack_Push(stack, foo)) {
perror("Stack_Push");
goto ERROR3;
}
Stack_Destroy(stack);
return 0;
ERROR3: Foo_Destroy(foo);
ERROR2: Stack_Destroy(stack);
ERROR: return 1;
}