泛型编程的含义

在编程的时候,总会遇到 同样的逻辑但是所需的类型不同 的问题。理想情况下,算法应是和数据结构和类型无关的,各种特殊的数据类型理应做好自己分内的工作

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操作、复制操作…
  • 标准化数据容器的操作:查找算法、过滤算法、聚合算法…
  • 标准化类型上的特有操作,需要有标准化的接口来回调不同类型的具体操作…

回到开头的那句话,泛型编程的本质—-屏蔽掉数据和操作数据的细节,让算法更为通用,让编程者更多地关注算法的结构,而不是在算法中处理不同的数据类型。