C语言实现“多接口”的另一种方式
前面有一篇文章介绍了如何使用兼容一点五编程
的方式去支持“多接口”。
作为对比,本文提供另一种实现“多接口”的方式,来帮助大家从另一个角度理解“多接口”实现的要点和面对的困难。
代码示例
为了方便参照对比,接口定义部分,同前面那篇文章一样:
typedef int (*bird_fly_fn_t)(void *self, int distance);
typedef int (*dog_run_fn_t)(void *self, int distance);
typedef int (*dog_bark_fn_t)(void *self, int count);
struct bird_i {
bird_fly_fn_t fly;
};
struct dog_i {
dog_run_fn_t run;
dog_bark_fn_t bark;
};
接下来是接口的实现者,同前面那篇文章类似:
struct crow {
const char *name;
struct bird_i *bird_interface;
};
int crow_fly(struct crow *self, int distance);
struct bird_i crow_interface = {
.fly = (bird_fly_fn_t)crow_fly,
};
int crow_init(struct crow *self, const char *name) {
self->bird_interface = &crow_interface;
self->name = name;
return 0;
}
int crow_fly(struct crow *self, int distance) {
my_printf("Crow %s is flying, target distance: %d\n", self->name, distance);
return 0;
}
注意看,这里特地把接口字段bird_interface
放到了“非首字段”,以和一点五编程
方案形成更明显的视觉区分。
同样的方式,我们实现一下另一个接口:
struct husky {
const char *name;
struct dog_i *dog_interface;
};
int husky_run(struct husky *self, int distance);
int husky_bark(struct husky *self, int distance);
struct dog_i husky_interface = {
.run = (dog_run_fn_t)husky_run,
.bark = (dog_bark_fn_t)husky_bark,
};
int husky_init(struct husky *self, const char *name) {
self->dog_interface = &husky_interface;
self->name = name;
return 0;
}
int husky_run(struct husky *self, int distance) {
my_printf("Husky %s is running, target distance: %d\n", self->name, distance);
return 0;
}
int husky_bark(struct husky *self, int distance) {
my_printf("Husky %s is barking, target count: %d\n", self->name, distance);
return 0;
}
然后是一个实现了多个接口的对象:
struct angel_dog {
const char *name;
int wing_span;
struct bird_i *bird_interface;
struct dog_i *dog_interface;
};
int angel_dog_fly(struct angel_dog *self, int distance);
int angel_dog_run(struct angel_dog *self, int distance);
int angel_dog_bark(struct angel_dog *self, int distance);
struct bird_i angel_dog_bird_interface = {
.fly = (bird_fly_fn_t)angel_dog_fly,
};
struct dog_i angel_dog_dog_interface = {
.run = (dog_run_fn_t)angel_dog_run,
.bark = (dog_bark_fn_t)angel_dog_bark,
};
int angel_dog_init(struct angel_dog *self, const char *name, int wing_span) {
self->dog_interface = &angel_dog_dog_interface;
self->bird_interface = &angel_dog_bird_interface;
self->wing_span = wing_span;
self->name = name;
return 0;
}
int angel_dog_fly(struct angel_dog *self, int distance) {
my_printf("Angel dog %s is flying, target distance: %d\n", self->name, distance);
return 0;
}
int angel_dog_run(struct angel_dog *self, int distance) {
my_printf("Angel dog %s is running, target distance: %d\n", self->name, distance);
return 0;
}
int angel_dog_bark(struct angel_dog *self, int distance) {
my_printf("Angel dog %s is barking, target count: %d\n", self->name, distance);
return 0;
}
上面的代码,除了接口指针所在的字段位置不同,总体上还是跟一点五编程
非常类似的。
这是必然的,通过虚函数表
或类似结构来进行接口编程时,“实现者”这部分必定是相似的。
区别主要体用在“使用者”这一边。
接下来我们编写“使用侧”的代码。首先必须定义下面这个宏:
#define CONTAINER_OF(P_OBJ, TYPE, FIELD) \
((TYPE *)((char *)P_OBJ - (uintptr_t)(&((TYPE *)0)->FIELD)))
这个宏,是用来通过字段地址,反推出字段所属的结构体对象地址。
这个宏在Linux
中经常使用,Linux
中的链表等数据结构,都是基于CONTAINER_OF
的原理实现的。
offsetof
这个关键字可以用来实现CONTAINER_OF
宏,但是本文还是使用更传统,编译器支持更广的计算方案。
接着我们准备好用来测试的数据:
struct crow black;
struct husky hus;
struct angel_dog air;
crow_init(&black, "Black");
husky_init(&hus, "Hus");
angel_dog_init(&air, "Air", 8);
直接使用的话,代码按上去比较复杂:
hus.dog_interface->bark(CONTAINER_OF(&hus.dog_interface, struct husky, dog_interface), 1);
black.bird_interface->fly(CONTAINER_OF(&black.bird_interface, struct crow, bird_interface), 2);
air.dog_interface->bark(CONTAINER_OF(&air.dog_interface, struct angel_dog, dog_interface), 3);
air.bird_interface->fly(CONTAINER_OF(&air.bird_interface, struct angel_dog, bird_interface), 4);
上面的调用语句太长,我们提取关键信息简化一下得到:
obj.i->f(&obj.i - OFFSET_OF_obj_i, 1);
上面的形式还不够清晰,让我们将它等价变换成另一种形式:
(*p)->f(p - OFFSET_OF_obj_i, 1);
省略掉业务参数,只保留核心骨架,这个公式等同于:
(*p)->f(p - N);
变成这种形式,大家就能发现这种方案,跟
一点五编程
的(*p)->f(p)
的区别了。
同时,这个公式也出现在前面一篇文章《C++的虚函数,在机器层面的实现方式》中, 大家可以对比思考,融会贯通。
对于不同类型的obj
,这个OFFSET_OF_obj_i
的值不同,而OFFSET_OF_obj_i
这个值只在编译期才有,运行期没有,
所以这种形式的调用,只好通过宏来实现简化和统一。
使用方式简化
通过引入这样一个宏:
#define INTERFACE_CALL(P_OBJ, I_FIELD, METHOD, ...) \
(P_OBJ)->I_FIELD->METHOD(CONTAINER_OF(&(P_OBJ)->I_FIELD, typeof(*(P_OBJ)), I_FIELD), __VA_ARGS__)
调用形式可以在视觉上得到简化:
INTERFACE_CALL(&hus, dog_interface, bark, 5);
INTERFACE_CALL(&black, bird_interface, fly, 6);
INTERFACE_CALL(&air, dog_interface, bark, 6);
INTERFACE_CALL(&air, bird_interface, fly, 7);
使用方式再度简化
如果我们对使用再加一个约束:要求字段必须用特定的名字。(这个限制是否过于苛刻另谈)
比如bird_i
的接口字段必须叫bird_interface
,dog_i
的接口字段必须叫dog_interface
,
那么我们就可以对外提供更简洁的接口了:
#define BIRD_FLY(P_OBJ, ...) \
INTERFACE_CALL(P_OBJ, bird_interface, fly, __VA_ARGS__)
#define DOG_BARK(P_OBJ, ...) \
INTERFACE_CALL(P_OBJ, dog_interface, bark, __VA_ARGS__)
使用的时候,用户只需要这样调用接口:
DOG_BARK(&hus, 8);
BIRD_FLY(&black, 9);
DOG_BARK(&air, 10);
BIRD_FLY(&air, 11);
总结
此方案实现的“多接口”对比前面那篇文章的链表方案,优点在于:运行性能更高,实现原理也相对更简单。
但缺点也很明显:没有实现真正的动态化。调用者需要向实现者提供很多对象之外的额外信息,比如对象类型,接口字段名。
这个缺点无法有效避免,相关信息要么通过参数传进去,要么通过“约定”对实现者的代码做限制。 在C语言里,我们可以通过宏粗略地掩盖一下。