C++ Templates中文版

978-7-115-31281-5
作者: 【美】David Vandevoorde 【德】Nicolai M. Josuttis
译者: 陈伟柱
编辑: 傅道坤
分类: C++

图书目录:

详情

本书是C++模板编程的完全指南,旨在通过基本概念、常用技巧和应用实例3方面的有用资料,为读者打下C++模板知识的坚实基础。全书共22章,以教程的风格介绍了模板的基本概念,阐述了模板的语言细节,介绍了C++模板所支持的基本设计技术,深入探讨了各种使用模板的普通应用程序。附录A和B分别为一处定义原则和重载解析的相关资料。本书适合C++模板技术的初学者阅读,也可供有一定编程经验的C++程序员参考。

图书摘要

版权信息

书名:C++ Templates中文版

ISBN:978-7-115-31281-5

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

• 著    [美]David Vandevoorde [德]Nicolai M. Josuttis

  译    陈伟柱

  责任编辑 傅道坤

• 人民邮电出版社出版发行  北京市丰台区成寿寺路11号

  邮编 100164  电子邮件 315@ptpress.com.cn

  网址 http://www.ptpress.com.cn

• 读者服务热线:(010)81055410

  反盗版热线:(010)81055315

David Vandevoorde, Nicolai M. Josuttis: C++ Templates: The Complete Guide

Copyright © 2003 Pearson Education, Inc.

ISBN: 0201734842

All rights reserved. No part of this publication may be reproduced, stored in a retrieval system, or transmitted in any form or by any means, electronic, mechanical, photocopying, recording, or otherwise without the prior consent of Addison Wesley.

版权所有。未经出版者书面许可,对本书任何部分不得以任何方式或任何手段复制和传播。

本书中文简字体版由人民邮电出版社经Pearson Education, Inc.授权出版。版权所有,侵权必究。

本书封面贴有Pearson Education(培生教育出版集团)激光防伪标签。无标签者不得销售。


本书是C++模板编程的完全指南,旨在通过基本概念、常用技巧和应用实例三方面的有用资料,为读者打下C++模板知识的坚实基础。

全书共22章。第1章全面介绍了本书的内容结构和相关情况。第1部分(第2~7章)以教程的风格介绍了模板的基本概念,第2部分(第8~13章)阐述了模板的语言细节,第3部分(第14~18章)介绍了C++模板所支持的基本设计技术,第4部分(第19~22章)深入探讨了各种使用模板的普通应用程序。附录A和附录B分别为一处定义原则和重载解析的相关资料。

本书适合C++模板技术的初学者阅读,也可供有一定编程经验的C++程序员参考。


C++真可谓是包罗万象、博大精深。每个在C++中沉迷多年的爱好者都难免有这样的感慨:使用C++多年过后,我们往往只能算是一个熟练的使用者,却从来不敢给自己冠上“精通C++”的头衔。难道“精通C++”永远都是不惭的大言?然而,在学习、使用和研究C++的过程中,我们总是期望能够向“精通”不断迈进,并领悟C++语言的精髓。我想,要做到这一点起码要注意三个方面:一要把握语言发展的脉搏;二要多应用标准技术;三要洞悉标准技术背后的实现细节。做到这些往往能够事半功倍。

近年来,C++的新发展主要是在GP(泛型程序设计)方面大放异彩:标准库、boost库、容器、迭代子、仿函数等都是围绕着GP不断呈现出来的,它们代表了现今C++程序设计的特性。而在这种种技术的背后,隐含着一种根深蒂固的共性:模板技术,处处都是模板代码。我们可以说:泛型程序设计本身就是基于模板的程序设计。也正是模板的这种编译期机制,进一步地展现了GP的优越,体现C++高效率的特点,更有助于GP达到与OO并驾齐驱的地位。

使用了多年标准库等技术之后,每个人都曾经编写过许许多多模板代码,但在每天的重复劳动之余,很多人却未能真正洞悉隐藏在模板背后的实现细节。诸如特化、局部特化、实例化、重载解析等编译器实现机理,相信真正了解的人并不多。这使得我们始终未能真正摆脱我们所使用的特性的束缚,也就无法实现更加符合具体应用的技术与特性。在这种情况下,用起这些特性来总会觉得心里不踏实。这未免是程序员的一种悲哀。

从前面列出的3个方面来看,本书都能够解决读者的疑惑。本书前半部分内容为读者释疑解惑,后半部分内容则更加贴近开发者,使所探讨的技术真正发挥其效能。因而,也总能带给人豁然开朗的感觉,并使你深深体会到作者选材的独到之处。关于本书内容的全面介绍,请参考第1章,我在此就不再赘述了。

C++编程的书籍,现如今已是琳琅满目、硕果累累。但是对于C++和模板这个至关重要的领域,即使在未来很长一段时间里,本书也必定有着不可替代的地位,这一点从亚马逊的5星级公评和一直位于前列的销售排名可见一斑。

对于本书的翻译,我力求做到语言平实无华,期望能以流畅的语句带给读者一个轻松的阅读过程。在近一年的翻译过程中,我一次又一次地拖延了出版社的计划,正是为了真正尽到一个译者的职责,对技术和文字把好关。但“丑媳妇总要见公婆”,这本书也终究还是要和读者见面,所以我的修润也只能告一段落。在阅读的过程中,如果你有中肯的批评意见,我一定虚心地接受。我也希望能够就此书的内容与读者有更多的交流。

首先,我要感谢人民邮电出版社的编辑。对我每次提交的电子稿件,他们都仔细研读,并与我细细讨论书中的每个细节。与他们合作是一次令人愉快的经历。

我要感谢我的恩师北京理工大学的陈英老师,感谢陈老师的宽容与培养。袁卫东是本书前半部分的第1位读者,他花了很多时间,为我指出了许多不足之处。王曦(虫虫)是第16章的初稿译者,他的译文准确生动,给我带来很多宝贵的启发。另外,孟岩和熊节两位好友对C++有着多年的学习经历和丰富的知识背景,他们在我不断学习探索的过程中,给予我极大的帮助。

再一次,我要感谢在深圳的许多好友的支持。最后,感谢我的亲人和我的女友;在我工作的时候,每次都是你们在我身边;在我收获的时候,我最先想到的人总是你。

陈伟柱

2003年12月


在C++中,模板(Template)这个概念已经存在十几年了,1990年出版的Annotated C++ Reference Manual(即“ARM”,见[EllisStroustrupARM])就已经介绍了模板的一些内容。实际上,在这之前的许多专业文档也已经对模板进行了一些描述。然而,即使过了十几年之后,对于模板这一吸引人的、复杂的、强有力的C++特性,仍然没有一本著作能够集中阐述它的基础概念和高级技术。我们觉得有必要阐述这些令人费解的地方,于是就决定编写这本关于模板的书籍(这些说法或许会显得有点不够谦虚)。

然而,我们两人有着不同的背景;对于这项任务,也有着不同的目的。David是一个很有经验的编译器实现者,同时也是C++标准委员会核心语言工作组的成员。他的目的在于详细而且准确地描述模板的功能(和问题)。Nico是一个“普通”的(应用程序)程序员,同时也是C++标准委员会程序库工作小组的成员。他的目的在于让读者理解他所使用的各种模板技术和使用过程中的收获。另外,我们期望可以与你(读者)和整个(C++)社团共享这些知识,让我们都避免那些对模板的误解、疑惑和忧虑。

于是,你在书中既会看到带有例子的概念性介绍,也会看到模板具体行为的详细描述。我们将从模板的基本概念开始介绍,逐步过渡到“模板程序设计的艺术”,其中你将会发现(或者再次发现)诸如静态多态、policy类、metaprogramming、表达式模板等技术。另外,基于标准库中几乎到处都涉及到模板,在此你还可以加深对标准库的理解。

在本书的编写过程中,我们学会了很多知识,也获得了不少的乐趣。我们希望你在阅读的过程中也能有这样的感受,享受这本书和这份乐趣!


这本书引用了许多别人的思想、概念、解决方案和例子,在此我们感谢在过去几年里所有帮助和支持我们的个人和公司。

首先,我们感谢所有的审阅者和对我们的早期草稿提意见的人,这本书的质量很大程度上要归功于他们(她们)的付出。本书的审阅者有:Kyle Blaney、Thomas Gschwind、Dennis Mancl、Patrick Mc Killen和Jan Christiaan Van Winkel;特别要感谢Dietmar Kühl,他细致入微地审阅和校对了整本书,他的反馈大大提高了这本书的质量。

我们还要感谢所有帮助我们在不同的编译器平台测试书中例子程序的个人和公司。特别要感谢Edison Design Group公司,他们为我们提供了一个很优秀的编译器,还给予了我们大力的支持;这对本书的编写和C++的标准化过程都带来了很大的帮助。另外,我们还要感谢免费的GNU和egcs编译器的开发者(Jason Merrill是特别要感谢的人)和Microsoft,他们为我们提供了一份评估版本的Visual C++(Jonathan Caves、Herb Sutter、Jason Shirk是我们在那边的朋友)。

总的说来,现存的许多“C++ Wisdom”得益于在线C++团体。其中的大多数内容都来自新闻组comp.lang.c++.moderated和comp.std.c++;因此我们要特别感谢这些新闻组活跃的管理者,正是他们的努力才使所讨论的内容更加有用,更具建设性。我们还要感谢那些多年来不遗余力地为我们解释并和我们分享他们的想法的人。

Addison-Wesley出版公司的团队做了一份很出色的工作。我们特别要感谢Debbie Lafferty(我们的编辑),感谢他那温和的督促、良好的建议、不倦的工作和对这本书的支持。另外还要感谢Tyrrell Albaugh、Bunny Ames、Melanie Buck、Jacquelyn Doucette、Chanda Leary-Coutu、Catherine Ohala和Marty Rabinowitz。我们还要衷心感谢Marina Lang,正是他首先在Addison-Wesley内部提出这个选题的。最后,Susan Winer的早期编辑工作,也大大有助于我们后面的工作。

我首先要感谢我的家人:Ulli、Lucas、Anica和Frederic,感谢他们对我和这本书的耐心、关怀和鼓励。

另外,我还要感谢David,他是一个非常优秀的专家,而且他的耐心显得格外难得(有时,我甚至会问一些比较幼稚的问题)。和他一起工作让我感到极大的乐趣。

首先要感谢我的妻子Karina,这本书能够完成要归功于她的帮助和在生活中给我带来的一切。当写书时间和其他活动安排发生冲突的时候,“利用空闲时间”写书显然是不现实的。Karina帮我安排了整个时间计划,为了挤出更多的时间来写作,她教我如何对一些活动说“不”;所有的这一切都对这本书的完成给予了极大的帮助。

能够和Nico一起工作让我感到非常荣幸。除了承担部分书稿的写作,另外,正是由于Nico的经验和专业精神,才令这本原先显得凌乱的草稿变成一本结构合理的书籍。

John “Mr. Template” Spicer和Steve “Mr. Overload” Adamczyk 是我很好的朋友和同事;在我看来,他们都是核心C++语言的权威。他们澄清了书中一些令人疑惑的问题;如果你在书中找到关于C++语言的特性(element)的一些错误,那么是我的疏忽,怪我没有向他们请教。

最后,我要对下面这些支持我这份工作的许多人表达我的谢意;尽管他们的支持是间接的,但他们给我带来的一切同样是不可低估的。首先,我要感谢我的父母,正是他们的关爱和鼓励,才使我的一切发生了改变。下面是一些给我关怀(譬如询问“书进行得怎么样了?”)的朋友,他们的鼓励同样给予了我很大的动力:Michael Beckmann、Brett and Julie Beene、Jarran Carr、Simon Chang、Ho and Sarah Cho、Christophe De Dinechin、Ewa Deelman、Neil Eberle、Sassan Hazeghi、Vikram Kumar、Jim和Lindsay Long、R.J. Morgan、Mike Puritano、Ragu Raghavendra、Jim和Phuong Sharp、Gregg Vaughn和John Wiegley。


模板,作为C++中的一部分已经有了十几年之久(而且也以各种形式存在),但我们仍然会对它误解、误用甚至产生争论。同时,我们又发现模板可以作为一个工具,用来开发更加干净、更具效率、更加智能的软件。事实上,模板已经成为许多新的C++程序设计范例(paradigm)的基石。

然而,我们发现大部分关于C++模板的书籍和论文对模板理论和应用的介绍都是很肤浅的。即使是少数几本讨论各种模板设计技术的书籍,也未能准确地描述C++语言是如何支持这些模板技术的。于是,无论是C++的初级程序员还是高级程序员,都会发现模板总是令他们感到困惑,他们也期望能知道(涉及到模板的)代码为什么总是出乎意料地出错。

这种现象是我们编写这本书籍的主要原因。然而,即使同样是针对模板的话题,我们两人选择的落脚点又有所不同,写书的方式也带有差异:

就某种意义而言,你会发现我们是一对科学家—工程师组合:虽然面对的是同一个话题,但我们的着重点却有所不同(当然,肯定会有一些重叠)。

Addison-Wesley让我们两个人走到了一起,才有了这本(我们认为)带有详细参考的C++模板教程。该教程不仅介绍了模板的语言特性,更注重于阐述一些与实际应用相关的设计方法。也就是说,该书不仅是一本关于C++模板的语法和语义的详细参考,也是一份介绍广为人知(和少为人知)的模板用法和技术的概要。

为了能够理解本书中的大部分知识,你应该熟悉C++:我们描述的是该语言的一个特性(即模板),而不是语言本身的基础知识。你应该熟悉类和继承的概念,并且能够使用诸如IOstream和容器等C++标准库组件来编写程序。另外,在有必要的时候我们还会谈到语言的一些复杂话题,即使这些话题和模板并没有直接的关联。所有的这些说明了这本书主要适合于C++的专家和中级程序员。

我们所采用的语言是1998年标准化的C++语言(见[Standard98]),以及C++标准委员会在它的首份技术勘误表(见[Standard02])中所提供的澄清说明。如果你觉得你所理解的C++基本概念已经有些过时,那么我们建议你阅读[SroustrupC++PL]、[JosuttisOOP]和[JosuttisStdLib]来更新你的知识;这些书对现今的C++语言及其标准库都有很好的介绍,另外的书籍可以参考附录B.3.5。

我们的目的有两方面:一方面是为了给那些刚刚开始使用模板的程序员提供必要的信息,让他们可以从使用模板中受益;另一方面是为那些经验丰富的程序员介绍一些深入的知识,使他们可以走在模板应用的前列。为了实现这个目的,我们将整本书组织如下:

每个部分都由几个章节组成。另外,我们还提供了一些附录,它们涉及的范围并不局限于模板(例如,对C++重载解析的概述)。

对第1部分的每一章,你最好是按顺序阅读。例如,第3章就是建立在第2章(的内容)的基础之上的。然而,在其他的部分,章与章之间的关联是比较松散的。你可以随意安排阅读顺序,譬如先阅读关于仿函数的第22章,接下来才阅读关于智能指针的第20章。

如果你是一个希望学习或者温习模板概念的C++程序员,那么你应该仔细阅读第1部分——基础。即使你已经对模板非常熟悉,我们还是建议你大概浏览一下第1部分,这样有助于你了解我们所使用的风格和术语。这一部分还介绍了:当遇到包含模板的源代码时,你应该如何(逻辑地)组织这些源代码。

根据自己的学习方法,你既可以深入理解第2部分的许多细节,也可以直接阅读第3部分所介绍的实用编码技术(然后才回到第2部分阅读一些复杂的语言话题)。如果你每天面对使用模板的压力,那么后一种阅读方法通常是相当有用的。第4部分和第3部分有些类似,但它主要注重于如何把模板技术应用到具体的应用程序中,而不仅仅在于设计技术。因此,在阅读第4部分之前,最好熟悉第3部分所介绍的一些话题。

附录部分包含了很多内容,在本书的正文中我们也经常引用这些内容。我们也根据它们(指附录的内容)的特点,尽量把它们写得浅显易懂。

就我们自己的经验而言,学习一种新事物最好的方法就是研习描述该事物的例子。因此,在整本书中你到处都会看到许多代码例子。某些例子只是用于阐明某个抽象概念的短短几行代码,其他的则是完整的程序,为你提供了一份原汁原味的应用程序。后一种例子通过C++注释来引入,注释中描述了包含程序代码的文件,你可以在本书的网站http://www.josuttis.com/tmplbook/找到所有的这些文件。

C++程序员的编程风格通常是互不相同的,我们也不例外:风格所涉及的问题包括在哪里加入分隔符、界定符(花括号、圆括号)等。我们会尽量保持一致的风格,但就当前的一些(特殊的)话题,偶尔也会有所例外。例如,在教程这一部分(第1部分),我们使用了大量的空格和具体的名称,是为了令代码更加形象;而在高级话题讨论部分(第4部分),我们就使用了比较紧凑的风格,这样显得更加适合话题的讨论。

关于“类型、参数、变量”的声明,我们希望你能注意一些稍微特殊的用法。显然,下面几种风格都是可能的:

void foo (const int &x);
void foo (const int& x);
void foo (int const &x);
void foo (int const& x);

对“常整数”而言,上面的几种用法虽然差别不大,但我们趋向于使用int const,而不使用const int。作出这个选择,主要有两个原因:首先,针对问题“什么是恒定不变的”,int const提供了很容易理解的答案。实际上,“恒定不变部分”指的是const限定符前面的部分。例如,尽管

const int N = 100;

等价于:

int const N = 100;

但是对于:

int* const book mark;  //指针不能改变,但指针指向的值可以改变

却没有相应的等价形式(就是说如果把const限定符放在运算符*的前面,与前者并不等价)。在这个例子中,我们只是说明了指针本身是个常量,而并没有说明这个int值(即指针指向的值)是个常量。

第2个原因涉及到使用模板时一个很常用的语法替换原则。考虑下面的两个类型定义[1]

typedef char* CHARS;
typedef CHARS const CPTR; //指向char类型的常量指针

当我们用CHARS所代表的含义对它进行替换之后,第2个声明的含义是不变的:

typedef char* const CPTR;  //仍然是指向char类型的常量指针

然而,如果我们把const放在它所限定的类型的前面,那么这个原则就不再适用了。针对我们前面给出的两个类型声明,考虑下面的替换代码:

typedef char* CHARS;
typedef const CHARS CPTR; //指向char类型的常量指针

但如果我们替换掉CHARS之后,第2个声明却会导致不同的含义:

typedef const char* CPTR;  //指向常量char类型的指针

当然,同样的现象(规则)也适用于volatile限定符。

对于间隔符,我们决定在 & 符号和参数名称之间留出一个空格:

void foo (int const& x);

借助这种方法,我们同时也强调了:参数类型和参数名称是分离的。

显然,诸如下面的声明更容易令人混淆:

char* a, b;

上面代码中,如果根据C语言的规则,a是一个指针,而b是一个char类型的普通变量。为了避免产生这种混淆,我们尽量不在同一个语句中声明多个实体。

虽然这并不是一本介绍C++标准库的书,但我们在很多例子中都会用到标准库。在很多情况下,我们都使用了C++的特定头文件(例如我们使用< iostream >,而不是< stdio.h >)。唯一的例外情况是< stddef.h >,我们趋向于使用这个头文件,而不使用< cstddef >,从而也就不用给size_t和ptrdiff_t添加std:: 名字空间限定;另外,< stddef.h >具有更好的可移植性;而且,使用std::size_t替换size_t并不能得到任何好处。

C++标准自从1998年下半年以后就已经存在了。然而,直到2002年,才有了第一个完全符合标准的C++编译器。也就是说,大多数编译器对语言的支持仍然有所差异。有几个编译器可以编译本书的大部分代码,但一些(常用的)编译器并不能编译本书的很多代码。于是,针对这些编译器的(子标准)实现,我们经常提供了一些代替的技术,以获得一份完整(或者局部)的解决方案,但某些代替技术仍然不能为这些编译器所支持。总之,我们期望通过全世界的程序员要求编译器开发商支持标准,从很大程度上解决这个问题。

即使处于这样的现状,但随着时间的推移,C++程序设计语言仍然会不断地发展。C++社团的专家们(也包括非C++标准委员会成员的专家)正在讨论改善语言的各种方法,其中有几种候选方法就是与模板相关的,我们在第13章讨论这些发展趋势。

通过本书的网站,你可以获得本书的所有例子程序和相关信息,网站的地址是:http://www.josuttis.com/tmplbook

另外,在David Vandevoorde的网站http://www.vandevoorde.com/templates和一些别的网站也可以找到该书的一些信息。在本书后面的参考书目中我们给出了另外的一些可供查询的信息。

我们欢迎你向我们反馈书中的任何信息——包括正面的和负面的反馈。我们付出了很多的努力,希望可以给你带来一本很优秀的书。另外,我们的写作、审阅和修润必须告一段落,只有这样才能保证这本书出版。因此,如果你发现一些错误、不一致的地方、能够改进的陈述方式或者遗漏了某些话题,请给我们提供反馈信息,让我们能够通过网站告诉所有的读者,也能在接下来的重印中进行改正。

你可以通过E-mail联系我们:tmplbook@josuttis.com

但是在给我们写信之前,请确定你已经查看了网站上关于本书的勘误表。

衷心感谢!

[1]   在C++中,类型定义只是定义了一个“类型别名”,而不是一个新的类型。例如:

typedef int Length;        //定义Length 为int的别名
int i = 42;
Length l = 88;
i = l;//正确
l = i; //正确


这一部分介绍C++模板的一些常用概念和语言特性。通过展示函数模板和类模板的例子,我们先讨论普遍的目的和常用的概念。接下来介绍诸如非类型模板参数、关键字typename、成员模板等基本的模板技术。最后为接下来要介绍的模板的实际应用提供一些线索。

这些(关于模板的)介绍的部分内容来自于Nicolai M. Josuttis的书籍Object-Oriented Programming in C++,这本书由John Wiley公司出版,书号为ISBN 0-470-84399-3,它是一本循序渐进为你讲解C++语言所有特性和C++标准库的教程。

在声明变量、函数和大多数其他类型的实体的时候,C++要求我们使用指定的类型。然而,对于许多代码,除了类型不同之外,其余的代码看起来都是相同的。特别是当你实现诸如quicksort的算法,或者为不同的类型实现诸如链接表或者二叉树数据结构的行为时,这些代码除了类型有区别之外,其余的都是相同的。

假想程序设计语言并不支持这个语言特性(即模板),为了实现相同的功能,你只能使用下面这些糟糕的替代方法。

1.针对每个所需相同行为的不同类型,你可以一次又一次地实现它。

2.你可以把通用代码放在一个诸如Object或者void*的公共基础类(common base class)里面。

3.你可以使用特殊的预处理程序。

如果原来所使用的语言是C、Java或者类似的语言,那么你可能就不得不选择上面的一或多种替代方法。然而,每一种替代方法都有自身的缺点。

1.如果你一次又一次地实现同一个行为,那么你就做了许多重复的工作。你会犯同一个错误;你还会舍弃复杂但更好用的算法;因为复杂算法通常都趋向于引入更多的错误。[1]

2.如果你借助公共基类来编写通用代码,那么你将失去类型检查这个优点。另外,对于以后实现的许多类,都必须继承自某个特定的基类,这会令代码的维护更加困难。

3.如果你使用了一个诸如C或C++预处理器的预处理程序,那么你将会失去“源代码具有很好的格式”这个优点。你必须使用一些“愚蠢的文本替换机制”来替换源代码,而这将不会考虑作用域和类型。

然而,应用模板的解决方案却没有这些缺点。模板是一些为多种类型而编写的函数和类,而且这些类型都没有指定。当使用模板的时候,你只需要把所希望的类型作为一个(显式或者隐式的)实参传递给模板。另外,由于模板是语言本身所具有的特性,所以它完全支持类型检查和作用域。

在现今的程序中,模板的应用非常广泛。例如,在C++标准库中,几乎所有的代码都是模板代码。程序库提供了多种模板:可以对指定类型的对象和值排序的排序算法;用于管理指定类型元素的数据结构(也称为容器类);可以对字符进行参数化的字符串,等等。然而,这仅仅是简单的模板应用,模板还允许我们对行为进行参数化、优化代码,甚至对一些内容进行参数化,等等。这些我们将在后续章节讨论,现在让我们从最简单的模板开始。

[1] 译注:作者这里的含义是,如果有很多重复代码,那么任何算法替换都会引入很多错误。


这一章介绍函数模板。函数模板是那些被参数化的函数,它们代表的是一个函数家族。

函数模板提供了一种函数行为,该函数行为可以用多种不同的类型进行调用;也就是说,函数模板代表一个函数家族。它的表示(即外形)看起来和普通的函数很相似,唯一的区别就是有些函数元素是未确定的:这些元素将在使用时被参数化。为了阐明这些概念,让我们先来看一个简单的例子。

下面就是一个返回两个值中最大者的函数模板:

//basics/max.hpp
template <typename T>
inline T const& max (T const& a, T const& b)
{
  // 如果a < b,那么返回b,否则返回a
  return a < b ? b : a;
}

这个模板定义指定了一个“返回两个值中最大者”的函数家族,这两个值是通过函数参数a和b传递给该函数模板的;而参数的类型还没确定,用模板参数T来代替。如例子中所示,模板参数必须用如下形式的语法来声明:

template < comma-separated-list-of-parameters>
//template < 用逗号隔开的参数列表 >

在我们这个例子里,参数列表是typename T。可以看到:我们用小于号和大于号来组成参数列表外部的一对括号,并把它们称作尖括号。关键字typename引入了所谓的类型参数T,到目前为止它是C++程序使用最广泛的模板参数;也可以用其他的一些模板参数,我们将在后面介绍(见第4章)。

在上面程序中,类型参数是T。你可以使用任何标识符作为类型参数的名称,但使用T已经成为了一种惯例。事实上,类型参数T表示的是,调用者调用这个函数时所指定的任意类型。你可以使用任何类型(基本类型、类等)来实例化该类型参数,只要所用类型提供模板使用的操作就可以。例如,在这里的例子中,类型T需要支持operator< ,因为a和b就是使用这个运算符来比较大小的。

鉴于历史的原因,你可能还会使用class取代typename,来定义类型参数。在C++语言的演化过程中,关键字typename的出现相对较晚一些;在它之前,关键字class是引入类型参数的唯一方式,并一直作为有效方式保留下来。因此,模板max()还可以有如下的等价定义:

template <class T>
inline T const& max (T const& a, T const& b)
{
  // 如果 a < b ,那么返回 b ,否则返回 a
  return a < b ? b : a;
}

从语义上讲,这里的class和typename是等价的。因此,即使在这里使用了class,你也可以用任何类型(前提是该类型提供模板使用的操作)来实例化模板参数。然而,class的这种用法往往会给人误导(这里的class并不意味着只有类才能被用来替代T,事实上基本类型也可以);因此对于引入类型参数的这种用法,你应该尽量使用typename。另外还应该注意,这种用法和类的类型声明不同,也就是说,在声明(引入)类型参数的时候,不能用关键字struct代替typename。

下面的程序展示了如何使用max()函数模板:

//basics/max.cpp
#include <iostream>
#include <string>
#include ”max.hpp”

    int main()
    {
    int i = 42;
    std::cout << “max(7,i) :  “ << ::max(7,i) <<std::endl;

    double f1 = 3.4;
    double f2 = -6.7;
    std::cout << “max(f1,f2):  “ << ::max(f1,f2) <<std::endl;

    std::string s1 = “mathematics”;
    std::string s2 = “math”;
    std::cout << “max(s1,s2):  “ << ::max(s1,s2) <<std::endl;
}

在上面的程序里,max()被调用了3次,调用实参每次都不相同:一次用两个int,一个用两个double,一次用两个std::string。每一次都计算出两个实参的最大值,而调用结果是产生如下的程序输出:

max(7,i):42
max(f1,f2):3.4
max(s1,s2):mathematics

可以看到:max()模板每次调用的前面都有域限定符 :: ,这是为了确认我们调用的是全局名字空间中的max()。因为标准库也有一个std::max()模板,在某些情况下也可以被使用,因此有时还会产生二义性[1]

通常而言,并不是把模板编译成一个可以处理任何类型的单一实体;而是对于实例化模板参数的每种类型,都从模板产生出一个不同的实体[2]。因此,针对3种类型中的每一种,max()都被编译了一次。例如,max()的第一次调用:

int i = 42;
… max(7,i) …

使用了以int作为模板参数T的函数模板。因此,它具有调用如下代码的语义:

inline int const& max (int const& a, int const& b)
{
  // 如果 a < b ,那么返回 b ,否则返回 a
  return a < b ? b : a;
}

这种用具体类型代替模板参数的过程叫做实例化(instantiation)。它产生了一个模板的实例。遗憾的是,在面向对象的程序设计中,实例和实例化这两个概念通常会被用于不同的场合——但都是针对一个类的具体对象。然而,由于本书叙述的是关于模板的内容,所以在未做特别指定的情况下,我们所说的实例指的是模板的实例。

可以看到:只要使用函数模板,(编译器)会自动地引发这样一个实例化过程,因此程序员并不需要额外地请求模板的实例化。

类似地,max()的其他调用也将为double和std::string实例化max模板,就像具有如下单独的声明和实现一样:

const double& max (double const&, double const&);
const std::string& max ( std::string const&, 
                std::string const&);

如果试图基于一个不支持模板内部所使用操作的类型实例化一个模板,那么将会导致一个编译期错误,例如:

std::complex<float> c1, c2; //std::complex并不支持 operator <
…
max(c1,c2);     //编译器错误

于是,我们可以得出一个结论:模板被编译了两次,分别发生在

1.实例化之前,先检查模板代码本身,查看语法是否正确;在这里会发现错误的语法,如遗漏分号等。

2.在实例化期间,检查模板代码,查看是否所有的调用都有效。在这里会发现无效的调用,如该实例化类型不支持某些函数调用等。

这给实际中的模板处理带来了一个很重要的问题:当使用函数模板,并且引发模板实例化的时候,编译器(在某时刻)需要查看模板的定义。这就不同于普通函数中编译和链接之间的区别,因为对于普通函数而言,只要有该函数的声明(即不需要定义),就可以顺利通过编译。我们将在第6章讨论这个问题的处理方法。在此,让我们只考虑最简单的例子:通过使用内联函数,只在头文件内部实现每个模板。

当我们为某些实参调用一个诸如max()的模板时,模板参数可以由我们所传递的实参来决定。如果我们传递了两个int给参数类型T const&,那么C++编译器能够得出结论:T必须是int。注意,这里不允许进行自动类型转换;每个T都必须正确地匹配。例如:

template <typename T>
inline T const& max (T const& a, T const& b);
…
max(4,7)     //OK: 两个实参的类型都是int
max(4,4.2)    //ERROR:第1个T是int,而第2个T是double

有3种方法可以用来处理上面这个错误:

1.对实参进行强制类型转换,使它们可以互相匹配:

    max ( static_cast<double>(4),4.2)    //OK

2.显式指定(或者限定)T的类型:

    max<double>(4,4.2)             //OK

3.指定两个参数可以具有不同的类型。

关于这些话题更详细的讨论,请看下一节。

函数模板有两种类型的参数。

1.模板参数:位于函数模板名称的前面,在一对尖括号内部进行声明:

     template <typename T>     //T是模板参数

2.调用参数:位于函数模板名称之后,在一对圆括号内部进行声明:

     …max (T const& a, T const& b)  //a和b都是调用参数

你可以根据需要声明任意数量的模板参数。然而,在函数模板内部(这一点和类模板有区别),不能指定缺省的模板实参[4]。例如,你可以定义一个“两个调用参数的类型可以不同的”max()模板:

template <typename T1, typename T2>
inline T1 max (T1 const& a, T2 const& b)
{
  return a < b ? b: a;
}
…
max(4,4.2)   //OK, 但第1个模板实参的类型定义了返回类型

这看起来是一种能够给max()模板传递两个不同类型调用参数的好方法,但在这个例子中,这种方法是有缺点的。主要问题是:必须声明返回类型。对于返回类型,如果你使用的是其中的一个参数类型,那么另一个参数的实参就可能要转型为返回类型,而不会在意调用者的意图。C++并没有提供一种“指定并且选择一个‘最强大类型’”的途径(然而,你可以使用一些tricky模板编程来提供这个特性,详见15.2.4小节)。于是,取决于调用实参的顺序,42和66.66的最大值可以是浮点数66.66,也可以是整数66。另一个缺点是:把第2个参数转型为返回类型的过程将会创建一个新的局部临时对象,这导致了你不能通过引用[]来返回结果。因此,在我们的例子里,返回类型必须是T1,而不能是T1 const&。

因为调用参数的类型构造自模板参数,所以模板参数和调用参数通常是相关的。我们把这个概念称为:函数模板的实参演绎。它让你可以像调用普通函数那样调用函数模板。

然而,如前所述,针对某些特定的类型,你还可以显式地实例化该模板:

template <typename T>
inline T const& max (T const& a, T const& b);
…
max<double>(4,4.2)  //用double来实例化T

当模板参数和调用参数没有发生关联,或者不能由调用参数来决定模板参数的时候,你在调用时就必须显式指定模板实参。例如,你可以引入第3个模板实参类型,来定义函数模板的返回类型:

template <typename T1, typename T2, typename RT>
inline RT max (T1 const& a, T2 const& b);

然而,模板实参演绎并不适合返回类型[6],因为RT不会出现在函数调用参数的类型里面。因此,函数调用并不能演绎出RT。于是,你必须显式地指定模板实参列表。例如:

template <typename T1, typename T2, typename RT>
inline RT max (T1 const& a, T2 const& b);
…
max<int, double, double>(4,4.2)  //OK, 但是很麻烦

到目前为止,我们只是考察了显式指定所有函数模板实参的例子,和不显式指定函数任何模板实参的例子。另一种情况是只显式指定第一个实参,而让演绎过程推导出其余的实参。通常而言,你必须指定“最后一个不能被隐式演绎的模板实参之前的”所有实参类型。因此,在上面的例子里,如果你改变模板参数的声明顺序,那么调用者就只需要指定返回类型:

template <typename RT, typename T1, typename T2>
inline RT max (T1 const& a, T2 const& b);
…
max<double>(4,4.2)  //OK: 返回类型是double

在这个例子里,调用max< double >时显式地把RT指定为double,但其他两个参数T1和T2可以根据调用实参分别演绎为int和double。

可以看出,所有这些修改后的max()版本都不能得到很大的改进。由于在单(模板)参数版本里,如果传递进来的是两个不同类型的实参,你已经可以指定参数的类型(和返回类型)。因此,尽量保持简洁并且使用单参数版本的max()就是一个不错的主意(在接下来的几节里,当讨论其他模板话题的时候,我们将使用这种方法)。

关于演绎过程的更多内容请参照第11章。

和普通函数一样,函数模板也可以被重载。就是说,相同的函数名称可以具有不同的函数定义;于是,当使用函数名称进行函数调用的时候,C++编译器必须决定究竟要调用哪个候选函数。即使在不考虑模板的情况下,做出该决定的规则也已经是相当复杂,但在这一节里,我们将讨论有关模板的重载问题。如果你对不含模板的重载的基本规则还不是很熟悉,那么请先阅读附录B,在那里我们对重载解析规则进行了很详细的叙述。

下面的简短程序叙述了如何重载一个函数模板:

//basics/max2.cpp
//求两个int值的最大值
inline int const& max (int const& a, int const& b) 
{
     return  a < b ? b : a;
}

// 求两个任意类型值中的最大者
template <typename T>
inline T const& max (T const& a, T const& b)
{
    return  a < b ? b : a;
}

// 求3个任意类型值中的最大者
template <typename T>
inline T const& max (T const& a, T const& b, T const& c)
{
    return ::max (::max(a,b), c);
}

int main()
{
::max(7, 42, 68);         // 调用具有3个参数的模板
    ::max(7.0, 42.0);        // 调用max<double> (通过实参演绎) 
    ::max('a', 'b');         // 调用max<char> (通过实参演绎) 
    ::max(7, 42);             // 调用int重载的非模板函数
    ::max<>(7, 42);          // 调用 max<int> (通过实参演绎)
    ::max<double>(7, 42); //调用max<double> (没有实参演绎)
    ::max('a', 42.7);       //调用int重载的非模板函数
}

如例子所示,一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。对于非模板函数和同名的函数模板,如果其他条件都是相同的话,那么在调用的时候,重载解析过程通常会调用非模板函数,而不会从该模板产生出一个实例。第4个调用就符合这个规则:

max(7,42)    //使用两个int值,很好地匹配非模板函数

然而,如果模板可以产生一个具有更好匹配的函数,那么将选择模板。这可以通过max()的第2次和第3次调用来说明:

max(7.0,42.0);   //调用 max<double>(通过实参演绎)
max('a', 'b');   //调用 max<char>(通过实参演绎)

还可以显式地指定一个空的模板实参列表,这个语法好像是告诉编译器:只有模板才能来匹配这个调用,而且所有的模板参数都应该根据调用实参演绎出来:

max<>(7,42)    //call max<int>(通过实参演绎)

因为模板是不允许自动类型转化的;但普通函数可以进行自动类型转换,所以最后一个调用将使用非模板函数(‘a’和42.7都被转化为int):

max('a',42.7)   //对于不同类型的参数,只允许使用非模板函数

下面这个更有用的例子将会为指针和普通的C字符串重载这个求最大值的模板:

//basics/max3.cpp
#include <iostream>
#include <cstring>
#include <string>

// 求两个任意类型值的最大者
template <typename T>
inline T const& max (T const& a, T const& b)
{
    return  a < b  ?  b : a;
}

// 求两个指针所指向值的最大者
template <typename T>
inline T* const& max (T* const& a, T* const& b)
{
    return  *a < *b  ?  b : a;
}

// 求两个C字符串的最大者
inline char const* const& max (char const* const& a,
                               char const* const& b)
{ 
    return  std::strcmp(a,b) < 0  ?  b : a;
}

int main ()
{
int a=7;
    int b=42;
    ::max(a,b);      // max() 求两个int值的最大值

    std::string s="hey";
    std::string t="you";
    ::max(s,t);     // max() 求两个std:string类型的最大值

    int* p1 = &b;
    int* p2 = &a;
    ::max(p1,p2);    // max() 求两个指针所指向值的最大者

    char const* s1 = "David";
    char const* s2 = "Nico";
    ::max(s1,s2);    // max() 求两个c字符串的最大值
}

注意,在所有重载的实现里面,我们都是通过引用来传递每个实参的。一般而言,在重载函数模板的时候,最好只是改变那些需要改变的内容;就是说,你应该把你的改变限制在下面两种情况:改变参数的数目或者显式地指定模板参数。否则就可能会出现非预期的结果。例如,对于原来使用传引用的max()模板,你用C-string类型进行重载;但对于现在(即重载版本的)基于C-strings的max()函数,你是通过传值来传递参数;那么你就不能使用3个参数的max()版本,来对3个C-string求最大值:

//basics/max3a.cpp
#include <iostream>
#include <cstring>
#include <string>

// 两个任意类型值的最大者 (通过传引用进行调用) 
template <typename T>
inline T const& max (T const& a, T const& b)
{
    return  a < b  ?  b : a;
}

// 两个C字符串的最大者 (通过传值进行调用) 
inline char const* max (char const* a, char const* b)
{ 
    return  std::strcmp(a,b) < 0  ?  b : a;
}

// 求3个任意类型值的最大者 (通过传引用进行调用) 
template <typename T>
inline T const& max (T const& a, T const& b, T const& c)
{
    return max (max(a,b), c);  //注意:如果max(a,b)使用传值调用
                                    //那么将会发生错误
}

int main ()
{
    ::max(7, 42, 68);     // OK

    const char* s1 = "frederic";
    const char* s2 = "anica";
    const char* s3 = "lucas";
    ::max(s1, s2, s3);    // 错误。

}

问题在于:如果你对3个C-strings调用max(),那么语句:

return max (max(a,b),c);

将会产生一个错误。这是因为对于C-strings而言,这里的max(a,b)产生了一个新的临时局部值,该值有可能会被外面的max函数以传引用的方式返回,而这将导致传回无效的引用。

对于复杂的重载解析规则所产生的结果,这只是具有非预期行为的代码例子中的一例而已。例如,当调用重载函数的时候,调用结果就有可能与该重载函数在此时可见与否这个事实有关,但也可能没有关系。事实上,定义一个具有3个参数的max()版本,而且直到该定义处还没有看到一个具有两个int参数的重载max()版本的声明;那么这个具有3个int实参的max()调用将会使用具有2个参数的模板,而不会使用基于int的重载版本max():

//basics/max4.cpp
// 求两个任意类型值的最大者
template <typename T>
inline T const& max (T const& a, T const& b)
{
    return  a < b ? b : a;
}

// 求3个任意类型值的最大者
template <typename T>
inline T const& max (T const& a, T const& b, T const& c)
{
    return max (max(a,b), c);      //使用了模板的版本,即使有下面声明的int                                    
                                  //版本,但该声明来得太迟了 
}
// 求两个int值的最大者
inline int const& max (int const& a, int const& b) 
{
    return  a < b ? b : a;
}

我们将在9.2节讨论这个细节;但就目前而言,你应该牢记一条首要规则:函数的所有重载版本的声明都应该位于该函数被调用的位置之前。

[1]  例如,如果在名字空间std定义了某种实参类型(如string),那么根据C++的查找规则,全局名字空间的max()模板和std max()模板都可以被找到。

[2]  “one-entity-fits-all(一个实体适应所有类型)”的方法或许是可以实现的,但实际中几乎没有被实现过。因为所有的语言规则都是以“产生出不同实体”这个概念为基础的。

[3]  译注:也有人把deduction翻译成推演、推导、推断和推算。

[4]  这个限制主要是由函数模板在历史发展过程中的一个失误造成的。事实上,对于现今的C++编译器,要实现这个特性并没有技术障碍,C++在以后可能会提供这个特性(见13.3节)。

[5]  对于那些作用域局部于函数内部的值,你就不应该通过引用来返回该值(即返回一个指向该值的引用)。因为当程序离开这个函数的作用域之后,该值将不再存在,该引用也不再有效。

[6]  可以把演绎看成是重载解析的一部分——重载解析是一个不依赖于返回类型选择的过程,唯一的例外就是转型操作符成员的返回类型。


相关图书

代码审计——C/C++实践
代码审计——C/C++实践
CMake构建实战:项目开发卷
CMake构建实战:项目开发卷
C++ Templates(第2版)中文版
C++ Templates(第2版)中文版
C/C++代码调试的艺术(第2版)
C/C++代码调试的艺术(第2版)
计算机图形学编程(使用OpenGL和C++)(第2版)
计算机图形学编程(使用OpenGL和C++)(第2版)
Qt 6 C++开发指南
Qt 6 C++开发指南

相关文章

相关课程