在C语言中,有没有办法使用带有任意/可变参数的回调函数?

gopyfrb3  于 2022-12-03  发布在  其他
关注(0)|答案(3)|浏览(129)

我想用不同的签名为同一个函数发送回调。类似这样的事情:

#include <stdio.h>
#include <stdarg.h>

void a(int pa) {}
void b(int pb1, float pb2) {}

// exec implementation

int main() {
    exec(a, 1);
    exec(b, 1, 2.3);
}

我想到了使用类似这样的语句:

void exec(void (*func)(...), ...) {
    int arg1;
    float arg2;

    va_list valist;
    va_start(valist, size);

    arg1 = va_arg(valist, int);
    if (size == 1) {
        (*func)(arg1);
        va_end(valist);
        return;
    }

    arg2 = va_arg(valist, float);
    if (size == 2) {
        (*func)(arg1, arg2);
        va_end(valist);
        return;
    }
}

但很明显,这是行不通的:(

pu82cl6c

pu82cl6c1#

为了使回调函数接口在提供给函数的数据方面具有灵活性,通常的解决方案是给予回调签名一个void *参数(可能还有其他参数)。通过这样的参数可以提供任意数据。类似于:

void exec(void (*func)(void *), void *data) {
    func(data);
}

struct s2 {
    int i;
    float f;
};

void func1(void *data) {
    int i = *(int *)data;
    // ...
}

void func2(void *data) {
    struct s2 s = *(struct s2 *)data;
    // ...
}

int main(void) {
    int i = 42;
    struct s2 s = { .i = 17, .f = 3.14 };

    exec(func1, &i);
    exec(func2, &s);
}

然而,通过指定回调类型而不使用原型,可以实现与您描述的更相似的功能,其中回调函数确实具有不同的签名。在这种情况下,至少仍有以下警告:

  • 如果回调函数本身是用原型定义的(因为它们应该是)则参数类型不应该是任何被默认参数提升所改变的类型。但不包括float s、short int s或char s如果你想支持其他的参数类型,那么你需要在调用函数之前转换函数指针,这将在后面描述。
  • 回调函数不能是变量函数。
  • 如果前端是变元的,那么在运行时需要以某种方式告诉它参数的实际数量和类型。
  • 此外,需要使用正确的参数显式调用回调函数,因此只能支持一组固定的预定回调签名。

例如,可能如下所示:

enum sig { INT, INT_DOUB };

void exec(void (*func)(/* no prototype */), enum sig cb_sig, ...);

void a(int pa) {}
void b(int pb1, double pb2) {}

int main(void) {
    exec(a, INT, 1);
    exec(b, INT_DOUB, 1, 2.3);
}

void exec(void (*func)(/* no prototype */), enum sig cb_sig, ...) {
    va_list valist;
    va_start(valist, cb_sig);

    switch (cb_sig) {
        case INT: {
            int i = va_arg(valist, int);
            func(i);
            break;
        }
        case INT_DOUB: {
            int i = va_arg(valist, int);
            double d = va_arg(valist, double);
            func(i, d);
            break;
        }
        default:
            assert(("Can't be reached", 0));
    }

    va_end(valist);
}

这可能会引发一些警告,例如关于函数声明没有提供原型,以及关于调用(declared,but)非原型函数。但是,由于在执行调用时您已经知道了签名,因此可以通过适当的强制转换来消除后一种警告。例如,

// ...
        case INT: {
            int i = va_arg(valist, int);
            ((void (*)(int))func)(i);
            break;
        }
        // ...
0tdrvxhp

0tdrvxhp2#

您可以更改回调以接受单个va_list参数:

void a(va_list args) 
{
    int pa = va_arg(args,int);
}

void b(va_list args)
{
    int pb1 = va_arg(args,int);
    double pb2 = va_arg(args,double);
}

并让另一个函数传递va_list

void exec(void (*func)(va_list), ...)
{
    va_list valist;
    va_start(valist, func);
    func(valist);
    va_end(valist);
}
9w11ddsr

9w11ddsr3#

您可以使用va_args来解决这个问题。

#include <stdio.h>
#include <stdint.h>
#include <stdarg.h>

#define exec_func(func, ...) func(__VA_ARGS__)

long func(char *a, int b, long c, long d)
{
    printf("a: %s, b: %d, c: %ld, d: %ld\n", a, b, c, d);
    return c + d;
}

int main()
{
    printf("c + d: %ld\n", exec_func(func, "test", 10, 1000, 1000));
}

相关问题