我对基于模板或类型的动态推导而实现的泛型编程方法是有看法的,这些看法可能有些极端。
我觉得无论是模板还是类型的动态推导,基于它们只能建立面向数据的泛型编程范式,而更好的泛型编程范式应该是面向运算。
例如下面这个 C++ 模板函数:
template<typename T> T const & max_element(T const *array, unsigned int n) { T const *max_value = array; for (unsigned int i = 1; i < n; i++) { if (array[i] > *max_value) { max_value = &(array[i]); } } return *max_value; }
虽然它的理想是浪漫的,从所有类型的值构成的数组中获取最大值,但这种理想就类似于古人的天圆地方的地理观,更多的是来自直觉,而非逻辑论证。
在代码中通过『占位符』的形式来实现泛型,这需要一个前提,那就是所『泛』的『型』要支持既定的运算。在 max_element
中,就是所『泛』的 T
类型,它必须能够参与 >
运算。显然,对于很多非数字形式的数据类型而言,它们是不支持 '>' 运算的,所以为了 max_element
能够工作下去, max_element
的用户必须去对 >
运算进行一些扩展,让它能够接受它原本无法接受的数据类型的值。所以,模板不仅没有解决泛型的根本问题,反而对这个问题进行了一些不必要的『掩盖』。
动态语言中的『泛型』也存在类似的问题。例如下面的 python
版本的 max_element
函数:
def max_element(array): max_value = array[0] for e in array[1:]: if e > max_value: max_value = e return max_value
在 Python 中, array
中的元素可以是任意类型,并且 Python 的解释器能够识别出这些类型。动态类型,但是这些功能仅仅是消除了 C++ 中的类型占位符,让语法简洁了一些,对于不支持 >
运算的数据类型而言, max_element
的用户依然要想办法让自己所处里的数据能够支持 >
运算。
面向数据的泛型编程范式只能让一些简单类型的数据具备泛型运算能力,而对于现实中的大部分数据其泛型运算过程依然要交付给用户去实现。既然如此,那又何必去绕模板、类型推导以及运算符重载这个圈子?干脆将某种运算所涉及的数据类型都交给用户来处理就是了,这样不仅能够消减不必要的语法,同时也大幅避免了繁琐复杂的心智模型。
我觉得更好的泛型编程范式,应该像古老的 C 语言版本的 qsort
函数这样:
void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));
也许正是因为 C 语言在语言层面上不支持模板与运行时数据类型识别,所以 qsort
函数在设计上不必绕圈子,直接告诉用户:如果你想对存放在 base
数组里的数据进行快速排序,那么你必须告诉我应当如何对这个数组里的任意两个元素进行大小比较。
按照 qsort
这种风格,那么 C 版本的 max_element
可以像下面这样设计:
void * max_element(void *base, size_t size, int (*gt)(const void *, const void *));
gt
是一个函数指针,它指向用户为自己所处理的数据类型而定义的 >
运算函数。
也许很多人会抱怨,难道像 int
, float
, double
这样天生就能够参与 >
运算的数据类型也需要用户自行定义 gt
函数嘛?
理论上必须是这样。因为既然你选择了的是一个支持泛型运算的 qsort
或 max_element
,那么你就应该为自己的选择而付出代价。在现实中, qsort
或 max_element
的设计者如果想减轻用户的负担,他可以为常用的数据类型预定义相应的 compar
或 gt
。
人类已经基于 C++ 建立了许多庞大的库,特别是在数值运算、几何运算以及图形图像处理领域,例如 CGAL, PCL, OpenSceneGraph, OpenCV 等 C++ 库,它们不约而同的走上了面向数据的泛型道路,代码中充满了 C++ 模板的奇技淫巧……C++ Boost 库或许是模板的奇技淫巧的集大成者。
一个库,如果设计者真的希望它简单好用并且又能支持泛型运算,那么他首先应该去思考这个库所涉及的基本运算有哪些,这些基本运算中又有那些是与数据类型相关的,然后将这部分与数据类型相关的运算抽离出来,交给库的用户来填充。库的用户填充具体的运算的过程,就类似于在一个窗口程序中为鼠标点击按钮事件而增加处理函数……
像 qsort
那样基于 C 的函数指针所实现的东西,叫做『回调函数』,即用户调用了 qsort
函数,而 qsort
又调用了用户自己编写的一个函数。这是解决所有泛型运算问题的根本之道。
仔细观察 qsort
函数的 compar
参数,假设这个函数指针指向我编写的回调函数 str_compare
。下面是一个完整的 qsort
用法示例:
#include <stdio.h> #include <stdlib.h> #include <string.h> static int str_compare (const void *s1, const void *s2) { char *str1 = *(char **)s1; char *str2 = *(char **)s2; size_t l1 = strlen (str1); size_t l2 = strlen (str2); if (l1 > l2) return 1; else if (l1 == l2) return 0; else return -1; } int main (void) { char *str_array[5] = {"a", "abcd", "abc", "ab", "abcde"}; qsort (&str_array, 5, sizeof (char *), str_compare); for (int i = 0; i< 5; i++) printf ("%s ", str_array[i]); printf ("/n"); return 0; }
str_compare
函数所接受的参数值有些玄妙。如果你将自己想象为 str_compare
函数,你所接受的东西来自其他人。从你自身的角度来看,没有这些来自其他人的东西,你的存在就是没有意义的。从传递给你这些东西的人的角度来看,没有你的存在,这些值的存在也是没有意义的。无论从哪个角度来看,你与你所接受的东西都是不可分割的,也就是说你们构成了一个单元。这个单元就是所谓的『闭包』。
闭包只是个概念,与函数指针或者回调函数没有什么必然联系。实际上你在一个函数里调用的任何一个函数,后者总是能够与前者所提供的数据形成闭包的。因为这种现象普遍存在,所以有些编程语言在设计上,是将闭包作为一个真正的单元而实现的,也就是说在语法层面上支持闭包,就像 C++ 在语法层面支持模板那样。这类在语法层面上支持闭包的语言,通常被称为『函数式语言』,因为它们支持闭包,所以它们能够将函数作为值传递与返回。
我们每一个人,都像是一个闭包。所以,我们所生存的这个世界就具备了泛型运算的能力!