泛型编程的含义
在编程的时候,总会遇到 同样的逻辑但是所需的类型不同 的问题。理想情况下,算法应是和数据结构和类型无关的,各种特殊的数据类型理应做好自己分内的工作。
Generic programming centers around the idea of abstracting from concrete, efficient algorithms
to obtain generic algorithms that can be combined with different data representations to produce
a wide variety of useful software.
— Musser, David R.; Stepanov, Alexander A., Generic Programming
上面的话,翻译过来就是—- 屏蔽掉数据和操作数据的细节,让算法更为通用,让编程者更多地关注算法的结构,而不是在算法中处理不同的数据类型。
C语言泛型
c语言虽然在底层的微观控制上有很大的作用,但在泛型上做的不是很好。
swap 函数
交换两个变量的swap函数。
函数实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| /// 交换两个值
/// 给定了交换值的类型,如果需要交换别的类型的值,则需要另一个函数
/// \param x 传入参数指针
/// \param y 传入参数指针
void SwapInt(int *x, int *y) {
int temp = *x;
*x = *y;
*y = temp;
}
/// 交换两个值
/// 使用 void * 代替普通类型
/// 由于使用void * ,编译器不能通过类型得到类型的尺寸,添加 size 参数表示类型的长度
/// \param x 传入参数指针
/// \param y 传入参数指针
/// \param size 传入指针大小
void SwapVoid(void *x, void *y, size_t size) {
char tmp[size];
memcpy(tmp, y, size);
memcpy(y, x, size);
memcpy(x, tmp, size);
}
/**
* 交换两个值,宏定义模式
* 宏定义的方式有个弊端,在使用宏定义时,由于宏替换的缘故,会导致传入的参数发生改变
* 例如:
*
* #define min(x, y) ((x)>(y) ? (y) : (x))
*
* 在使用此宏时 int k = min(i++, j++);
* 由于宏替换的缘故,相当于执行了
*
* (i++) > (j++) ? (j++) : (i++)
*
* x 或 y 的值会被累加两次
* */
#define swap(x, y, size) {\
char temp[size]; \
memcpy(temp, &y, size); \
memcpy(&y, &x, size); \
memcpy(&x, temp, size); \
}
|
search 函数
搜索输入对象中是否含有目标。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| /// 在int 指针对象中搜索目标出现的位置,在此版本中传入对象的类型一定,目标一定
/// \param a 传入对象
/// \param size 对象长度
/// \param target 目标
/// \return 目标所在位置
int SearchNormal(int *a, size_t size, int target) {
for (int i = 0; i < size; i++) {
if (a[i] == target) {
return i;
}
}
}
/// 查找泛型函数
/// - 输入对象任意
/// - 偏移依靠元素 size * i 实现
/// - 对比函数由外界提供
/// \param a 传入对象
/// \param size 传入对象长度
/// \param target 目标
/// \param elem_size 传入对象元素大小
/// \param cmpFn 对比函数
/// \return 目标所在位置
int SearchParadigm(void *a, size_t size, void* target, size_t elem_size, int(*cmpFn)(void*, void*)){
for (int i = 0; i < size; i++) {
if(cmpFn((unsigned char *)a + elem_size * i, target) == 0){
return i;
}
}
}
/// int 类型比较操作
/// \param x 待比较的参数1
/// \param y 待比较的参数2
/// \return 比较结果, 0为相等
int int_cmp(int *x, int *y) {
return *x - *y;
}
/// char 类型比较操作
/// \param x 待比较的参数1
/// \param y 待比较的参数2
/// \return 比较结果, 0为相等
int string_cmp(char *x, char *y) {
return strcmp(x, y);
}
|
小结
在search函数中,传入了比较函数,这种方式将比较的过程由用户自己定义,能够适配更多类型的对象,自由度加大。
但在使用c语言时,在适配数据类型的过程中,只能使用void*
或宏
代替,这两种类型带来了很多其他的问题。
C++泛型
C++ 是支持编程范式最多的一门语言,解决了C语言泛型编程的问题。
- 通过类的方式
- 类中会有构造函数、析构函数表示这个类的分配和释放
- 拷贝构造函数,表示对内存的复制
- 操作符重载,用于比较大小、等于、不等于
- 通过模板达到类型和算法的妥协
- 模板特化会根据使用者的类型在编译时期生成那个模板的代码
- 模板可以通过一个虚拟的类型进行绑定,这样不会导致类型转换时的问题
- 通过虚函数和运行时类型识别
- 虚函数带来多态在语义上可以支持"同一类"的类型泛型
- 运行时类型识别技术可以做到在泛型时对具体类型的特殊处理
reduce函数
下面的例子是使用C++模板实现泛型编程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| template<class Iter, class T, class Op>
T reduce(Iter start, Iter end, T init, Op op) {
T result = init;
for (; start != end; start++) {
result = op(result, *start);
}
return result;
}
struct Employee {
string name;
string id;
int vacation;
double salary;
};
template<class T, class Cond>
struct counter {
Cond cond;
size_t operator()(size_t c, T t) {
return c + (cond(t) ? 1 : 0);
}
};
template<class Iter, class Cond>
size_t count_if(Iter begin, Iter end, Cond c) {
auto op = counter<typename Iter::value_type, Cond>{c};
return reduce(begin, end, size_t(0), op);
}
|
reduce函数的作用只是遍历迭代对象,reduce函数的传入参数init为结果的初始化值,op为对迭代对象的操作。这样做的好处在于,使得程序的扩展性得到了很大的提升。
例如现在需要计算所有工资的总和,只需要编写一个相加的函数即可。当使用别的功能函数后,又变成了新的功能。
1
2
3
4
5
6
7
8
9
10
11
12
| vector<Employee> v_employee;
Employee employee_001 = {"Tom", "001", 10, 10000.0};
v_employee.push_back(employee_001);
Employee employee_002 = {"John", "002", 7, 220.0};
v_employee.push_back(employee_002);
Employee employee_003 = {"Alfons", "003", 6, 9000.0};
v_employee.push_back(employee_003);
Employee employee_004 = {"Mich", "004", 1, 101000.0};
v_employee.push_back(employee_004);
double sum_salaries = reduce(v_employee.begin(), v_employee.end(), 0.0,
[](double s, Employee e) -> double { return s + e.salary; });
|
小结
C++的模板方式极大的扩展了c语言的泛型编程方式。reduce函数只是遍历迭代对象,真正的功能函数被当成输入参数,由用户传入,极大的扩展了用户的自由度。
类型系统与泛型编程
泛型编程的主要目的是 使得我们编写的代码不依赖于指定的类型,这自然绕不过 类型系统。
类型系统
类型系统 用于定义如何将编程语言中的数值和表达式归类为许多不同的类型,如何操作这些类型,这些类型如何互相作用。
一般来说,编程语言的类型有两种:
- 内建类型 - int、float、char等
- 抽象类型 - struct、class、function等
抽象类型在程序运行中,可能不表示为值。类型系统在各种语言之间有非常大的不同,最主要的差异在于 编译时期的语法,以及运行时期的操作实现方式。
实际上,类型系统 的存在根本原因还是对 内存的分配问题。
- 类型是对内存的一种抽象,不同的类型,会有不同的内存布局和内存分配的策略。
- 不同的类型有不同的操作,对于特定的类型,也有特定的操作。
- 对于 静态类型语言 来说
- 类型的检测发生在 编译器进行语义分析时进行的,这样做的好处在于,可以让编译器明确的知道程序员的意图,可以利用这一消息做很多代码优化工作,转换成更高效的机器指令,使得程序运行的更快。
- 在编译时期就可以较早的发现错误,并且增加运行时期的性能。
- 对于 动态类型语言 来说
- 类型的检测发生在 运行时期做动态类型标记和相关检测,所以,动态类型语言中会有很多类型判断的函数:is_int(),is_string()等。
- 程序员可以将更多的精力放在 程序业务、逻辑的处理上,能够更快的编写程序。但是会经常出现类型错误的问题。
泛型的本质
在编程中,我们需要处理好两件事:
- 编程语言中的类型问题
- 对真实世界中业务代码的抽象、重用和拼装
要做到泛型,我们需要做下面的事情:
- 标准化类型的内存分配、释放和访问。
- 标准化类型的操作:比较操作、IO操作、复制操作…
- 标准化数据容器的操作:查找算法、过滤算法、聚合算法…
- 标准化类型上的特有操作,需要有标准化的接口来回调不同类型的具体操作…
回到开头的那句话,泛型编程的本质—-屏蔽掉数据和操作数据的细节,让算法更为通用,让编程者更多地关注算法的结构,而不是在算法中处理不同的数据类型。
Author
Alfons
LastMod
2019-04-17
License
Creative Commons BY-NC-ND 3.0