C 语言中统一的函数指针
有时候,我们需要把多个模块粘合在一起。而这些模块的接口参数上有少许的不同。在 C 语言中,参数(或是返回值)不同的函数指针属于不同的类型,如果混用,编译器会警告你类型错误。
在 C 语言中,函数定义是可以不写参数的。比如:
void foo();
这个函数定义表示了一个返回 void 的函数,参数未定。也就是说,它是个弱类型,诸如:
void foo(int);
void foo(void *);
这些类型都可以无害的转换成它。正如在 C 语言中,具体的指针类型如 int * ,char * 都可以转换为 void * 一样。
注1:如果要严格定义一个无参数的函数,应该写成 void foo(void);
注2:如果有部分参数固定,而其后的参数可变,则定义看起来是这样: void foo(int , ...); 这表示第一个参数为 int ,从第 2 个参数开始可变。
不过,C 语言的这种语法,实际上不太使用。因为用 C 语言无法主动控制函数调用的参数压栈。我们很难根据程序的上下文来决定如何传入参数去调用某个函数。如果需要逐级传递多个函数的参数,用的更多的是 va_list
比如,你很难对 printf 做封装,通常为了方便做封装,还提供了形为 vprintf 的接口。
C++ 解决此类问题的方案是用类去模拟一个函数,通过重载 () 操作符的方法,让函数调用看起来和普通函数一致(并美其名曰 functor/仿函数)。当然,也有撕破语法糖的伪装,用更直白的类继承的方式来定义出接口。
这里想说的是,C 语言里也还有一种有趣的方案来在保证类型安全的基础上解决类似问题。
在 X-Window 的消息定义中就可以看到这样的手法。
在 Windows 的接口中,Windows 的消息携带的数据通常用两个参数来表示:WPARAM 和 LPARAM ,均为 32bit 整数。我们知道,消息本质上等同于对象的方法。在更早的面向对象语言如 smalltalk 中,调用对象的方法即被看成向对象发送一个消息。Windows 如此把所有消息处理相关函数的接口都以 WPARAM 与 LPARAM 的形式传递参数,正是为了方便统一其接口形式。各种五花八门的参数都蕴涵于这 64 bit 数据中。
Xlib 处理类似的问题,对 C 程序员的亲合力则大的多。至少更为类型安全。
Xlib 定义了一个叫做 XEvent 的结构体(实际是一个 union)。然后把各种可能的消息类型放在这个 union 中。例如,我想取键盘消息,则可以用 event.xkey.keycode 。
一般说来,我们可以把模块的对外接口看成是接收一组输入参数并加以处理。如果需要粘合多个不同的模块,他们需要处理不同的输入参数的话,可以借鉴 XLib 的这个方法。在粘合层定义一个 union ,把所有可能的参数组,每组定义成一个 struct 然后定义在同一个 union 中。这个粘合层的统一接口则为这个 union 指针。有必要的话,所有的参数组 struct 的头部都留下 type 字段。这样比较容易分发消息。
这样做的本质是:把函数调用时由编译生成的、将调用参数逐个压栈的代码,改由程序员主动填写(填写参数结构体)。利用结构的类型安全,保证了函数调用时的参数类型安全。再利用 union 的语法,把不同的参数组联合到一起变成同一类型。
给 api 传递一个 struct 或 union 指针而不是逐个参数传递,是 C 接口设计的一种常见手法。除了 XLib 的设计,还能找到很多耳熟能详的例子。例如,我们在 socket api 上也可以看到类似的东西。例如 connect 的参数中有一 sockaddr 结构,就适用于各种不同的网络底层协议。