05-模板-同功能不同类型的抽象
模板:同功能、不同类型的抽象
封装解决了「很多地方做同一件事」的重复;但还有另一种重复——同一件事,只是类型不同。写十个几乎一样的函数,只因为参数是 int、float、string、结构体……模板把「类型」也变成参数,让一个函数覆盖所有类型。这是和原子化接口正交的另一条线。
一、先看一种被忽略的重复
原子化接口线解决了这种重复:
1 | open(path); // 到处都是 |
到处复制的是同一个调用。封装一次,处处复用。
但还有另一种重复,形态完全不同——同一段逻辑,参数类型不同:
1 | // 从文件偏移处读一个 int |
三个函数,逻辑完全相同:pread → 检查字节数 → 异常处理。不同的只有一个词:类型。
复制粘贴的代价不在当下,在以后:
- 改异常策略(
throw→ 返回错误码)→ 三个函数都要改; - 加一个类型(
double)→ 再复制一遍; - 有一处忘了同步修改 → 悄悄不一致。
这是「同功能不同类型」的重复——复制调用是横向重复,这种是纵向重复。
二、解法:把类型也变成参数
C 语言做不到,因为 C 的类型是硬编码的。C++ 给出模板:
1 | template<typename T> |
用法:
1 | int32_t a = read_at<int32_t>(fd, 100); // 读 int |
一个函数,覆盖所有类型。 逻辑只写一遍,类型作为参数传入。
注意它和普通参数的区别:
1 | 普通函数参数:运行时传入的值 → read_at(fd, 100) |
类型不是运行时数据,是编译期选择。模板在编译期针对每个用到的类型各生成一份代码——read_at<int32_t>、read_at<float> 编译后是两份独立的函数,只是源码里只写了一份。
三、模板封装原子化接口
回到原子化接口线的例子。safe_open、safe_mmap、safe_close 每个只针对一种底层接口,不需要模板。
但再往上一步——功能函数要「读文件里的任意类型」——就有了纵向重复。模板正好落在原子化接口之上:
1 | 功能函数 = 数据结构 + 算法 + 原子化接口 |
这就是「模板封装原子化接口」的含义:
1 | 模板 = 原子化接口的泛化 |
功能函数因此可以写一次:
1 | template<typename T> |
异常处理仍被收敛在 read_at 里,功能函数不需要每个类型各写一份。
四、两种模板:函数模板与类模板
模板不只在函数层面。
函数模板——把类型作为函数参数:
1 | template<typename T> |
类模板——把类型作为类的成员类型:
1 | template<typename T> |
选择依据和接口形态线一致:
1 | 无状态、单次调用 → 函数模板(如 max_of) |
模板是「接口形态演化」线在类型维度上的推广:函数导出 → 函数模板;类封装 → 类模板。
五、模板和两条主线的关系
模板解决的是重复,不是拆分。两条主线各有分工:
1 | 职责混乱 → 拆分(函数、文件、模块、系统) |
放到演化主线的收束里:
1 | 一条指令 → … → 函数(逻辑复用)→ 文件(编译单元)→ 分层(职责分离) |
主线「从指令到业务系统」讲的是结构怎么长出来(拆分);模板讲的是类型怎么被抽象(复用)——它不改变结构,只消灭同一结构下的类型重复。
六、模板不是万能的
模板是手段,不是目的。过度模板化的症状:
1 | ❌ 只为「可能以后要换类型」而模板化 |
判断标准:
1 | 现在就有多种类型 → 模板化(如容器、通用算法) |
软件组织原则也说得很清楚:「使用合适的抽象方式减少重复,而不是固定使用模板。」 普通函数、继承、组合、数据驱动都可以减少重复,模板只是其中一种——专门针对「类型」这一维度的重复。
七、和其他线的关系
1 | 模板 ←→ 原子化接口 |
收束
1 | 同功能不同类型 → 一堆几乎相同的函数(纵向重复) |
模板是「重复代码就封装」这条原则在类型维度上的延伸:前面所有线封装的是行为,模板封装的是类型。
