如何指定一个指向参数数量未知的 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 = f0
,cb_func = f2
改为 cb.func2 = f2
。在 C++ 中类似,所有来自 lambda 的转换现在都消失了。唯一剩下的危险迹象是潜在的 union
.
您必须在两个地方更改代码:
- 图书馆。在这里你已经知道调用
func0
/func1
/...中的哪一个,没什么大不了的。
- 写入
cb_func
的用户代码。现在它必须知道写入哪个联合成员。据推测,相同的代码也会写一个编译时常量 cb_arity
,所以 agian 没什么大不了的。如果 cb_arity
是从其他人那里收到的,那么为了更好的类型安全,那个人也应该使用标记联合而不是分别传递 cb_arity
和 cb_func
。
旁注:C++(相对于 C)禁止访问 union 的非活动成员。这不应该影响代码的正确性,因为通过指向 func1
的指针调用 func0
本身就是 UB。
我正在编写一个 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 = f0
,cb_func = f2
改为 cb.func2 = f2
。在 C++ 中类似,所有来自 lambda 的转换现在都消失了。唯一剩下的危险迹象是潜在的 union
.
您必须在两个地方更改代码:
- 图书馆。在这里你已经知道调用
func0
/func1
/...中的哪一个,没什么大不了的。 - 写入
cb_func
的用户代码。现在它必须知道写入哪个联合成员。据推测,相同的代码也会写一个编译时常量cb_arity
,所以 agian 没什么大不了的。如果cb_arity
是从其他人那里收到的,那么为了更好的类型安全,那个人也应该使用标记联合而不是分别传递cb_arity
和cb_func
。
旁注:C++(相对于 C)禁止访问 union 的非活动成员。这不应该影响代码的正确性,因为通过指向 func1
的指针调用 func0
本身就是 UB。