这里是通过记录学习的纸质书籍《深入实践C++模板编程》,因此放上华章的连接:
《深入实践C++模板编程》
在我们日常学习C++的过程中,其实不难发现该语言是一门强类型的语言,是一个对于面向对象编程支持的非常好的语言。因此在强类型的要求下,如果只是单纯地遵守**“万物皆对象”**的法则,那么必然会出现同一种操作/算法在不同的对象里大量的重复实现的过程,或者有的程序员会采用,设计一个抽象的类型来专门设计容纳一些通用的操作和算法。
但是,不然是后者高级的设计,对于大量的对象来说,工作起来依然是看起来捉襟见肘的,那么对于一个C++程序员来讲,是否理解模板编程的原理以及是否掌握模板编程的方法是普通C++程序员与高级C++程序员的分水岭,今天我在此通过阅读《深入实践C++模板编程.温宇杰》本书,记录相关的学习过程,深入理解C++模板编程,进一步提升自己的编程能力,革新本人的C++编程思想。
该书主要包含以下知识点:
作者在工作实践中,几番研究与实践下来,作者尚不敢称对C++的模板编程掌握几分,却早已叹于其对作者编程思想革新起到的巨大的帮助。从“万物皆对象”,到如今渐渐变为关注设计容器以及抽象算法的过程,这是拜模板所赐。赞叹之余,不禁想尝试写一部书介绍C++中的模板编程,与人分享心得,于己则巩固琢磨。不论章节条理,只求为位到来。如果对读者略有裨益,便是幸甚。
其实,很多程序员对于分享自己的见解和认识都还是比较热衷的,相互学习各种思想,让自己对于技术有更大的提升和思考。对于书中的技术和见解,像作者希望的那样,希望能够做到“择其善者而从之,其不善者而改之”。
本部分的内容有:
C++可以被称为强类型语言,凡是值必有类型,凡是变量声明时必须声明其类型,且变量类型“一生”不变。这样就出现了,对于某些算法,针对不同数据其操作过程完全一致,只因为操作数据类型不同,在C++中需实现为不同的函数,难免重复了。
试想一个寻找最大值的案例,从一列值里面计算最大的值的操作,用Python这种动态类型的语言来做,算法代码就非常的简洁:
def max_element(l):
max_value = l[0]
for elem in l[1:] :
if elem > max_value: max_value = elem
return max_value
print(max_element([2, 0, 1, 1, 0, 8, 1, 8]))
print(max_element(['2011', 'August', '11', 'Thursday']))
Python实现上面的代码,不需要操心类型的前提下,会发现实现是非常简单的。但是,使用C++来实现一个通用的max_element函数就没有那么简单了,C++中变量的类型不可变,不能像Python那样用同一个函数、同一个变量处理不同的列表类型。如果没有使用C++的模板类型的话,对于整数数组,代码如下:
int max_element(int const *1, unsigned sz)
(
int max_value = 1[0];
for (unsigned i = 1; i < sz; ++i)
if (1 [i] > max_value) max_value = 1[i];
return max_value;
}
对于字符数组,则函数代码就改为:
char max_element(char const *1, unsigned sz)
{
char max_value = 1[0];
for (unsigned i = 1; i < sz; ++i)
if (1[i] > max_value) max_value = 1[i];
return max_value;
}
会发现,除了函数返回值、第一个函数参数类型有变化之外,其他的内容基本是完全一摸一样的。因此使用函数模板进行简化就是一个值得学习的思路和方法。因此,我们与模板的第一次亲密接触就应该从模板函数开始了。函数模板是C++模板机制中的一种,其作用是为不同类型的数据生成操作相同或相似的函数。
模板以关键字template开头,其后是以一对尖括号划分的模板参数列表。模板参数列 表中可以声明多个模板参数,多个参数声明之间以逗号分隔。使用函数模板实现就是以下的代码:
template<typename T>
T const& max_element(T const *l, unsigned sz)
{
T const *max_value(l);
for (unsigned i = 1; i < sz; ++i)
if (l[i] > *max_value) max_value = &(l[i]);
return *max_value;
}
上面的例子中函数模板的函数体与前面两份的函数代码非常相似,不同之处在于凡是有关列表类型之处,皆由模板类型参数T代替。 另外,考虑到列表元素可能为复杂自定义类型,其赋值会导致额外开销,在模板中将max_ value改为指针以避免无谓赋值。
那么,就可以使用函数模板的过程来模拟实现上面两个代码的过程了:
int main()
{
int l[] = {2, 0, 1, 1, 0, 8, 2, 5};
char cl[] = "August";
using namespace std;
cout << max_element<int>(l, 8) << endl;
cout << max_element<char>(cl, 6) << endl;
return 0;
}
可以像调用一个普通函数那样调用函数模板。不同之处在于,调用函数模板时需要指明模板参数的“值”。对于类型参数,其“值”即为具体类型如int、char或者是用户自定义的类。根据所给定的模板参数值以及完整的函数模板声明,编译器可自动生成一个对所需数据类型进行操作的函数,称为函数模板实例。模板参数的值在函数模板名后接尖括号内声明。
实际上,在C++语言中实现了这一自动推导模板参数值的功能。凡是可以推导出的模板参数"值”,就无需在模板实参列表中写明。因此,例1.4中main函数的两次max_ element调用,可以简写成以下形式:
std::cout << max_element(1, 8) << std::endl;
std::cout << max_element(cl, 6) << std::endl;
从而使得模板调用看起来与普通函数调用无异,也使代码看起来更整洁。
利用模板参数推导时需要注意以下几点:
下面是一个简单的多模板参数的函数使用案例:
#include
template<typename T0,
typename T1,
typename T2,
typename T3,
typename T4>
T2 func(T1 v1, T3 v3, T4 v4);
int main() {
double sv2;
using namespace std;
sv2 = func<double, int, int>(1, 2, 3);
cout << "\tsv2: " << sv2 << endl;
sv2 = func<double, int, int>(1, 2, 3);
cout << "\tsv2: " << sv2 << endl;
sv2 = func<double, int, int>(1, 0.1, 0.1);
cout << "\tsv2: " << sv2 << endl;
sv2 = func<int, double, double>(0.1, 0.1, 0.1);
cout << "\tsv2: " << sv2 << endl;
}
template<typename T0,
typename T1,
typename T2,
typename T3,
typename T4>
T2 func(T1 v1, T3 v3, T4 v4)
{
T0 static sv0 = T0(0);
T2 static sv2 = T2(0);
std::cout << "\tv1: " << v1
<< "\tv3: " << v3
<< "\tv4: " << v4
<< "\t|| sv0: " << sv0;
T2 v2 = sv2;
sv0 -= 1;
sv2 -= 1;
return v2;
}
模板实参列表中只可将T3及T4省略,而T0、T1及T2 不能省略。
最新的C++11标准允许为函数模板参数赋默认值,在为func中无法根据函数参数推导的模板参数赋予默认值后,调用模板时的模板实参列表可以完全省略。例如将func的声明改为以下形式:
tempiate< typename T0 = float,
typename Tl,
typename T2 = float,
typename T3,
typename T4>
T0 func (T1 vl, T3 v3, T4 v4)
既然编译器是在需要生成模板实例时自动生成,这就带来一个与传统C/C++编程习惯 的冲突,即函数模板中的函数体应该放在哪里。
按照C++语言习惯,普通函数及类的声明应该放在一个头文件(通常以h、hpp或者 hh为扩展名)里,而将其实现放在一个主代码文件(通常以c、cpp或者cc为扩展名)里,这样便于将代码分散编译到多个目标文件中,最后通过链接形成一个完整的目标文件。但是由于模板的实现是随用随生成,并不存在真实的函数实现代码,如果还是按照“头文件放声明,主文件放实现”的做法,则会导致编译失败。
template<typename T>
T const& func(T const &v) {return v;}
template int const& func(int const &v);
例中用到一种尚未介绍过的语法——明确生成模板实例。当关键字template后没有模板参数列表,而是一个函数声明时,意味着指示编译器根据此函数声明寻找合适的模板实现。当然,所声明函数必须与某一已知模板函数同名,并且其参数可用模板匹配。
例中将函数声明为T=int,从而在编译func2.cpp时,会在目标文件中生成 func< int >的代码而不会在链接时产生错误。但这只是权宜之计,倘若还需要func< float > 或 者func< char >,那么在代码文件中还得增加相应的语句,以促使编译器生成相应函数模板实例。如此一来, 又变成由人工生成模板实例,违背了当初由编译器随用随生成的初衷。
可见,虽然模板中的函数也可以有自己的声明和实现,但编译器不会在读到模板实现时立刻生成实际代码,因为具体的模板参数类型还未知,无法进行编译。对于编译器来说, 模板实现也是一种声明,声明如何自动生成代码。所以模板的实现也应该放在头文件内, 这样,在其他代码文件中可以直接将模板的实现也包含进来,当需要生成模板实例时,编译器可根据已知模板实现当场生成,而无需依赖在别的目标文件中生成的模板实例。
但这样会带来另一个问题,即重复模板实例。对此问题,C++标准中给出的解决方案是:在链接时识别及合并等价的模板实例。
caller1.cpp
//======================================
//文件名caller1.cpp
#include
template<typename T>
void func(T const &v)
{
std::cout << "func1: " << v << std::endl;
}
void caller1() {
func(1);
func(0.1);
}
caller2.cpp
//======================================
//文件名caller2.cpp
#include
template<typename T>
void func(T const &v)
{
std::cout << "func2: " << v << std::endl;
}
void caller2() {
func(2);
func(0.2f);
}
main.cpp
//======================================
//文件名main.cpp
void caller1();
void caller2();
int main()
{
caller1();
caller2();
return 0;
}
这两个目标文件再与main.cpp编译所得目标文件共同链接成可执行文件后会出现什么情况呢?
执行结果如下:
$ ./a.out
func1 : 1
func1: 0.1
func1: 2
func2: 0.2
由此例的运行结果可以推知,链接器不考虑函数具体内容,仅仅通过函数名、模板实参列表以及**参数列表等“接口”**信息来判断两个函数是否等价。
常情况下,根据函数接口判断等价函数实例并在链接时合并的简单方法,可以有效解决重复模板实例的问题。但正如例中所演示那样,使用这种方法也有弊端。倘若有不同的作者在写不同的模板库时,碰巧用到同一函数名以及相同的模板参数列表和函数形参列表,对于一些简单函数,这也是非常有可能的。
降低落入这一陷阱的可能性,最好的方法就是避免使用相同的函数名。此时,C++中的命名空间(namespace)机制就显得异常重要。
实际上,除了将模板的声明与实现一同放在头文件中编译之外,C++标准(98版)还 提供了另一种组织模板代码的方式——Export Template(暂且称为外名模板)。而定义与实现放在同一头文件中的模板可称为内名模板。
外名模板提倡将模板的声明与实现分别写在头文件和主文件内,如此实现模板需要在模板声明前加关键字export以标记外名模板,可以看如下代码:
square.hpp
//======================================
//文件名:square.hpp
export template<typename T>
T square(T const &v);
square.cpp
//======================================
//文件名:square.cpp
#include "square.hpp"
export template<typename T>
T square(T const &v) {return v * v;}
main.cpp
//======================================
//文件名:main.cpp
#include "square.hpp"
#include
int main()
{
std::cout << square(0.1f) << std::endl;
}
而在链接时,会因为无法找到main.cpp中 所需要的square< float >模板实例而报错。例如用GCC编译时情况大抵如此,并且GCC还会给出一个“不支持export”的警告。
由于实现成本高,而且需求可替代,使得外名模板不受编译器开发者的青睐并且最终 被逐出最新的C++11标准。虽然如此,这样一种设计思想仍然值得了解,并且现在也有完全支持外名模板的编译器可供使用。对于看重编译速度及目标码质量而不太看重代码移植 性的开发者来说,外名模板仍然是一种不错的选择。
本章所讨论的只是C++模板编程中的一小部分——函数模板而已。通过对函数模板编译过程的介绍,希望能对读者理解模板的本质有所帮助。
模板本身不是可编译的代码,而是用来指导编译器生成可编译代码的文本。函数模板 实际上是提取一系列具有相同(或者近似)操作流程的函数中的共性并给予规范描述,从而使得编译器可以在需要时,根据描述自动生成相应可编译代码并编译。有了模板的支持, 程序员的工作从体力劳动向着脑力劳动又前进了一小步。但是如果仅仅是函数模板而已, 这一小步并不会走岀太远,仍然有许多问题是单纯用函数模板无法解决的,另一小步便是第2章将要介绍的类模板及其功用。
成长,就是一个不动声色的过程,一个人熬过一些苦,才能无所不能。