C语言“模板”编程模型
关于C++的模板技术,从未停止过争论,反对者与拥趸者态度截然不同,如果你是模板技术的反对者,那么对于本文的阅读可以到此return了。
模板(template)是C++程序设计语言中采用类型作为参数的程序设计,是泛型编程的基础。C语言中没有模板功能,但是有些场景非常适合用模板来编码实现,我们可以使用宏来模拟“模板”。
假设有这样一种情况,某个业务函数需要处理int、char、long、int64、float、double等多种不同类型参数,业务逻辑高度相似,如果不使用模板,每种类型参数都需要对应一个函数,代码看上去是不是很笨拙。这种情况在数据库软件中十分常见,成熟数据库软件支持的数据类型多达几十种,但业务逻辑十分相似。试想一下每种类型都对应一套代码,代码维护成本剧增。
业内有一款开源软件对于模板的使用达到了极致——ClickHouse,代码性能极高,有兴趣的可以下载源码研读。
一、待重构代码
假设有这么一个问题,现在有两种结构体,功能非常相似,每个结构体都有对应的处理函数,例如:
struct osan_iocmd
{
int start;
long flags;
};
struct osan_segcmd
{
int start;
long tags;
};
/* 以下代码为待重构 */
int get_iocmd_start(osan_iocmd* p_iocmd)
{
return p_iocmd->start;
}
int get_segcmd_start(osan_segcmd* p_segcmd)
{
return p_segcmd->start;
}
void demo()
{
……
int st1 = get_iocmd_start(p_iocmd);
int st2 = get_segcmd_start(p_segcmd);
}
以上代码片段,get_iocmd_start_time和get_segcmd_start_time业务逻辑一样,唯一区别就是函数参数类型不同。为此,我们维护了两个版本的函数。如果这种情况发生在实际代码场景中,针对这两种数据解构相应的处理函数非常多,每个函数都使用两个版本函数,代码的维护成本就整体翻倍了。
这种情况非常适合使用模板编程进行代码重构。
二、C++模板
先看一下如何使用C++模板重构代码。
创建头文件cmd_inc.h:
/* 使用C++模板重构 */
template <class T>
int get_cmd_start(T* p_cmd) // 函数模板声明与定义
{
return p_cmd->start;
}
函数测试文件demo.c:
#include "cmd_inc.h"
void demo()
{
……
int st1 = get_cmd_start<osan_iocmd>(p_iocmd); // 使用osan_iocmd实例化
int st2 = get_cmd_start<osan_segcmd>(p_segcmd); // 使用osan_segcmd实例化
}
我们用模板将两个版本函数合成了一个,代码变得更优雅了。
三、C语言模拟“模板”
C语言没有模板,我们使用宏模拟模板,达到与模板相似的功能。注意,无法完全实现模板特性。
创建头文件cmd_inc.h:
#ifndef _CMD_INC_H_
#define _CMD_INC_H_
……
// 此处放函数“模板”之外的代码
#ifndef // end of _CMD_INC_H_
/* 声明两个版本函数原型 */
int get_iocmd_start(osan_iocmd* p_iocmd);
int get_segcmd_start(osan_segcmd* p_segcmd);
#ifdef ENABLE_C_TEMPL
int get_cmd_start(CMD_TYPE* p_cmd) // 函数“模板”定义
{
return p_cmd->start;
}
#endif // end of ENABLE_C_TEMPL
创建函数“模板”文件cmd_inc.c:
……
// 上面是其它代码
#define ENABLE_C_TEMPL
/* 使用宏展开osan_iocmd版本函数定义 */
#define get_cmd_start get_iocmd_start
#include "cmd_inc.h"
#undef get_cmd_start
/* 使用宏展开osan_segcmd版本函数定义 */
#define get_cmd_start get_segcmd_start
#include "cmd_inc.h"
#undef get_cmd_start
#undef ENABLE_C_TEMPL
// 下面是其它代码
……
函数测试文件demo.c:
#include "cmd_inc.h"
void demo()
{
……
int st1 = get_iocmd_start(p_iocmd);
int st2 = get_segcmd_start(p_segcmd);
}
关于上述代码说明:
- 使用宏ENABLE_C_TEMPL启用函数“模板”, demo.c文件也include cmd_inc.h文件了,但是无需再定义模板函数,此时因为没有define该宏,所以,不会启用函数“模板”代码。
- cmd_inc.c文件中,我们include两次cmd_inc.h,每次define get_cmd_start不同来展开不同函数,也就是说,我们通过宏展开生成了get_iocmd_start和get_segcmd_start两个版本函数定义。
- 函数可以重复声明,所以,cmd_inc.h中的函数声明可以被多次include引入。
四、C语言“模板”的优缺点
优点:
- 功能重复,参数类型不同的函数仅保留一个版本,便于维护。
- 不影响debug时源码与指令对照。
- 编译期类型检查仍然有效。
缺点:
- 编码时,编辑器无法对“模板”参数进行智能提示,这也是C++模板的缺点。
- 使用模板需要把函数模板放在头文件里,这样会暴露函数实现,不适合用作接口,这同样是C++模板的缺点。
- 阅读代码时,contex不能直接关联到模板函数。可以对代码布局进行调整,将函数声明与模板函数书写在相近位置。