深入实践C++模板编程(txt+pdf+epub+mobi电子书下载)


发布时间:2021-01-26 21:33:19

点击下载

作者:温宇杰

出版社:机械工业出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

深入实践C++模板编程

深入实践C++模板编程试读:

前言

为什么要写这本书

笔者在工作中最常用的编程语言就是C++。在最初接触C++语言的时候,只当它是C语言与面向对象编程的组合,在写代码时也是积极实践“万物皆对象”的法则,不管什么操作,总要将其在某个对象的某个成员函数中实现才满意。但在长年的编程实践过程中,却常常对面向对象的设计方法产生怀疑。

面向对象的设计思想,是将与某个数据类型相关的操作与该数据类型捆绑在一起,构成一个整体。然而,有时候操作并不完全依赖于数据类型。同一种算法,可能适用于不同的数据类型。如果硬要遵循“万物皆对象”的法则,那么只能将其在各个具体数据类型中重复实现,或是设计一个抽象的类型专门容纳算法。无论怎样,总显得有些“削足适履”。

而在实践中最苦恼的事情是,虽然算法固定,但是只因数据结构不同,每当引入新数据时,只得将算法重写一遍,徒费人工。高手看到此,自然就会想到将算法写成模板。也正因有此困扰,笔者才开始正视C++语言中的“模板编程”,一旦走入这套精密强力却又不广为人知的机制,方知此处别有洞天。

几番研究与实践下来,笔者尚不敢称对C++的模板编程掌握几分,却早已叹于其对笔者编程思路革新起到巨大的帮助。从“万物皆对象”,到如今渐渐变为关注设计容器及抽象算法,这是拜模板所赐。赞叹之余,不禁想尝试写一本书介绍C++中的模板编程,于人分享心得,于己则巩固琢磨。不论章节条理,只求娓娓道来。如能对读者略有裨益,便是幸甚。读者对象

本书假定读者对C++的语法有基本了解,要求仅此而已。笔者尽力将C++语言中这一部分非常有趣的内容,用详尽的示例以及平实的语言展现在读者面前。即使是刚刚踏入C++队伍的新兵,相信也能跟随本书的进度,渐渐领略这一片C++秘境。而对于久经沙场的老兵,笔者限于自身阅历有限,不敢夸口定能有所提高,唯有以诚意将所思所得写下,请诸位老兵“择其善者从之,其不善者改之”。

本书面向了解C++基本语法并有C++编程经历的读者。对C++模板编程技术、标准模板库用法和原理感兴趣的读者,可以将本书作为一本入门与提高的书使用。对于经常开发中小型C++代码库的读者,本书中有关模板高级编程技巧的内容可为您提供有益的补充。另外,本书中有关C++11新标准与模板技术的内容,也可作为对新标准的概要介绍供您参考。如何阅读本书

本书的内容分为四个部分。

模板基础(第1~4章)介绍模板编程的基本概念与用法,并着重讨论编译器对模板的具体实现方法及其局限。学习完本部分之后,读者可以理解模板的基本原理并自行实现简单的类模板与函数模板。

标准库中的模板(第5~9章)介绍标准库中的算法与容器,并对其实现细节进行仔细推敲。算法与容器可以说是C++模板编程的最典型用法。C++程序员可以不知道模板编程技术,但不可能不用到由模板写成的标准算法与容器。了解其实现原理的重要意义不言而喻。学习完本部分,读者将能洞悉标准库容器的实现原理,对标准库中的算法、迭代器与容器之间的关系有深入的理解,从而可以精确调节标准容器的行为以及自行开发适用于标准算法的容器类模板等。

模板编程高级技巧(第10~13章)讨论模板编程中“概念”的设计、控制代码量的技术、编译期逻辑的控制以及元编程的基本方法等。学习完本部分,读者可以开发规模更大、更加智能的模板库,并利用元编程技术实现编译期的逻辑演绎与类型推导。

模板与C++11(第14~16章)介绍新标准C++11中的重要改进及其对模板编程的影响。在学习完本部分之后,读者将能够准确把握C++11的几大重要革新的用意和用法,为利用新一代标准进行软件开发打下坚实基础。勘误和支持

除封面署名外,本书编写工作的还有:陈凯、石少华、刘宏业、林文雯、王春、张敏、付强。由于笔者学识与经历有限,书中内容难免有偏颇及谬误之处。笔者将秉持谦虚的态度和开放的胸襟,认真对[1]待各位读者的批评与指教。书中的全部源文件可以从华章网站下载,如您对本书内容有任何意见和建议,或者有关C++和程序设计的任何话题,欢迎发送邮件至yjwen.ty@qq.com和通过新浪微博@宇杰W与笔者交流。

[1]登录华章网站www.hzbook.com本书页面下载完整代码。致谢

感谢我的好友张哲君。作为本书草稿的第一位读者,你的意见与鼓励是我写作本书的动力之源。

感谢机械工业出版社华章公司的各位同仁为本书的出版付出的辛勤劳动。特别要感谢杨福川编辑和白宇编辑,本书从草稿到成书的过程,离不开你们的精心指导与耐心审校。

最深深的谢意送给我的母亲和我的妻子。没有你们在我身边一直默默地支持和付出,不会有提笔写作的我,也不会有本书存在。

最美好的祝愿送给我即将出生的孩子们。愿你们快乐成长,并拥有淡定从容的人生。

最后,借此机会,向C++语言的发明者BjarneStroustrup教授、标准模板库的主要作者Alexander Stepanov和Meng Lee致以崇高的敬意。温宇杰第一部分模板基础本部分内容:❑Hello模板❑类亦模板❑模板参数类型详解❑凡事总有“特例”第1章 Hello模板

C++可称为强类型语言,凡值必有类型,凡变量声明时必声明其类型,且变量类型“一生”不变。除少数预设类型外,不同类型值之间不能自动转换,除非有用户自定义的相关构造函数和类型转换函数。如此则编译时已知各变量所占内存大小,编译器可精确规划变量值在内存与寄存器间之调度,提高运行效率。而其弊端是代码无法独立于类型之外。对于某些算法,针对不同数据其操作过程完全一致,只因所操作数据类型不同,在C++中需实现为不同的函数,难免重复。1.1 为什么需要模板

以从一列值中找出最大值为例,其算法可概述为“设置一个变量max_value,从列中选择某一元素值作为max_value初值;然后将max_value与列中其余元素一一比较,更新max_value为其中较大值,待全部元素比较完毕,则max_value的值即列中最大值”。以上叙述都不涉及具体元素类型,可见算法描述可脱于类型之外。

如果以某种动态类型语言,比如Python,来实现此算法则非常简洁。在Python中,一个从序列中寻找最大值并返回的函数其代码可写[1]成以下形式:

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中变量类型随所赋值而变,因此函数max_element的参数l既可接纳整数列表,又可接纳字符串列表。又因为在Python中字符串默认按字典顺序比较大小,所以无论是整数列表还是字符串列表都适用上述代码。而函数中变量max_value既可接受“整数”值,也可接受“字符串”值。正因变量类型可变,同一函数可用于在各种列表中寻最大值。

但使用C++实现一个通用的max_element函数就不那么简单。由于C++中变量的类型不可变,不能像Python那样用同一函数、同一变量处理不同类型的列表。如果没有模板,则必须根据所操作列表类型确定函数参数以及内部各变量的类型。假如列表为一个整数数组,则函数代码可写成例1.1的形式。

例1.1

int max_element(int const*l, unsigned sz)

{

int max_value=l[0];

for (unsigned i=1;i<sz;++i)

if (l[i]>max_value) max_value=l[i];

return max_value;

}

假如列表为一个字符数组,则函数代码可写成例1.2的形式。

例1.2

char max_element(char const*l, unsigned sz)

{

char max_value=l[0];

for (unsigned i=1;i<sz;++i)

if (l[i]>max_value) max_value=l[i];

return max_value;

}

除函数返回值、第一个参数l以及局部变量max_value的类型以外,两个函数其余部分完全一样。只因列表数据类型不同,不得不将同一算法反复实现。如此机械繁复的工作,本应由机器代为完成。而利用C++的模板机制,便可根据给定类型自动生成所需函数。所以,我们与模板的“第一次亲密接触”,就从一个函数模板开始。

[1]Python中已有内建函数max()实现此功能。1.2 初识函数模板

函数模板是C++模板机制中的一种,其作用是为不同类型的数据生成操作相同或相似的函数。1.2.1 函数模板的实现

模板以关键字template开头,其后是以一对尖括号划分的模板参数列表。模板参数列表中可以声明多个模板参数,多个参数声明之间以逗号分隔。例1.3是一个可用于在不同类型的数组中寻找最大值的函数模板。

例1.3

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;

}

例1.3是将例1.1与例1.2中的函数max_element“模板化”。也就是将两者的不同之处——参数及内部数据的类型——提取出来作为模板的参数,然后编译器根据需要将模板参数替换为具体的类型,并根据模板自动生成针对所需类型进行操作的函数模板实例,即真实的函数。

模板参数有多种类型,在例1.3中只用到其中之一,即类型模板参数(有关其他模板参数将在之后详细介绍)。

类型模板参数以关键字typename或者class标记,后接参数名。两个关键字在标记模板参数类型时完全等价,都表示参数为类型参数,本书遵从习惯都用typename标记。参数列表之后为模板的内容。例1.3中声明的是一个函数模板,其后内容便是一个函数声明。

函数模板中的函数体声明与普通函数的写法完全相同。并且,模板参数列表中所声明的类型模板参数可当做一个已知类型来使用。例1.3中函数模板的函数体与例1.1、例1.2的函数代码非常相似,不同之处在于凡是有关列表类型之处,皆由模板类型参数T代替。另外,考虑到列表元素可能为复杂自定义类型,其赋值会导致额外开销,在模板中将max_value改为指针以避免无谓赋值。1.2.2 如何使用函数模板

模板可以指导编译器为特定的类型自动生成所需的函数或者数据结构。利用例1.3中的函数模板分别从一个整数数组及一个指针数组中搜索最大值的代码如例1.4所示。

例1.4

#include<iostream>

template<typename T>

T const&max_element(T const*l, unsigned sz)

{/*同例1.3,略*/}

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或者是用户自定义的类。根据所给定的模板参数值以及完整的函数模板声明,编译器可自动生成一个对所需数据类型进行操作的函数,称为函数模板实例。模板参数的值在函数模板名后接尖括号内声明。如例1.4中main函数中对max_element的调用,在函数名后紧跟<int>来声明模板参数的值是一个整数类型,指示编译器利用max_element模板生成一个可以处理整数数组的函数模板实例。而第二次调用max_element时,模板参数值是char字符型,则编译器可生成处理字符数组的真实函数。

可见,虽然C++无法如弱类型语言那样一个函数处理各种类型,但为各种类型写相似代码的重复性工作,可以借助模板交给编译器去完成。并且,由于是在编译期间完成依模板生成函数并链接的工作,与之相关的函数调用都是静态调用。较之弱类型语言(如Python)在运行时查看数据类型的动态方法,模板生成的静态调用其运行效率更高。1.2.3 模板参数自动推导

如果仔细观察例1.4中的两次对max_element的调用,就会发现模板参数值与函数调用给出的实参的类型是相关的。

比如第一次调用时给出的模板参数T=int,而调用时给出的实参l的类型是int[],即一个整数数组指针,与const int*等价。同样,第二次调用时模板参数T=char,而给出的实参cl类型也是char[]。对比max_element模板的函数参数列表中形参l的类型为T const*,无需显式指定,完全可以根据调用时的实参类型推导出模板参数值。

实际上,在C++语言中实现了这一自动推导模板参数值的功能。凡是可以推导出的模板参数“值”,就无需在模板实参列表中写明。因此,例1.4中main函数的两次max_element调用,可以简写成以下形式:

std::cout<<max_element(l, 8)<<std::endl;

std::cout<<max_element(cl, 6)<<std::endl;

从而使得模板调用看起来与普通函数调用无异,也使代码看起来更整洁。

利用模板参数推导时需要注意以下几点:

❑编译器只根据函数调用时给出的实参列表来推导模板参数值,与函数参数类型无关的模板参数其值无法推导。

❑与函数返回值相关的模板参数其值也无法推导。

❑所有可推导模板参数必须是连续位于模板参数列表尾部,中间不能有不可推导的模板参数。

例1.5是一个简单的程序。

例1.5

#include<iostream>

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;

}

在这个简单的程序中首先是一个函数模板func的声明。是的,模板与普通函数及类一样,可以先声明后实现。编译器在处理该模板声明时,已知这个模板接受5个模板参数,依次为T0~T4,其中用于声明函数参数类型的模板参数为T1、T3和T4。T2用于声明函数的返回值类型,T0则与函数参数类型以及返回值类型都不相关。编译器在遇到main函数中对func函数模板的调用时,根本无法从实参类型推导出T0的值,所以T0的值必须在模板实参列表中给定。至于T2,虽然与函数返回值类型相关,粗略一想,似乎可以在某些情况下反推出T2的值,例如:

double d=func<...>(...);

可否认为T2的值就应该是一个double型呢?其实不然。由于C++中存在内建自动类型转换机制,double型变量不仅可以接受其他double型变量或常量的赋值,也可以接受诸如int型、char型等多种类型变量常量的值,更何况还有用户自定义的类型转换。如果不在模板参数列表中显式给定函数返回值类型,则以上代码就会产生歧义。况且有的时候函数返回值有可能被忽略。因此函数返回值类型不能根据上下文推断,也就无法自动推导出与函数返回值相关的模板参数值,所以T2的值也必须在调用时给定。又因为T2必须给定,而模板形参与实参之间只能通过位置相关联,因此虽然T1的值可由函数实参类型推导出,也必须在模板实参列表中给出。很遗憾,C++中并不能支持类似以下的方式来跳过对T1赋值:

sv0=func<double, , int>(1, 2, 3);//语法错误!

所以,例1.5中调用func时,模板实参列表中只可将T3及T4省略,而T0、T1及T2不能省略。1.2.4 模板参数默认值

最新的C++11标准允许为函数模板参数赋默认值(此前只可为类模板参数赋默认值,第2章介绍)。在为func中无法根据函数参数推导的模板参数赋予默认值后,调用模板时的模板实参列表可以完全省略。例如将func的声明改为以下形式:

template<typename T0=float,

typename T1,

typename T2=float,

typename T3,

typename T4>

T0 func(T1 v1, T3 v3, T4 v4);

则在main函数中的调用可以写成以下形式:

func(1, 2, 3);

func('a', 'b',"cde");

但要编译带模板函数默认值的程序,需要支持最新标准的编译器。如笔者所用的GCC 4.6.1,需要在编译时开启参数“-std=c++0x”。

注意 为什么开启参数是c++0x?因为大家原本期望新标准可以在20世纪的第一个10年诞生,所以新一代C++标准原本的代号是C++0x。然而岁月在无休止的争论和审议中蹉跎,新标准最终通过ISO认定已经是2011年8月了,所以代号C++0x也改成了C++11。而在新标准通过之前,作者和厂商们已经提前使自己的编译器支持待定新标准加入到自己的编译器中。在标准出台后的编译器版本中(如GCC 4.7),开启新标准的选项已经改为“-std=c++11”。当然,为了向后兼容,旧选项依然有效。1.2.5 模板函数的静态变量

我们还是恢复到最通用的旧标准上。前面说过,编译器会根据函数模板自动生成函数实例。通过实际考查函数模板中的静态成员变量的实现方法,可以对这一机制进行侧面印证。例1.5中的func函数模板中有两个静态变量sv0和sv2。普通函数中的静态变量可以解释为函数自有,不同函数之间的同名静态变量不通用。那么,函数模板中的静态变量如何实现呢?在回答这个问题前,先看一下例1.5编译运行的输出,如下所示:

v1:1 v3:2 v4:3||sv0:0 sv2:0

v1:1 v3:2 v4:3||sv0:-1 sv2:-1

v1:1 v3:0.1v4:0.1||sv0:0 sv2:0

v1:0.1 v3:0.1v4:0.1||sv0:0 sv2:0

在func函数模板中,静态变量sv0和sv2每调用一次就自减1。所以如果调用的是同一函数的话,sv0和sv2的值会有变化。再看以上程序的输出,只有第二行的sv0与sv2的值有变化,其他三行都是初值0。这说明第二次调用func生成的函数与第一次(也只可能与第一次)调用的函数应该是同一函数,共用相同的静态函数变量;而第三、第四次调用的则是另外的函数。在四次调用中,总共调用了三个不同的函数,才会出现三组0值。再仔细检查四次调用时的模板参数值就会发现,第一次和第二次调用时模板各参数值都相同,为T0=double,而T1~T4都等于int。而第三、第四次调用时模板参数值均与之前不同。这说明,当有多个调用使用相同的模板参数值时,编译器只为此模板参数值组生成同一函数而将不同调用都链接到同一函数上。只要有任意模板参数值不同,编译器就会生成不同的函数实体以供链接。

编译器是否真如我们所说,为相同参数值生成唯一函数实体呢?编译生成的目标文件可做另一佐证。当然,方法依编译器及编译平台而不同。还是以笔者所用Linux系统中的GCC 4.6.1编译器为例,将例1.5的程序编译成可执行目标文件(最简单的方法是使用默认输出文件名a.out),使用命令nm查看其中符号表时可以发现有如下三个符号:

$nm-C a.out|grep func|grep W

0000000000400abc W int func<double, int, int, double, double>

(int, double, double)

00000000004009fb W int func<double, int, int, int, int>

(int, int, int)

0000000000400b81 W double func<int, double, double, double, double>

(double, double, double)

$

输出清晰地显示目标文件中只有三个func函数模板实例。根据其模板参数类型可以确定,编译器为同一组模板参数值只生成唯一函数实体。1.3 如何处理函数模板中的函数体

既然编译器是在需要生成模板实例时自动生成,这就带来一个与传统C/C++编程习惯的冲突,即函数模板中的函数体应该放在哪里。1.3.1 HPP文件还是CPP文件

按照C++语言习惯,普通函数及类的声明应该放在一个头文件(通常以h、hpp或者hh为扩展名)里,而将其实现放在一个主代码文件(通常以c、cpp或者cc为扩展名)里,这样便于将代码分散编译到多个目标文件中,最后通过链接形成一个完整的目标文件。但是由于模板的实现是随用随生成,并不存在真实的函数实现代码,如果还是按照“头文件放声明,主文件放实现”的做法,则会导致编译失败。

例如一个最简单的模板函数声明如下所示:

//文件名func.hpp

template<typename T>

T const&func(T const&v);

这个声明放在一个名为“func.hpp”的头文件中,其实现放在名为“func.cpp”的文件中,代码如下所示:

//文件名func.cpp

template<typename T>

T const&func(T const&v){return v;}

在一个名为“main.hpp”的文件中定义一个main函数用来调用func函数模板,代码如下所示:

//文件名main.cpp

#include"func.hpp"

int main(){func(0);}

正如我们通常安排普通函数的代码那样,如果单独编译这两个CPP文件都没有问题,但是在链接两个目标文件时链接器就会报错。在笔者的编译环境下出现的错误如下:

$g++func.o main.o

main.o:In function 'main':

main.cpp:(.text+0x17):undefined reference to`int const&func<int>(int const&)'

collect2:ld返回1

链接器报的错误是func<int>即func函数模板的某个实例未定义。按常理,这样一个函数的实现应该是在func.cpp编译出的目标文件中定义。但如果查看该目标文件(笔者用的是func.o)就会发现其中空空如也,并无任何函数定义。

回想一下模板的工作原理就不难理解这一现象。编译器在编译func.cpp时,只是读到了func函数模板的实现,并没有读到任何需要生成函数模板实例的语句,所以不会生成任何func函数的实例。而在编译main.cpp时,虽然用到了一个函数模板实例,但因为main. cpp只是将func.hpp头文件包含进来,而后者只有一个func函数模板的声明,并无具体函数体实现,此时编译器也无法生成func函数模板实例,只好预留一个调用链接,期望在最后的链接过程中可以找到函数实现。但很遗憾这样的实现并不存在,于是最后链接时出错。

稍微修改func.cpp中的代码,使其生成一个func<int>的函数实现,如例1.6所示。

例1.6

//文件名func2.cpp

template<typename T>

T const&func(T const&v){return v;}

template int const&func(int const&v);

例1.6中用到一种尚未介绍过的语法——明确生成模板实例。当关键字template后没有模板参数列表,而是一个函数声明时,意味着指示编译器根据此函数声明寻找合适的模板实现。当然,所声明函数必须与某一已知模板函数同名,并且其参数可用模板匹配。

例1.6中将函数声明为T=int,从而在编译func2.cpp时,会在目标文件中生成func<int>的代码而不会在链接时产生错误。但这只是权宜之计,倘若还需要func<float>或者func<char>,那么在代码文件中还得增加相应的语句,以促使编译器生成相应函数模板实例。如此一来,又变成由人工生成模板实例,违背了当初由编译器随用随生成的初衷。

可见,虽然模板中的函数也可以有自己的声明和实现,但编译器不会在读到模板实现时立刻生成实际代码,因为具体的模板参数类型还未知,无法进行编译。对于编译器来说,模板实现也是一种声明,声明如何自动生成代码。所以模板的实现也应该放在头文件内,这样,在其他代码文件中可以直接将模板的实现也包含进来,当需要生成模板实例时,编译器可根据已知模板实现当场生成,而无需依赖在别的目标文件中生成的模板实例。

但这样会带来另一个问题,即重复模板实例。1.3.2 链接器如何识别重复模板实例

假设将例1.6中函数模板func的实现也放在头文件func.hpp中,并且文件caller.cpp及main.cpp中各有函数caller及main都调用func生成实例func<int>,易知编译后的目标文件caller.o及main.o中各自都有func<int>实例。两个函数由同一模板生成,完全等价,则这两个函数为重复模板实例。

如果在最后链接步骤中不做特殊处理,则会在最终目标代码中存在多个等价的模板实例,造成目标文件尺寸的增加,尤其是在大量用到模板库时,这种情况会愈发严重。对此问题,C++标准中给出的解决方案是:在链接时识别及合并等价的模板实例。

那么,链接器如何识别等价的模板实例呢?答案见例1.7。

例1.7

//======================================

//文件名caller1.cpp

#include<iostream>

template<typename T>

void func(T const&v)

{

std::cout<<"func1:"<<v<<std::endl;

}

void caller1(){

func(1);

func(0.1);

}

//======================================

//文件名caller2.cpp

#include<iostream>

template<typename T>

void func(T const&v)

{

std::cout<<"func2:"<<v<<std::endl;

}

void caller2(){

func(2);

func(0.2f);

}

//======================================

//文件名main.cpp

void caller1();

void caller2();

int main()

{

caller1();

caller2();

return 0;

}

例1.7中用到3个代码文件。其中caller1.cpp和caller2.cpp中都有一个名为func的函数模板,且两个同名模板的模板参数也相同,都只有一个类型模板参数。但两个函数模板内容不同,区别在于打印出的前导字符串。此外,caller1.cpp和caller2.cpp中还分别声明两个函数caller1及caller2,其中都用到各自文件的func模板生成函数实例并调用。

细看代码便知,caller1.cpp编译所得目标文件中有func<int>及func<double>两个函数模板实例,而caller2.cpp编译所得目标文件中有func<int>及func<float>两个函数模板实例。这两个目标文件再与main.cpp编译所得目标文件共同链接成可执行文件后会出现什么情况呢?还是以笔者所用GCC编译器为例,如果用以下命令行编译:

$g++caller1.o caller2.o main.o-o a.out

执行程序的输出如下:

$./a.out

func1:1

func1:0.1

func1:2

func2:0.2

很有趣,在函数caller2()中本意是调用caller2.cpp中的func<int>,所以应该输出“func2:2”。但是由于caller1.cpp与caller2.cpp中均有func<int>实例,并且函数参数列表也相同(都为空),那么在链接时链接器基于函数名、模板实参列表以及参数列表判断两个函数模板实例等价,而将caller2.cpp中的func<int>除名。所有func<int>的调用都被链接到caller1.cpp中的func<int>实例。所以在以上程序输出第三行才会打印“func1:2”。而caller1()和caller2()中还分别调用了func<double>(无修饰浮点常数默认是double型)及func<int>。由于模板参数类型不同,这是两个不同的函数。链接器在链接时可以区分二者而做出如我们所想的链接。由此例的运行结果可以推知,链接器不考虑函数具体内容,仅仅通过函数名、模板实参列表以及参数列表等“接口”信息来判断两个函数是否等价。

实际上,编译器在编译函数模板实例时,将根据函数名、函数参数类型以及模板参数值等信息来重命名编译所生成的目标函数名,这一处理方式称为Name-Mangling。如果发现“接口”等价的函数(即编译后的函数名相同),则在最终可执行代码中只保留等价函数之一作为链接候选,而放弃其他等价函数。具体保留哪个函数是随机的,可能与用户输入有关。

比如在写链接命令时,将caller2.o放在caller1.o之前,如下所示:

$g++caller2.o caller1.o main.o-o a.out.2

程序运行结果会变为如下所示:

$./a.out.2

func2:1

func1:0.1

func2:2

func2:0.2

显然,因为命令行中文件顺序的关系,导致caller2.o中的func<int>先出现,而使得caller1.o中的func<int>实例被编译器放弃。

通常情况下,根据函数接口判断等价函数实例并在链接时合并的简单方法,可以有效解决重复模板实例的问题。但正如例1.7中所演示那样,使用这种方法也有弊端。倘若有不同的作者在写不同的模板库时,碰巧用到同一函数名以及相同的模板参数列表和函数形参列表,对于一些简单函数,这也是非常有可能的。又碰巧两个模板库用在同一项目的不同代码文件之中,则在最终链接时,有可能因为链接器的去重复功能而导致意外的链接结果,使得最终程序工作异常。降低落入这一陷阱的可能性,最好的方法就是避免使用相同的函数名。此时,C++中的命名空间(namespace)机制就显得异常重要。

模板库作者最好为自己的作品起一个独特的名字,并将所有模板库代码放在此命名空间内,例如所有的C++标准模板库代码都放在std命名空间内。即使名字很长,库的用户也可以通过为空间改名或者利用using语句显示引用所需函数等办法来降低代码量。只要两个库的命名空间不一样,库中的函数名就不会重复。除非用户采用以下方式强行将两库命名空间内的所有元素引入自己的空间,人为地制造命名冲突:

using namespace libA;

using namespace libB;

因此,无论是库开发者还是用户,管理命名的习惯至关重要。这不仅为了提高代码可读性,更是关系到编译结果是否正确。1.4 尴尬的Export Template

实际上,除了将模板的声明与实现一同放在头文件中编译之外,C++标准(98版)还提供了另一种组织模板代码的方式——Export Template(暂且称为外名模板)。而定义与实现放在同一头文件中的模板可称为内名模板。

外名模板的作用在于使得函数模板(或者第2章讲到的类模板)代码可依照C/C++语言的习惯,将声明与实现分别放在头文件与主文件中。但是为了支持外名模板,需要在编译与链接之间增加额外处理,这也使得外名模板成为C++标准的争论热点之一。

支持者认为外名模板使得模板代码组织更加模块化,并且可以享受模板代码所带来的一系列好处,例如多个模板实例共用同一本地函数等。而反对者认为,既然内名模板也可以在大部分情况下很好地实现编译意图,再因为外名模板而对编译工具流程做改动则无异于削足适履。

而现实是,外名模板虽然写入C++98标准,但少有主流编译器支持。这使得外名模板成了镜花水月一般,看起来很美,但是考虑到代码要跨平台可移植等,很少有人会用。并且在最新的标准C++11中,外名模板也被排除在标准之外,代之以使用关键字extern声明来阻止编译器在本目标文件中生成某些模板实例。

虽然外名模板即将成为(短期内的)历史尘埃,也不妨对其稍做介绍,至少可以给我们提供另一种实现模板的思路。1.4.1 什么是外名模板

外名模板提倡将模板的声明与实现分别写在头文件与主文件内,如此实现的模板需要在模板声明前加关键字export以标记为外名模板,如例1.8所示。

例1.8

//======================================

//文件名:square.hpp

export template<typename T>

T square(T const&v);

//======================================

//文件名:square.cpp

#include"square.hpp"

export template<typename T>

T square(T const&v){return v*v;}

//======================================

//文件名:main.cpp

#include"square.hpp"

#include<iostream>

int main()

{

std::cout<<square(0.1f)<<std::endl;

}

例1.8中有一个函数模板square,其作用是返回给定参数的平方值。该函数模板的声明和实现分别放在头文件square.hpp和主文件square.cpp中,并且被声明为一个外名模板,即声明以关键字export开头后接正常声明或实现。在文件main.cpp中调用该函数模板计算一个浮点数的平方并打印。

与通常的模板用法不同,在main.cpp中只是将square的声明包含进来,所以在编译main.cpp时,编译器无从得知模板如何实现,只好留一个函数调用以待在链接阶段可以找到对应函数链接。另外,文件square.cpp中只含有一个模板实现而无模板调用,一般编译器在编译此文件时不会生成任何函数实例代码。而在链接时,会因为无法找到main.cpp中所需要的square<float>模板实例而报错。例如用GCC编译时情况大抵如此,并且GCC还会给出一个“不支持export”的警告。1.4.2 C++编译器对外名模板的处理

据笔者所知,公开发布的支持外名模板的C++编译器有Comeau C++和Borland C++。本节以Comeau C++编译器como为例,简单介绍一下对外名模板的处理方式。

在编译main.cpp时,como的处理与其他编译器无太大差异,也是生成一个对square<float>的调用等待链接。而在编译square.cpp时,由于square模板声明是一个外名模板,虽然como不会为其生成任何模板实例代码,但是会额外生成一个square.et文件,其中包含对square函数模板实现的索引信息。之后进入一个预链接(prelink)阶段。在此阶段,编译器将根据之前编译时发现的对模板实例的需求,从所有ET文件中查找到所需模板实现所在代码文件(CPP文件),并重新编译出所需模板实例。例如例1.8中,在main. cpp中调用了square<float>。那么como将从square.et文件中找到模板square的实现在文件square.cpp中,然后重新编译square.cpp以生成square<float>供链接使用。随后的链接过程与其他C++编译器类似,最终形成链接完整的可执行文件。

可见,由于外名模板将模板的声明与实现分布在不同代码文件中,使得编译器不能像处理内名模板那样当场生成模板实例。因此需要在编译与链接之间加入一个预链接步骤以补全所缺模板实例。内名模板是在编译时当场生成实例随后在链接时消除重复。与此方式相比,在预链接时才根据需要生成模板实例的外名模板方式,由于避免了在多个目标文件中生成等价的模板实例的情况,可以缩短编译时间。这也是外名模板的优势之一。而其劣势主要在于对编译器设计的冲击。为了支持外名模板,需要对现有编译器软件做出非常大的改动,其人力与时间成本不可忽视。况且,即使没有外名模板,采用传统的内名模板编译去重复的方式也可以解决问题。

由于实现成本高,而且需求可替代,使得外名模板不受编译器开发者的青睐并且最终被逐出最新的C++11标准。虽然如此,这样一种设计思想仍然值得了解,并且现在也有完全支持外名模板的编译器可供使用。对于看重编译速度及目标码质量而不太看重代码移植性的开发者来说,外名模板仍然是一种不错的选择。1.5 本章小结

本章所讨论的只是C++模板编程中的一小部分——函数模板而已。通过对函数模板编译过程的介绍,希望能对读者理解模板的本质有所帮助。

模板本身不是可编译的代码,而是用来指导编译器生成可编译代码的文本。函数模板实际上是提取一系列具有相同(或者近似)操作流程的函数中的共性并给予规范描述,从而使得编译器可以在需要时,根据描述自动生成相应可编译代码并编译。有了模板的支持,程序员的工作从体力劳动向着脑力劳动又前进了一小步。但是如果仅仅是函数模板而已,这一小步并不会走出太远,仍然有许多问题是单纯用函数模板无法解决的,另一小步便是第2章将要介绍的类模板及其功用。第2章 类亦模板

学习软件开发的朋友,一定听说过这句名言:数据结构+算法=程序(Niklaus E. Wirth)。通过第1章的讨论我们知道,借助函数模板,算法描述可以脱于具体类型之外。借助C++语言中的另一大模板功能——类模板,数据结构的描述同样也可以脱于数据类型之外,而只专注于数据存取方法及数据间的关联方式。2.1 类型无关的数据结构

以最常见的栈(stack)为例,栈是一种先入后出队列,其所存数据总数不定,一般可由链表来实现。由于栈只可在栈首存取数据,无需对链表中间数据进行操作,所以最简单的实现方式是单向链表。

单向链表由一系列的节点构成。节点包含两个数据,一个是该节点所存数据,另一个是一个指向链表中下一节点的指针。可见,抽象的数据结构描述也可以与具体的类型无关。但在C++中,如果没有模板,则会因具体类型不同而不得不将数据结构一一实现。例如同样是链表节点,如果一个要保存整数值而另一个要保存浮点数值,则其节点类分别写成以下形式:

struct int_node

{

int value;

int_node*next;

};

struct float_node

{

float value;

float_node*next;

};

显然,两个节点类很类似,都有两个成员变量分别名为value和next,并且next是一个指针。只因所存数据不同而使其成员变量类型不同而已。

我们已经领略过函数模板为我们展开的与类型无关的算法描述图景。现在,类模板将为我们展开另一幅图景——与类型无关的数据结构。例如以上的节点类可以统一用类模板表示,如例2.1所示。

例2.1

template<typename T=int>

struct list_node

{

T value;

list_node*next;

};

与函数模板相同,类模板也是以关键字template开头,后接模板参数以及类模板的声明或者实现。模板参数在整个类实现中都可见,也就是可以在类实现中利用模板参数定义成员变量类型、成员函数参数类型及返回值类型等。在本例中,模板参数T被用于定义成员变量value的类型,即节点所要保存的数据类型。而另一个成员变量next是一个指向list_node的指针,但list_node只是一个模板名,并不代表实际的类,这似乎有点儿说不通。在此,C++有一个特别的规定:在一个类模板内出现的自身模板名,等价于该模板被调用时所生成的实例。此处的list_node等价于list_node<T>,所以next指针是一个指向同类的指针。

调用类模板生成实例的方法与调用函数模板相同,采用类模板名后接尖括号内的模板实参列表的形式。例如如下代码。

list_node<int>inode;//保存整数值的节点。等价于int_node

list_node<float>fnode;//保存字符值的节点。等价于float_node

在介绍函数模板时曾经提过,新的C++11标准开始支持函数模板参数默认值,而在前一版标准C++98中就已经允许为类模板参数赋默认值了,其用法如例2.1中的模板参数列表中一样,即在模板参数名之后用“=”后接默认值。

如果类模板参数有默认值,则在实参列表中可将其省略。如果所有参数都有默认值,则模板实参列表可以省略,但尖括号不能省,如以下代码所示:

list_node<>inode;//保存整数值的节点。利用模板参数默认值

与函数模板不同的是,类模板没有模板参数实参推导机制(使用类时不需要参数,自然也就无法根据参数推导出模板参数了)。所以,对于没有默认值的模板参数,只能为其一一赋值,而可以省略的仅限于列表最后有默认值的若干相邻参数。2.2 实践——栈类模板

例2.1中只是定义一个复合数据结构,并没有涉及成员函数等。而类模板与普通类一样,可以有各种成员函数、继承、运算符重载等。为了使例子更加丰满一些,我们来设计一个虽然简陋但功能完整的栈类模板。2.2.1 栈类模板实例

首先,要有一个栈类来封装数据入栈/出栈的操作,并且可以报告栈是否为空。为使其可用于多种数据类型,该栈类显然也应该写成一个类模板。这里将该栈类模板命名为my_stack。

其次,例2.1的栈节点类模板list_node应当只为栈类模板所用。为简化代码,可以将list_node所有成员完全私有,但声明my_stack为友元类,使其可以访问list_node的私有成员。my_stack负责组织由list_node组成的单向链表。这样一个栈类模板代码如例2.2所示。

例2.2

//-----------------------------

//文件名:stack.hpp

#include<stdexcept>

template<typename T>class my_stack;//前置栈类模板声明

template<typename T>

class list_node

{

T value;

list_node*next;

//私有构造函数,只能由其友类构造

list_node(T const&v, list_node*n):

value(v), next(n){}

//友类必须是类模板my_stack的实例

friend class my_stack<T>;

};

template<typename T=int>

class my_stack

{

typedef list_node<T>node_type;

node_type*head;

//my_stack不可复制构造,也不可赋值

my_stack operator=(my_stack const&){}

my_stack(my_stack const&s){}

public:

//构造与析构

my_stack():head(0){}

~my_stack(){while (!empty()) pop();}

//在类模板内实现的成员函数模板

bool empty() const{return head==0;}

T const&top() const throw (std::runtime_error){

if (empty())

throw std::runtime_error("stack is empty.");

return head->value;

}

void push(T const&v){head=new node_type(v, head);}

//成员函数声明,将在类模板外实现

void pop();

};

//在类模板外实现的成员函数模板

template<typename T>

void my_stack<T>::pop()

{

if (head){

node_type*tmp=head;

head=head->next;

delete tmp;

}

}

例2.2中首先是类模板my_stack的前置声明。有此前置声明,编译器已知my_stack为类模板且需要一个模板参数,则认可之后遇到的my_stack<T>为合法声明,否则将报错。

类模板list_node内容不变,只是所有成员都是私有,并且声明my_stack<T>为其友类,以便my_stack直接访问其成员。接下来是一个略微复杂的my_stack类模板。在类模板内首先用typedef为list_node<T>赋一个别名node_type。这样在随后用到list_node模板时就无须一一给出实参。在需要用到大量模板实例类型时,这样做可以使得代码看起来更简洁。在以后的例子中还会看到,typedef在模板编程中的作用远不止于使代码整洁这么简单。

my_stack中只有一个成员变量,即由node_type组成的单向链表的表头指针。鉴于该指针是独占指针,即其所指内容不可与其他指针共享,为防止对栈进行复制构造或者赋值操作时破坏其独占权,最简单的处理方式是将复制构造函数及赋值操作符声明为私有,以禁止用户调用。

随后即为my_stack类模板的公开构造、析构函数和若干成员函数。函数本身并无新意,重点在于模板中成员函数的写法。既然my_stack是一个类模板,其成员函数也天然是模板,并且继承了类模板的参数。在C++中,类成员函数的实现代码既可以直接写在类实现体内,也可以单独写在类实现体外。对于第一种方式,类模板的成员函数与普通类的成员函数的写法并无多少差异,如例2.2中my_stack的成员函数top()。而对于第二种方式,由于类模板成员函数本身也是模板,单独实现时也需要写成模板。已知普通类成员函数在外部实现时,代码如下:

return_value class_name::func_name(param_list){/*函数实现体*/}

对于类模板成员函数来说,class_name本身是一个模板名,则需要写成如下所示:

template<T>

return_value class_name<T>::func_name(param_list){/*函数实现*/}

例2.2中my_stack的成员函数pop()即是如此。2.2.2 栈类模板衍生子类模板实例

类模板不仅可以用来生成类实例,还可以作为其他类或者类模板的基类。以上述栈类模板作为基类,衍生一个可以记录栈内所存数据总数的子类count_stack。同样,count_stack也是一个模板,可用于保存多种数据。

实现方法也很简单,在count_stack中增加一个整数型成员变量以记录栈内数据数,并重载push()和pop()函数以便适时更新该变量。这样一个子类模板代码如例2.3所示。

例2.3

#include"stack.hpp"

template<typename T=int>

class count_stack:public my_stack<T>

{

typedef my_stack<T>base_type;//非常有用的typedef

unsigned size;

public:

count_stack():base_type(), size(0){}

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载