在 C 中创建通用 "function pointer" 联合是个坏主意吗?

Is it a bad idea to create a generic "function pointer" union in C?

对于我正在从事的项目,希望有一个通用的“指向函数的指针”类型。但是,在C中,要有一个指向函数的指针,需要在函数指针的类型中指定原型。

例如,如果我有函数 void setdata(short data),我不能将它存储在与函数 int getdata() 相同的指针中,因为它们的参数和 return 值不一样。

经过一些横向思考,我想出了以下解决方法:

typedef long (* PFL)(); /* pointer to function that returns a long... */
typedef short (* PFI)(); /* pointer to function that returns a short... */
typedef void (* PFV)(); /* pointer to function that returns a void... */
typedef void (* PFVAL)(long); /* pointer to function that returns a void...
                                but has a long argument...*/
typedef void (* PFVAI)(short); /* pointer to function that returns a void...
                                but has an short argument...*/
typedef void (* PFVAU)(unsigned short);
typedef void (* PFVAII)(short, short); /* same as above, but two shorts... */
typedef void (* PFVAUU)(unsigned short, unsigned short); /* same as above, but two shorts... */

typedef union {
    PFV         pfv;
    PFI         pfi;
    PFL         pfl;
    PFVAL       pfval;
    PFVAI       pfvai;
    PFVAU       pfvau;
    PFVAII      pfvaii;
    PFVAUU      pfvauu;
} FP;

果然,我能够像这样初始化这种类型的实例:

FP funcpointer = { .pfvai = setdata };

Clang 和 GCC 没有抱怨。这是个坏主意吗?

你所做的是有效的。只要您通过正确的指针类型调用指向的函数,它就定义得很好。

这个联合可能会变大,但这取决于您必须支持多少种不同的函数类型,并且您必须使其与您的 typedef 集保持同步。事实证明,您可以通过强制转换从一种函数指针类型自由转换为另一种函数指针类型,您只需要确保使用正确的类型调用它即可。

C 标准的第 6.3.2.3p8 节说明了以下关于函数指针转换的内容:

A pointer to a function of one type may be converted to a pointer to a function of another type and back again; the result shall compare equal to the original pointer. If a converted pointer is used to call a function whose type is not compatible with the referenced type, the behavior is undefined.

所以您也可以只使用 void (*)() 作为通用指针类型而不是使用联合,然后您需要在调用它时应用正确的转换。例如:

typedef void (*FP)();
FP fp = setdata;
...
((PFVAI)fp)(123);

这很有趣,我在纯 C 项目中多次使用下面的模式。

这个想法是把你想要设置的所有信息收集到一个“table”中,然后运行这个table的内容通过各种宏“函数”。

#define WITH_FUNP_MEMBERS \
  ON_FUNP_MEMBER(0, PFV, void, void) \
  ON_FUNP_MEMBER(1, PFL, long, void) \
  ON_FUNP_MEMBER(2, PFS, short, void) \
  ON_FUNP_MEMBER(3, PFI, int, void) \
  ON_FUNP_MEMBER(4, PFVL, void, long) \
  ON_FUNP_MEMBER(5, PFVS, void, short) \
  ON_FUNP_MEMBER(6, PFVI, void, int) \
  ON_FUNP_MEMBER(7, PFVSS, void, short, short)

// NAME_tag: Tags
enum Type_Tag { void_tag = 0, long_tag, short_tag, int_tag };
#define ON_FUNP_MEMBER(id, NAME, ret, ...) NAME##_tag = ret##_tag << 8 | id,
enum FUNP_Tag { WITH_FUNP_MEMBERS };
#undef ON_FUNP_MEMBER

// NAME_ret: Return Types
#define ON_FUNP_MEMBER(id, NAME, ret, ...) typedef ret NAME##_ret;
WITH_FUNP_MEMBERS
#undef ON_FUNP_MEMBER

// NAME: Function Pointer Types
#define ON_FUNP_MEMBER(id, NAME, ret, ...) typedef ret(*NAME)(__VA_ARGS__);
WITH_FUNP_MEMBERS
#undef ON_FUNP_MEMBER

// Functor
#define ON_FUNP_MEMBER(id, NAME, ret, ...) NAME p_##NAME;
struct {
    union {
        WITH_FUNP_MEMBERS // PFV p_PFV; PFL p_PFL; ...
    };
    enum FUNP_Tag tag;
} typedef Functor;
#undef ON_FUNP_MEMBER

现在我们可以生成一些类型安全的 setters,其中被分配的函数的类型被捕获在 setter 的名称中。如果分配了错误类型的函数,编译器将发出警告。

// Setter Declarations
#define ON_FUNP_MEMBER(id, NAME, ret, ...) \
    void FUNP_Set_##NAME(Functor *f, NAME arg);    
WITH_FUNP_MEMBERS
#undef ON_FUNP_MEMBER

// Setter Definitions
#define ON_FUNP_MEMBER(id, NAME, ret, ...) \
    void FUNP_Set_##NAME(Functor *f, NAME arg) { \
        f->tag = NAME##_tag; \
        f->p_##NAME = arg; \
    }
WITH_FUNP_MEMBERS
#undef ON_FUNP_MEMBER

现在我们可以声明一个方便的仿函数初始值设定项和调用程序。调用程序是类型安全的,如果其类型与传递给调用程序的名称不匹配,则不会调用仿函数。

// Invoker Helpers
static inline int FUNP__set_ok(void* ok) { if (ok) *(int*)ok = 1; }
static inline int FUNP__clr_ok(void *ok) { if (ok) *(int*)ok = 0; }

// Functor Initializer
// Example: Functor f = FUNP_INIT(PFI, my_int_function);
#define FUNP_INIT(NAME, function) {.p_##NAME = (function), .tag = NAME##_tag}

// Functor Invoker
// Example: return FUNP_INVOKE(PFI, &f, NULL)   [returns an integer]
#define FUNP_INVOKE(NAME, f, ok, ...) \
    (((f) && (f)->tag == NAME##_tag && (f)->p_##NAME) ? (FUNP__set_ok(ok), (f)->p_##NAME(__VA_ARGS__)) : (NAME##_ret)FUNP__clr_ok(ok))

最后:

// Test
#include <assert.h>
#include <stdio.h>

int fun1(void) { return 42; }
void fun2(int i) { printf("%d\n", i); }
int main()
{
    Functor f = FUNP_INIT(PFI, fun1);
    int value = FUNP_INVOKE(PFI, &f, NULL);
    assert(value == 42);
    int ok = 0;
    FUNP_INVOKE(PFI, &f, &ok);
    assert(ok);
    FUNP_INVOKE(PFVI, &f, &ok, value); // type mismatch
    assert(!ok);

    FUNP_Set_PFVI(&f, fun2);
    FUNP_INVOKE(PFVI, &f, NULL, value);
}