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

In C language, is there any way to use callback with arbitrary / variable arguments?

提问人:hr13 提问时间:10/21/2022 更新时间:11/16/2022 访问量:94

问:

我想为同一函数发送具有不同签名的回调。像这样的称呼:

#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;
    }
}

但显然它不起作用:(

c 变量 参数 回调

评论

0赞 tstanisl 10/21/2022
宏是否适合您的需求?#define exec(f, ...) f(__VA_ARGS__)

答:

1赞 dbush 10/21/2022 #1

您可以更改回调以采用单个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);
}

评论

0赞 John Bollinger 10/21/2022
但这违背了既定的目的。它只支持一个回调签名,而不是具有不同签名的回调。考虑到建议的特定回调签名,这可能仍然会使 OP 接近他们想要的位置,但这不是实际要求的。
1赞 John Bollinger 10/21/2022 #2

使回调函数接口在提供给函数的数据方面具有灵活性的通常解决方案是为回调签名提供一个参数(可能除了其他参数之外)。可以通过这样的参数提供任意数据。像这样的东西: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);
}

但是,通过指定没有原型的回调类型,可以做一些更像您描述的那样的事情,其中回调函数确实具有不同的签名。在这种情况下,至少还有以下注意事项:

  • 如果回调函数本身是用原型定义的(它们应该是这样),那么参数类型不应该是默认参数升级更改的任何类型。因此,指针、s、s,但不是 s 或 s 或 s(不是详尽的列表)。如果要支持其他参数类型,则需要在调用函数之前强制转换函数指针,如下所述。intdoublefloatshort intchar

  • 回调函数不能是可变的。

  • 如果前端是可变的,那么它需要在运行时以某种方式被告知参数的实际数量和类型是什么。

  • 此外,还需要使用正确的参数显式调用回调函数,因此只能支持一组固定的预定回调签名。

例如,它可能如下所示:

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);
}

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

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

评论

0赞 tstanisl 10/21/2022
Afaik,没有原型的函数将在 C2x 中删除。他们将成为(void)
0赞 tsoa 11/16/2022 #3

您可以使用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));
}