带有通用指针参数的函数指针

时间:2018-05-18 01:04:56

标签: c

我有一个数据结构,为每个节点存储一个通用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)

当然,替换该代码会导致"参数类型不匹配"警告。有没有办法直接实现我想要做的事情,如果不是类似的方法来实现相同的清理任务?

2 个答案:

答案 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;
}