如何指定一个指向参数数量未知的 C 函数的 C++ 指针?

How to specify a C++ pointer to a C function with unknown number of arguments?

我正在编写一个 C 库,我希望它可以在 C 和 C++ 中使用。在某个时刻,它应该从用户那里获取一个带有 0-3 个参数的回调,稍后将在某个指针处调用。像这样(代码的副本也可以作为 GitHub Gist 使用):

// app_c.c
#include <stdio.h>
#include "lib.h"

double f0(void) {
    return 123;
}

double f2(double a, double b) {
    return a + b;
}

int main() {
    cb_arity = 0;
    cb_func = f0;
    printf("%f\n", cb_call());

    cb_arity = 2;
    cb_func = f2;
    printf("%f\n", cb_call());
}

我能够创建一个指向 C 函数的指针,该函数接受未知(但仍然固定)数量的参数,请注意它是 void (*cb_func)(),而不是 void (*cb_func)(void):

// lib.h
#ifndef LIB_H_
#define LIB_H_

#ifdef __cplusplus
extern "C" {
#endif

extern int cb_arity;
extern double (*cb_func)();
double cb_call(void);

#ifdef __cplusplus
}
#endif

#endif  // LIB_H_
// lib.c
#include "lib.h"
#include <stdlib.h>

int cb_arity;
double (*cb_func)();

double cb_call(void) {
    switch (cb_arity) {
        case 0:
            return cb_func();
        case 1:
            return cb_func(10.0);
        case 2:
            return cb_func(10.0, 20.0);
        case 3:
            return cb_func(10.0, 20.0, 30.0);
        default:
            abort();
    }
}

它在我的机器和 Wandbox 上都能成功编译和运行。据我了解,没有调用 UB。

现在我想让它在 C++ 中也能工作。不幸的是,看起来我现在需要 reinterpret_cast 因为 () 在 C++ 中意味着“无参数”,而不是“未知数量的参数”:

// app_cpp.cpp
#include <stdio.h>
#include "lib.h"

int main() {
    cb_arity = 0;
    cb_func = []() { return 123.0; };
    printf("%f\n", cb_call());

    cb_arity = 2;
    cb_func = reinterpret_cast<double(*)()>(static_cast<double(*)(double, double)>(
        [](double a, double b) { return a + b; }
    ));
    printf("%f\n", cb_call());
}

据我了解,这里也没有调用 UB:虽然我在 C++ 中将函数指针 double(*)(double, double) 转换为 double(*)(void),但在 C 代码中它又转换回 double(*)(double, double)就在打电话之前。

有什么方法可以摆脱 C++ 代码中这些丑陋的强制转换吗?我已经尝试将 cb_func 的类型指定为 void(*)(...),但 C++ 仍然不会将 double(*)(double, double) 隐式转换为它。

与其从回调中删除参数数量,不如保留它。

// lib.h
#ifndef LIB_H_
#define LIB_H_

#ifdef __cplusplus
extern "C" {
#endif

typedef struct {
    int arity;
    union {
        void(*zero)(void);
        void(*one)(double);
        void(*two)(double, double);
        void(*three)(double, double, double);
    }
} cb_type;
extern cb_type cb;

double cb_call(void);

#ifdef __cplusplus
}
#endif

#endif  // LIB_H_
// lib.c
#include "lib.h"
#include <stdlib.h>

cb_type cb;

double cb_call(void) {
    switch (cb.arity) {
        case 0:
            return cb.zero();
        case 1:
            return cb.one(10.0);
        case 2:
            return cb.two(10.0, 20.0);
        case 3:
            return cb.three(10.0, 20.0, 30.0);
        default:
            abort();
    }
}

如果你不暴露cb,你不能不匹配arity和union成员:

// lib.h
#ifndef LIB_H_
#define LIB_H_

#ifdef __cplusplus
extern "C" {
#endif

void register_zero(void(*)(void));
void register_one(void(*)(double));
void register_two(void(*)(double, double));
void register_three(void(*)(double, double, double));

double cb_call(void);

#ifdef __cplusplus
}
#endif

#endif  // LIB_H_

无参数函数声明是一个过时的 C 特性。我建议避免使用它。

本质上,您是在试图规避类型检查:您想将一个几乎任意的函数存储在一个 cb_func 变量中,然后能够自由调用它,而不会像显式强制转换那样出现任何危险迹象。然而,这段代码本质上是危险的:如果你搞砸了 cb_arity,行为是未定义的。此外,您甚至可以在没有任何警告的情况下将 double(*)(int, char*) 存储到 cb_func,这在您的示例中是行不通的。

在更深层次上,您的 cb_arity/cb_func 看起来很像 tagged union. The canonical way to implement it in C++ is std::variant (or something function-specific like unique_pseudofunction)。在 C 中实现它的规范方法是带有“标记”字段和匿名 union 的结构,如下所示:

struct cb_s {
    int arity;
    union {
        double (*func0)(void);
        double (*func1)(double);
        double (*func2)(double, double);
        double (*func3)(double, double, double);
    };
};

extern struct cb_s cb;

现在 cb_func = f0 改为 cb.func0 = f0cb_func = f2 改为 cb.func2 = f2。在 C++ 中类似,所有来自 lambda 的转换现在都消失了。唯一剩下的危险迹象是潜在的 union.

您必须在两个地方更改代码:

  1. 图书馆。在这里你已经知道调用func0/func1/...中的哪一个,没什么大不了的。
  2. 写入cb_func的用户代码。现在它必须知道写入哪个联合成员。据推测,相同的代码也会写一个编译时常量 cb_arity,所以 agian 没什么大不了的。如果 cb_arity 是从其他人那里收到的,那么为了更好的类型安全,那个人也应该使用标记联合而不是分别传递 cb_aritycb_func

旁注:C++(相对于 C)禁止访问 union 的非活动成员。这不应该影响代码的正确性,因为通过指向 func1 的指针调用 func0 本身就是 UB。