C/C++

C语言“模板”编程模型

0 条评论 编码 C/C++ FairyFar

  关于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);
}

  关于上述代码说明:

  1. 使用宏ENABLE_C_TEMPL启用函数“模板”, demo.c文件也include cmd_inc.h文件了,但是无需再定义模板函数,此时因为没有define该宏,所以,不会启用函数“模板”代码。
  2. cmd_inc.c文件中,我们include两次cmd_inc.h,每次define get_cmd_start不同来展开不同函数,也就是说,我们通过宏展开生成了get_iocmd_start和get_segcmd_start两个版本函数定义。
  3. 函数可以重复声明,所以,cmd_inc.h中的函数声明可以被多次include引入。

四、C语言“模板”的优缺点

  优点:

  • 功能重复,参数类型不同的函数仅保留一个版本,便于维护。
  • 不影响debug时源码与指令对照。
  • 编译期类型检查仍然有效。

  缺点:

  • 编码时,编辑器无法对“模板”参数进行智能提示,这也是C++模板的缺点。
  • 使用模板需要把函数模板放在头文件里,这样会暴露函数实现,不适合用作接口,这同样是C++模板的缺点。
  • 阅读代码时,contex不能直接关联到模板函数。可以对代码布局进行调整,将函数声明与模板函数书写在相近位置。