理解C++的函数重载,泛型编程和容器

-- TOC --

C++既具备高级的抽象编程风格,同时也具备底层的执行速度,C++编译器干了很多事情。而函数重载和泛型编程,也是C++编译器的杰作。

函数重载

重载用来实现函数接口名称相同,但是参数类型不同的一系列接口。相同的函数接口名称,带来了更优雅的代码,可读性更好。

#include <cstdio>


int add(int a, int b) {
    return a+b;
}

double add(double a, double b) {
    return a+b;
}

double add(double a, int b) {
    return a+b;
}

int add(int a, int b, int c) {
    return a+b+c;
}

int main(void) {
    printf("%d\n", add(1,2));
    printf("%f\n", add(1.2,3.4));
    printf("%f\n", add(1.2,9));
    printf("%d\n", add(1,9,99));
    return 0;
}

add被重载了4次,每次都是不同参数列表。不同不仅是类型不同,数量不同也OK。

如果参数类型和数量都相同,但是返回值不同,这就不可以了,是个错误,因为编译器在函数被调用的时候,可能无法定位具体调用的是哪个接口(比如调用了一个被overload的接口,但并没有使用其返回值)。

这就是函数重载,类中的成员函数也可以重载,原理一样,而且,重载的函数相互之间也可以调用。

泛型编程

仔细看看上面的4个重载的函数接口,他们内部代码其实都一样,这也是在写重复代码。此时,泛型编程登台。

所谓泛型,就是与具体类型无关。写一段代码,多种类型的数据都可以使用。将代码逻辑和数据类型分开。实现泛型编程的方式,就是使用C++模板。C++可以定义函数模板,也可以定义类模板

#include <cstdio>


template<typename T>
T add(T a, T b) {
    printf("two T add\n");
    return a+b;
}

double add(double a, int b) {
    printf("no template\n");
    return a+b;
}

template<typename T>
T add(T a, T b, T c) {
    printf("three T add\n");
    return a+b+c;
}

int main(void) {
    printf("%d\n", add<int>(1,2));
    printf("%f\n", add<double>(1.2,3.4));
    printf("%f\n", add(1.2,9));
    printf("%d\n", add<int>(1,9,99));

    // template type deduction
    printf("%d\n", add(1,2));
    printf("%f\n", add(1.2,3.4));
    return 0;
}

上面的测试代码,实现了两个函数模板,而且,函数模板也可以重载!

普通函数可以与模板函数相互重载!

C++还有类模板,即定义一个多种类型都适用的class。

代码中使用函数模板,就是模板名加上一个三角形括号,括号内是类型,编译器在编译的时候,会自动用类型来实例化模板

以上代码运行效果:

$ g++ -Wall -Wextra test.cpp -o test
$ ./test
two T add
3
two T add
4.600000
no template
10.200000
three T add
109
two T add
3
two T add
4.600000

这就是泛型编程,用得好的话,它可以让代码变得更少更优雅。

泛型主要是为了代码逻辑的复用,多种类型,只有一套实现逻辑。重载主要是保持接口名称不变,相同的接口,不同的参数个数和类型。

Python这种灵活的动态类型语言,天生就具备有函数重载的基因(通过判断参数类型走不通的代码分支),因为对鸭子类型的支持,支持泛型的多类型同一套代码逻辑也很OK。

注意上面代码中有一行注释,template type deduction。在使用函数模板的时候,可以忽略到<type>这部分,编译器可以自动通过参数类型的判断,定位到具体的接口。(就像auto关键词

容器

容器,container,是一类这样的对象,它可以持有其它对象或指向其它对像的指针,并在此基础上提供一系列通用的操作接口,这些操作接口在程序设计时,会频繁地被用到,所以容器带来一个好处, 就是容器类是一种对特定代码重用问题的良好的解决方案。

C++对容器的支持,体现在STL,Standard Template Library,标准模板库中。

实现容器,实际上就是通过类模板这个方法。既然是类模板,就可以用同一套类封装来支持多种类型。容器不关心具体类型,他只是封装接口,而接口就跟具体的数据结构有密切的关系。将容器中的不同类型的对象按什么样的数据结构组织起来,就成了对容器分类的方式。

STL的通用容器分三类:顺序性容器、关联式容器和容器适配器。

看一个实例吧:详解vector向量容器

Python中的list,set,dict,就是容器!

STL历史

Alexander Stepanov(后被誉为STL标准模板库之父,后简称Stepanov),1950年出生于前苏联的莫斯科,他曾在莫斯科大学研究数学,此后一直致力于计算机语言和泛型库研究。

在20世纪70年代,Stepanov开始考虑,在保证效率的前提下,是否能将算法从诸多具体类型中抽象出来?为了验证自己的思想,他和纽约州立大学教授 Deepak Kapur 以及伦塞里尔技术学院教授 David Musser 共同开发了一种叫做 Tecton 的语言,尽管这次尝试没有取得实用性的成果,但却给了 Stepanov 很大的启示。

在随后的几年中,他又和 David Musser 等人先后用 Schema 语言(一种 Lisp 语言的变种)和 Ada 语言建立了一些大型程序库。Stepanov 逐渐意识到,当时的面向对象程序设计思想中存在一些问题,比如抽象数据类型概念所存在的缺陷,他希望通过对软件领域中各组成部分的分类,逐渐形成一种新的软件设计的概念性框架。

1987年,在贝尔实验室工作的Stepanov开始首次采用C++语言进行泛型软件库的研究。由于当时的C++语言还没有引入模板的编程技术,泛型库只能是通过C++的继承机制来开发,代码表达起来非常笨拙。

但尽管如此,Stepanov还是开发出了一个庞大的算法库。与此同时,在与Andrew Koenig(前ISO C++ 标准化委员会主席)和Bjarne Stroustrup(C++语言创始人)等顶级大师们的共事过程中,Stepanov开始注意到C/C++语言在实现其泛型思想方面所具有的潜在优势。

就拿C/C++中的指针而言,它的灵活与高效运用使后来的STL在实现泛型化的同时更是保持了高效率。另外,在STL中占据极其重要地位的迭代器便是源自于C/C++中原生指针的一般化推广。

1988年,Stepanov开始进入惠普的Palo Alto实验室工作,在随后的4年中,他从事的是有关磁盘驱动器方面的工作。直到1992年,由于参加并主持了实验室主任Bill Worley所建立的一个有关算法的研究项目,才使他重新回到了泛型化算法的研究工作上来。(大佬也还是要优先工作啊...)

项目自建立之后,参与者从最初的8人逐渐减少,最后只剩下Stepanov和Meng Lee两个人。经过长时间的努力,最终完成了一个包含有大量数据结构和算法部件的庞大运行库(HP版本的C++ STL),这便是现在STL的雏形。

1993年,当时在贝尔实验室的Andrew Koenig看到了Stepanov的研究成果,在他的鼓励与帮助下,Stepanov于1993年9月在圣何塞为ANSI/ISO C++标准委员会做了一个题为《The Science of C++ Programming》的演讲,向委员们讲述了其观念。然后又于1994年3月,在圣迭戈会议上向委员会提交了一份建议书,以期将STL通用库纳入C++标准。

尽管这一建议十分庞大,以至于降低了被通过的可能性,但其所包含的新思想吸引了许多人的注意力。随后在众人的帮助之下,包括Bjarne Stroustrup在内,Stepanov又对STL进行了改进,同时加入了一个封装内存模式信息的抽象模块,也就是现在STL中的allocator(内存分配器),它使STL的大部分实现都可以独立于具体的内存模式,从而独立于具体平台。

最终在1994年的滑铁卢会议上,委员们通过了提案,决定将STL正式纳入C++标准化进程之中,随后STL便被放进了会议的工作文件中。自此,STL终于成为C++家族中的重要一员。

此后,随者C++标准的不断改进,STL也在不断地做着相应的演化。直至1998年,ANSI/ISO C++标准正式定案,STL始终是C++标准库不可或缺的重要组成部分。

标准C++程序员,都应该熟练掌握STL的使用。STL代码是开源的。

本文链接:https://cs.pynote.net/sf/c/cpp/202209142/

-- EOF --

-- MORE --