C语言的模板与泛型编程你了解吗
作者:日天少年wzh
模板与泛型编程浅谈
摘要(Effective C++):
C++template的最初发展动机很直接:让我们得以建立“类型安全”的容器如vector,list和map。然而当愈多人用上templates时,他们发现template有能力完成愈多可能的变化。容器当然很好,但泛型编程(generic programming)——写出的代码和其所处理的对象类型彼此独立——更好。STL算法如for_each,find和merge就是这一类编程的结果。最终人们发现,C++template机制自身是一部完整的图灵机:它可以用来计算任何可计算的值。于是导出了模板元编程(template mataprogramming),创造出“在C++编译器内执行并于编译完成时停止执行”的程序。
模板与泛型编程简单介绍
面向对象编程(OOP)和泛型编程都可以处理编写程序时不知道类型的情况;二者的不同之处在于:OOP能处理类型在程序运行之前都未知的情况;而在泛型编程中,在编译时就能获知类型了
我们所常用的STL标准库中,每一个容器都提供了单一的,泛型的定义,例如我们所常用的vector,我们可以定义很多类型的vector
vector<int> vi; // vi是装载int类型的vector容器的实例 vector<string> vs; // vs是装载string类型的vector容器的实例 vector<double> vd; // vd是装载double类型的vector容器的实例
模板是泛型编程的基础,一个模板就是一个创建类或者函数的蓝图或者公式
函数模板
// 简单的比较函数模板 template<typename T> int cmp(const T& v1,const T& v2) { if(v1<v2) return -1; else if(v1>v2) return 1; else return 0; }
函数定义以关键字template开始,后跟一个模板参数列表,这是一个逗号分隔的一个或多个模板参数的列表,用尖括号包围起来
**注:**在模板定义中,模板参数列表不能为空
模板参数列表表示在类或函数定义中用到的类型或者值。当我们使用模板的时候,我们可以(显式或隐式地)指定模板实参,将其绑定到模板参数上
简单了解模板的实例化过程
众所周知,当你觉得模板编程十分智能的时候,一定是有东西在为你负重前行,C++提供了模板与泛型编程的这个能力,这便意味着有一个东西在为你动态地实现模板的功能,而这一定是比C++这个高级语言层面更为底层的东西,而我们所了解的知识中,比C++高级语言较为底层的东西,除了操作系统,便是编译器了。
当我们调用一个函数模板的时候,编译器(通常)用函数实参来为我们推断模板实参。简单来讲,便是我们在调用函数模板的时候,编译器通过使用实参的类型来确定绑定到模板参数T的类型
cout<<cmp(1,0)<<endl; // T为int
在上诉代码中,函数cmp的实参类型是int,编译器便会推断出模板实参为int,并将它绑定到模板参数T上
简单来说,编译器用推断出的模板参数来为我们实例化一个特定版本的函数
模板编译
当编译器遇到一个模板定义的时候,它并不会生成代码。只有我们实例化出模板的一个特定的版本时,编译器才会生成其对应的代码。当我们使用(而不是定义)模板时,编译器才会生成代码。这个特性影响我们如何组织代码以及错误何时才可以被检测到
通常来说,我们将类定义和函数说明放在头文件中,而普通函数和类的成员函数的定义放在源文件中
模板则不尽相同:为了生成一个实例化的版本,编译器需要掌握函数模板或类模板成员函数的定义
总结:与非模板代码不同,模板的头文件通常既包括声明也包括定义,即函数模板和类模板成员函数的定义通常放在头文件中
大多数编译错误出现的时机
- 第一阶段,编译模板本身时,该时期所出现的错误大多数为语法错误
- 第二阶段,编译器遇到模板使用时
- 第三阶段,模板实例化时,而只有在这个阶段才能发现类型相关的问题
**注意事项:**保证传递给模板的实参支持模板所要求的操作,以及这些操作在模板中能正确的工作,是调用者的责任
类模板
类模板是用来生成类的蓝图的。与函数模板不同之处是,编译器不能为类模板推断模板参数类型。 所以我们必须在模板名后的尖括号中提供额外的信息——用来替代模板参数的模板实参列表
vector<int> vi; deque<double> dd; pair<string,int> key_val;
定义类模板
template<typename T> class T_vector { public: typedef T value_type; // 构造函数 T_vector() =default; T_vectot(std::initializer_list<T> il); // 容器的元素数目 size_type size() const { return data->size(); } bool empty() const { return data->empty(); } // 添加元素 void push_back(const T& val) { data->push_back(val); } void push_back(T &&val) { data->push_back(std::move(val)); } private: std::shared_ptr<std::vector<T> > data; // 若data[i]无效,则抛出msg void check(size_type i,const std::string &msg) const; }
类似函数模板,类模板以关键字template开始,后跟模板参数列表。在类模板(及其成员)的定义中,我们将模板参数当作替身,代替使用模板时用户需要提供的类型或值
**注:**一个类模板的每一个实例都形成一个独立的类,而类模板的每个实例都有其自己版本的成员函数
所以,我们可能会出现一个单一模板并不能满足所有类型的需求,而模板特例化就出现了
类模板成员函数的实例化
默认的情况下,一个类模板的成员函数只有在程序用到它的时候才会实例化
// 实例化T_vector和接受initializer_list<int>的构造函数 T_vector<int> T_vi = { 0,1,2,3,4,5 };
如果一个成员函数没有被使用,则它将不会被实例化
为什么我们需要模板特例化?
当我们编写单一的模板时,使其对任何可能的模板实参都是最合适的,都能实例化,但者往往都是过于理想化的情况。在某些特殊的情况下,通用的模板的定义可能对特定的类型是不合适的,通用定义的模板可能会出现编译失败或者做得不够完善的情况。
故,当我们不能(或者不希望)使用模板版本的时候,可以定义类或函数模板的一个特例化版本
定义函数模板特例化
// 原先cmp函数的特殊版本,用来处理特殊的字符数组的指针template<>int cmp(const char* const& p1,const char* const& p2) { return strcmp(p1,p2);}// 原先cmp函数的特殊版本,用来处理特殊的字符数组的指针 template<> int cmp(const char* const& p1,const char* const& p2) { return strcmp(p1,p2); }
函数重载与模板特例化的区别
当定义函数模板的特例化版本时,我们本质上接管了编译器的工作。即,我们为原先的模板的其中一个特殊的实例提供了定义。简而言之,特例化的本质是实例化一个模板,而非重载它,因此特例化并不影响函数匹配
注意事项:
- 为了特例化一个模板,原模版的声明必须在作用域中
- 在任何使用模板实例的代码之前,特例化版本的声明也必须在作用域中
- 所有同名模板的声明应该放在前面,然后是这些模板的特例化版本
类模板部分特例化
与函数模板不同的是,类模板的特例化不必为所有模板参数提供实参。一个类模板的部分特例化本身是一个模板,使用它时用户还必须为那些在特例化版本中指定的模板参数提供实参
注:我们只能部分特例化类模板,而不能部分特例化函数模板
总结
本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注脚本之家的更多内容!