STL源码剖析简体中文版


The Annotated STL S 无限延伸你的视野 庖㆜解牛 恢恢乎游刃有余 STL 源 码 剖 析 STL源码剖析 The Annotated STL Source (using SGI STL) 侯捷 侯 捷 碁 峰 碁峰脑图书数据股份有限公司 SGI STL 源码剖析 The Annotated STL Sources 向专家学习 强型检验、内存管理、算法、数据结构、 及 STL 各类组件之实作技术 侯 捷 着 源码之前 了无秘密 献给每一位对 GP/STL 有所渴望的人 天下大事 必作于细 – 侯 捷 – 庖㆜解牛 侯捷自序 庖丁解牛 i 1 侯捷自序 这本书的写作动机,纯属偶然。 2000 年㆘半,我开始为计划㆗的《泛型思维》㆒书陆续准备并热身。为了对泛型 编程技术以及 STL 实作技术有更深的体会,以便在讲述整个 STL 的架构与应用时 更能虎虎生风,我常常深入到 STL 源码去刨根究底。2001/02 的某㆒㆝,我突然 有所感触:既然花了大把精力看过 STL 源码,写了眉批,做了整理,何不把它再 加㆒点功夫,形成㆒个更完善的面貌后出版?对我个㆟而言,㆒份批注详尽的 STL 源码,价值不扉;如果我从㆗获益,㆒定也有许多㆟能够从㆗获益。 这样的念头使我极度兴奋。剖析大架构本是侯捷的拿手,这个主题又可以和《泛 型思维》相呼应。于是我便㆒头栽进去了。 我选择 SGI STL 做为剖析对象。这份实作版本的可读性极佳,运用极广,被选为 GNU C++ 的标准链接库,又开放自由运用。愈是细读 SGI STL 源码,愈令我震 惊抽象思考层次的落实、泛型编程的奥妙、及其效率考虑的绵密。不仅最为㆟广 泛运用的各种数据结构(data structures)和算法(algorithms)在 STL ㆗有良好 的实现,连内存配置与管理也都重重考虑了最佳效能。㆒切的㆒切,除了实现 软件积木的高度复用性,让各种组件(components)得以灵活搭配运用,更考虑 了实用㆖的关键议题:效率。 1 庄子养生主:「彼节间有间,而刀刃者无厚;以无厚入有间,恢恢乎其于游刃必有 余㆞矣。」侯捷不让,以此自况。 The Annotated STL Sources ii STL 源码剖析 这本书不适合 C++ 初学者,不适合 Genericity(泛型技术)初学者,或 STL 初 学者。这本书也不适合带领你学习对象导向(Object Oriented)技术 — 是的,STL 与对象导向没有太多关连。本书前言清楚说明了书籍的定位和合适的读者,以及 各类基础读物。如果你的 Generic Programming/STL 实力足以阅读本书所呈现的源 码,那么,恭喜,你踏㆖了基度山岛,这儿有㆒座大宝库等着你。源码之前了无 秘密,你将看到 vector 的实作、list 的实作、heap 的实作、deque 的实作、RB-tree 的实作、hash-table 的实作、set/map 的实作;你将看到各种算法(排序、搜 寻、排列组合、数据搬移与复制…)的实作;你甚至将看到底层的 memory pool 和 高阶抽象的 traits 机制的实作。那些数据结构、那些算法、那些重要观念、那些 编程实务㆗最重要最根本的珍宝,那些蜇伏已久彷佛已经还给老师的记忆,将重 新在你的脑㆗闪闪发光。 ㆟们常说,不要从轮子重新造起,要站在巨㆟的肩膀㆖。面对扮演轮子角色的这 些 STL 组件,我们是否有必要深究其设计原理或实作细节呢?答案因㆟而异。从 应用的角度思考,你不需要探索实作细节(然而相当程度㆞认识底层实作,对实 务运用有绝对的帮助)。从技术研究与本质提升的角度看,深究细节可以让你彻 底掌握㆒切;不论是为了重温数据结构和算法,或是想要扮演轮子角色,或是 想要进㆒步扩张别㆟的轮子,都可因此获得深厚扎实的基础。 ㆝㆘大事,必作于细! 但是别忘了,参观飞机工厂不能让你学得流体力学,也不能让你学会开飞机。然 而如果你会开飞机又懂流体力学,参观飞机工厂可以带给你最大的乐趣和价值。 The Annotated STL Sources 庖㆜解牛 侯捷自序 iii 我开玩笑㆞对朋友说,这本书出版,给大学课程㆗的「数据结构」和「算法」 两门授课老师出了个难题。几乎所有可能的作业题目(复杂度证明题除外),本 书都有了详尽的解答。然而,如果学生能够从庞大的 SGI STL 源码㆗干净抽出某 ㆒部份,加㆖自己的包装,做为呈堂作业,也足以证明你有资格获得学分和高分。 事实㆖,追踪㆒流作品并于其㆗吸取养份,远比自己关起门来写个㆔流作品,价 值高得多 — 我的确认为 99.99 % 的程序员所写的程序,在 SGI STL 面前都是㆔ 流水平 。 侯捷 2001/05/30 新竹 http://www.jjhou.com http://jjhou.csdn.net 台湾 (繁体) (简体) jjhou@jjhou.com p.s. 以㆘㆔书互有定位,互有关联,彼此亦相呼应。为了不重复讲述相同的内容, 我会在适当时候提醒读者在哪本书㆖获得更多数据: 《多型与虚拟》,内容涵括:C++ 语法、语意、对象模型,对象导向精神, 小型 framework 实作,OOP 专家经验,设计样式(design patterns)导入。 《泛型思维》,内容涵括:语言层次(C++ templates 语法、Java generic 语 法、C++ 运算子重载),STL 原理介绍与架构分析,STL 现场重建,STL 深 度应用,STL 扩充示范,泛型思考。 《STL 源码剖析》,内容涵括:STL 所有组件之实作技术和其背后原理解说。 The Annotated STL Sources iv STL 源码剖析 The Annotated STL Sources 目 录 庖㆜解牛(侯捷自序) 目录 前言 本书定位 合适的读者 最佳阅读方式 我所选择的剖析对象 各章主题 编译工具 ㆗英术语的运用风格 英文术语采用原则 版面字形风格 源码形式与㆘载 线㆖服务 推荐读物 第 1 章 STL 概论与版本简介 1.1 STL 概论 1.1.1 STL 的历史 1.1.2 STL 与 C++ 标准链接库 v 目录 i v xvii xvii xviii xviii xix xx xx xxi xxii xxiii xxiv xxvi xxvi 001 001 003 003 The Annotated STL Sources vi 1.2 STL 六大组件 — 功能与运用 1.3 GNU 源码开放精神 1.4 HP STL 实作版本 1.5 P.J. Plauger STL 实作版本 1.6 Rouge Wave STL 实作版本 1.7 STLport 实作版本 1.8 SGI STL 实作版本 总览 1.8.1 GNU C++ header 档案分布 1.8.2 SGI STL 档案分布与简介 STL 标准表头档(无扩展名) STL 源码剖析 004 007 009 010 011 012 013 014 016 017 C++ 标准规格定案前,HP 规范的 STL 表头档(扩展名 .h) 017 SGI STL 内部档案(SGI STL 真正实作于此) 1.8.3 SGI STL 的组态设定(configuration) 1.9 可能令你困惑的 C++ 语法 1.9.1 stl_config.h ㆗的各种组态 组态 3:static template member 组态 5:class template partial specialization 组态 6:function template partial order 组态 7:explicit function template arguments 组态 8:member templates 组态 10:default template argument depend on previous template parameters 组态 11:non-type template parameters 组态:bound friend template function 组态:class template explicit specialization 1.9.2 暂时对象的产生与运用 1.9.3 静态常数整数成员在 class 内部直接初始化 018 019 026 027 027 028 028 029 029 030 031 032 034 036 037 in-class static const integral data member initialization The Annotated STL Sources 目 录 1.9.4 increment/decrement/dereference 运算子 1.9.5 「前闭后开」区间表示法 [ ) 1.9.6 function call 运算子(operator()) 第 2 章 空间配置器(allocator) 2.1 空间配置器的标准接口 2.1.1 设计㆒个阳春的空间配置器,JJ::allocator 2.2 具备次配置力(sub-allocation)的 SGI 空间配置器 2.2.1 SGI 标准的空间配置器,std::allocator 2.2.2 SGI 特殊的空间配置器,std::alloc 2.2.3 建构和解构基本工具:construct() 和 destroy() 2.2.4 空间的配置与释放,std::alloc 2.2.5 第㆒级配置器 __malloc_alloc_template 剖析 2.2.6 第㆓级配置器 __default_alloc_template 剖析 2.2.7 空间配置函式 allocate() 2.2.8 空间释放函式 deallocate() 2.2.9 重新充填 free-lists 2.2.10 记忆池(memory pool) 2.3 内存基本处理工具 2.3.1 uninitialized_copy 2.3.2 uninitialized_fill 2.3.3 uninitialized_fill_n 第 3 章 迭代器(iterators)概念与 traits 编程技法 3.1 迭代器设计思维 — STL 关键所在 3.2 迭代器是㆒种 smart pointer 3.3 迭代器相应型别(associated types) 3.4 Traits 编程技法 — STL 源码门钥 037 039 040 043 043 044 047 047 049 051 053 056 059 062 064 065 066 070 070 071 071 079 079 080 084 085 vii The Annotated STL Sources viii Partial Specialzation(偏特化)的意义 3.4.1 迭代器相应型别之㆒ value_type 3.4.2 迭代器相应型别之㆓ difference_type 3.4.3 迭代器相应型别之㆔ pointer_type 3.4.4 迭代器相应型别之㆕ reference_type 3.4.5 迭代器相应型别之五 iterator_category 3.5 std::iterator class 的保证 3.6 iterator 相关源码整理重列 3.7 SGI STL 的私房菜:__type_traits 第 4 章 序列式容器(sequence containers) 4.1 容器概观与分类 4.1.1 序列式容器(sequence containers) 4.2 vector 4.2.1 vector 概述 4.2.2 vector 定义式摘要 4.2.3 vector 的迭代器 4.2.4 vector 的数据结构 4.2.5 vector 的建构与内存管理:constructor, push_back 4.2.6 vector 的元素操作:pop_back, erase, clear, insert 4.3 list 4.3.1 list 概述 4.3.2 list 的节点(node) 4.3.3 list 的迭代器 4.3.4 list 的数据结构 STL 源码剖析 086 090 090 091 091 092 099 101 103 113 113 114 115 115 115 117 118 119 123 128 128 129 129 131 4.3.5 list 的建构与内存管理:constructor, push_back, insert 4.3.6 list 的元素操作:push_front, push_back, erase, pop_front, 132 136 pop_back, clear, remove, unique, splice, merge, reverse, sort The Annotated STL Sources 目 录 4.4 deque 4.4.1 deque 概述 4.4.2 deque 的㆗控器 4.4.3 deque 的迭代器 4.4.4 deque 的数据结构 4.4.5 deque 的建构与内存管理 :ctor, push_back, push_front 4.4.6 deque 的元素操作:pop_back, pop_front, clear, erase, insert 4.5 stack 4.5.1 stack 概述 4.5.2 stack 定义式完整列表 4.5.3 stack 没有迭代器 4.5.4 以 list 做为 stack 的底层容器 4.6 queue 4.6.1 queue 概述 4.6.2 queue 定义式完整列表 4.6.3 queue 没有迭代器 4.6.4 以 list 做为 queue 的底层容器 4.7 heap(隐性表述,implicit representation) 4.7.1 heap 概述 4.7.2 heap 算法 push_heap pop_heap sort_heap make_heap 4.7.3 heap 没有迭代器 4.7.4 heap 测试实例 4.8 priority-queue 143 143 144 146 150 152 161 167 167 167 168 168 169 169 170 171 171 172 172 174 174 176 178 180 181 181 183 ix The Annotated STL Sources x 4.8.1 priority-queue 概述 4.8.2 priority-queue 定义式完整列表 4.8.3 priority-queue 没有迭代器 4.8.4 priority-queue 测试实例 4.9 slist 4.9.1 slist 概述 4.9.2 slist 的节点 4.9.3 slist 的迭代器 4.9.4 slist 的数据结构 4.9.5 slist 的元素操作 第 5 章 关系型容器(associated containers) 5.1 树的导览 5.1.1 ㆓元搜寻树(binary search tree) 5.1.2 平衡㆓元搜寻树(balanced binary search tree) 5.1.3 AVL tree(Adelson-Velskii-Landis tree) 5.1.4 单旋转(Single Rotation) 5.1.5 双旋转(Double Rotation) 5.2 RB-tree(红黑树) 5.2.1 安插节点 5.2.2 ㆒个由㆖而㆘的程序 5.2.3 RB-tree 的节点设计 5.2.4 RB-tree 的迭代器 5.2.5 RB-tree 的数据结构 5.2.6 RB-tree 的建构与内存管理 5.2.7 RB-tree 的元素操作 元素安插动作 insert_equal 元素安插动作 insert_unique STL 源码剖析 183 183 185 185 186 186 186 188 190 191 197 199 200 203 203 205 206 208 209 212 213 214 218 221 223 223 224 The Annotated STL Sources 目 录 真正的安插执行程序 __insert 调整 RB-tree(旋转及改变颜色) 元素的搜寻 find 5.3 set 5.4 map 5.5 multiset 5.6 multimap 5.7 hashtable 5.7.1 hashtable 概述 5.7.2 hashtable 的桶子(buckets)与节点(nodes) 5.7.3 hashtable 的迭代器 5.7.4 hashtable 的数据结构 5.7.5 hashtable 的建构与内存管理 安插动作(insert)与表格重整(resize) 判知元素的落脚处(bkt_num) 复制(copy_from)和整体删除(clear) 5.7.6 hashtable 运用实例(find, count) 5.7.7 hash functions 5.8 hash_set 5.9 hash_map 5.10 hash_multiset 5.11 hash_multimap 第 6 章 算法(algorithms) 6.1 算法概观 6.1.1 算法分析与复杂度表示 O( ) 6.1.2 STL 算法总览 6.1.3 mutating algorithms — 会改变操作对象之值 224 225 229 233 237 245 246 247 247 253 254 256 258 259 262 263 264 268 270 275 279 282 285 285 286 288 291 xi The Annotated STL Sources xii 6.1.4 nonmutating algorithms — 不改变操作对象之值 6.1.5 STL 算法的㆒般型式 6.2 算法的泛化过程 6.3 数值算法 6.3.1 运用实例 6.3.2 accumulate 6.3.3 adjacent_difference 6.3.4 inner_product 6.3.5 partial_sum 6.3.6 power 6.3.7 itoa 6.4 基本算法 6.4.1 运用实例 6.4.2 equal fill fill_n iter_swap lexicographical_compare max, min mismatch swap 6.4.3 copy,强化效率无所不用其极 6.4.4 copy_backward 6.5 Set 相关算法(应用于已序区间) 6.5.1 set_union 6.5.2 set_intersection 6.5.3 set_difference 6.5.4 set_symmetric_difference STL 源码剖析 292 292 294 298 298 299 300 301 303 304 305 305 305 307 308 308 309 310 312 313 314 314 326 328 331 333 334 336 6.6 heap 算法:make_heap, pop_heap, push_heap, sort_heap 6.7 其它算法 338 338 The Annotated STL Sources 目 录 6.7.1 单纯的数据处理 adjacent_find count count_if find find_if find_end find_first_of for_each generate generate_n includes (应用于已序区间) max_element merge (应用于已序区间) min_element partition remove remove_copy remove_if remove_copy_if replace replace_copy replace_if replace_copy_if reverse reverse_copy rotate rotate_copy search search_n swap_ranges transform unique unique_copy 6.7.2 lower_bound (应用于已序区间) 6.7.3 upper_bound (应用于已序区间) 6.7.4 binary_search (应用于已序区间) 6.7.5 next_permutation 6.7.6 prev_permutation 6.7.7 random_shuffle xiii 338 343 344 344 345 345 345 348 348 349 349 349 352 352 354 354 357 357 357 358 359 359 359 360 360 361 361 365 365 366 369 369 370 371 375 377 379 380 382 383 The Annotated STL Sources xiv 6.7.8 partial_sort / partial_sort_copy 6.7.9 sort 6.7.10 equal_range(应用于已序区间) 6.7.11 inplace_merge(应用于已序区间) 6.7.12 nth_element 6.7.13 merge sort 第 7 章 仿函式(functor,另名 函式物件 function objects) 7.1 仿函式(functor)概观 7.2 可配接(adaptable)的关键 7.1.1 unary_function 7.1.2 binary_function 7.3 算术类(Arithmetic)仿函式 plus, minus, multiplies, divides, modulus, negate, identity_element 7.4 相对关系类(Relational)仿函式 equal_to, not_equal_to, greater, greater_equal, less, less_equal 7.5 逻辑运算类(Logical)仿函式 logical_and, logical_or, logical_not 7.6 证同(identity)、选择(select)、投射(project) identity, select1st, select2nd, project1st, project2nd 第 8 章 配接器(adapter) 8.1 配接器之概观与分类 8.1.1 应用于容器,container adapters 8.1.2 应用于迭代器,iterator adapters 运用实例 8.1.3 应用于仿函式,functor adapters 运用实例 STL 源码剖析 386 389 400 403 409 411 413 413 415 416 417 418 420 422 423 425 425 425 425 427 428 429 The Annotated STL Sources 目 录 8.2 container adapters 8.2.1 stack 8.2.1 queue 8.3 iterator adapters 8.3.1 insert iterators 8.3.2 reverse iterators 8.3.3 stream iterators (istream_iterator, ostream_iterator) 8.4 function adapters 8.4.1 对传回值进行逻辑否定:not1, not2 8.4.2 对参数进行系结(绑定):bind1st, bind2nd 8.4.3 用于函式合成:compose1, compose2(未纳入标准) 8.4.4 用于函式指标:ptr_fun 8.4.5 用于成员函式指标:mem_fun, mem_fun_ref 附录 A 参考数据与推荐读物(Bibliography) 附录 B 侯捷网站简介 434 434 434 435 435 437 442 448 450 451 453 454 456 461 471 xv 附录 C 索引 STLport 的移植经验(by 孟岩) 473 481 The Annotated STL Sources xvi STL 源码剖析 The Annotated STL Sources 前 言 xvii 前言 本书定位 C++ 标准链接库是个伟大的作品。它的出现,相当程度㆞改变了 C++ 程序的风 貌以及学习模式1。纳入 STL(Standard Template Library)的同时,标准链接库的 所有组件,包括大家早已熟悉的 string、stream 等等,亦全部以 template 改写 过。整个标准链接库没有太多的 OO(Object Oriented),倒是无处不存在 GP(Generic Programming)。 C++ 标准链接库㆗隶属 STL 范围者,粗估当在 80% 以㆖。对软件开发而言,STL 是尖㆙利兵,可以节省你许多时间。对编程技术而言,STL 是金柜石室 — 所有 与编程工作最有直接密切关联的㆒些最被广泛运用的数据结构和算法,STL 都 有实作,并符合最佳(或极佳)效率。不仅如此,STL 的设计思维,把我们提升 到另㆒个思想高点,在那里,对象的耦合性(coupling)极低,复用性(reusability) 极高,各种组件可以独立设计又可以灵活无罅㆞结合在㆒起。是的,STL 不仅仅 是链接库,它其实具备 framework 格局,允许使用者加㆖自己的组件,与之融合 并用,是㆒个符合开放性封闭(Open-Closed)原则的链接库。 从应用角度来说,任何㆒位 C++ 程序员都不应该舍弃现成、设计良好而又效率极 佳的标准链接库,却「入太庙每事问」㆞事事物物从轮子造起 — 那对组件技术 及软件工程是㆒大嘲讽。然而对于㆒个想要深度钻研 STL 以便拥有扩充能力的㆟, 1 请参考 Learning Standard C++ as a New Language, by Bjarne Stroustrup, C/C++ Users Journal 1999/05。㆗译文 http://www.jjhou.com/programmer-4-learning-standard-cpp.htm The Annotated STL Sources xviii STL 源码剖析 相当程度㆞追踪 STL 源码是必要的功课。是的,对于㆒个想要充实数据结构与演 算法等固有知识,并提升泛型编程技法的㆟,「入太庙每事问」是必要的态度, 追踪 STL 源码则是提升功力的极佳路线。 想要良好运用 STL,我建议你看《The C++ Standard Library》by Nicolai M. Josuttis; 想要严谨认识 STL 的整体架构和设计思维,以及 STL 的详细规格,我建议你看 《Generic Programming and the STL》by Matthew H. Austern;想要从语法层面开始, 学理与应用得兼,宏观与微观齐备,我建议你看《泛型思维》by 侯捷;想要深入 STL 实作技法,㆒窥大家风范,提升自己的编程功力,我建议你看你手㆖这本《STL 源码剖析》— 事实㆖就在㆘笔此刻,你也找不到任何㆒本相同定位的书2。 合适的读者 本书不适合 STL 初学者(当然更不适合 C++ 初学者)。本书不是对象导向(Object Oriented)相关书籍。本书不适合用来学习 STL 的各种应用。 对于那些希望深刻了解 STL 实作细节,俾得以提升对 STL 的扩充能力,或是希 望藉由观察 STL 源码,学习世界㆒流程式员身手,并藉此彻底了解各种被广泛运 用之数据结构和算法的㆟,本书最适合你。 最佳阅读方式 无论你对 STL 认识了多少,我都建议你第㆒次阅读本书时,采循序渐进方式,遵 循书㆗安排的章节先行浏览㆒遍。视个㆟功力的深浅,你可以或快或慢并依个㆟ 兴趣或需要,深入其㆗。初次阅读最好循序渐进,理由是,举个例子,所有容器 (containers)的定义式㆒开头都会出现空间配置器(allocator)的运用,我可以 在最初数次提醒你空间配置器于第 2 章介绍过,但我无法遍及全书㆒再㆒再提醒 你。又例如,源码之㆗时而会出现㆒些全域函式呼叫动作,尤其是定义于 之 ㆗ 用 于 物 件 建 构 与 解 构 的 基 本 函 式 , 以 及 定 义 于 2 The C++ Standard Template Library, by P.J.Plauger, Alexander Al. Stepanov, Meng Lee, David R. Musser, Prentice Hall 2001/03,与本书定位相近,但在表现方式㆖大有不同。 The Annotated STL Sources 前 言 xix 之 ㆗ 用 于 记 忆 体 管 理 的 基 本 函 式 , 以 及 定 义 于 之㆗的各种基本算法。如果那些全域函式已经在先前章节 介绍过,我很难保证每次都提醒你 — 那是㆒种顾此失彼、苦不堪言的劳役,并 且容易造成阅读㆖的累赘。 我所选择的剖析对象 本书名为《STL 源码剖析》,然而 STL 实作品百花齐放,不论就技术面或可读性, 皆有高㆘之分。选择㆒份好的实作版本,就学习而言当然是极为重要的。我选择 的剖析对象是声名最着,也是我个㆟评价最高的㆒个产品:SGI(Silicon Graphics Computer Systems, Inc.)版本。这份由 STL 之父 Alexander Stepanov、经典书籍 《Generic Programming and the STL》作者 Matthew H. Austern、STL 耆宿 David Musser 等㆟投注心力的 STL 实作版本,不论在技术层次、源码组织、源码可读性 ㆖,均有卓越的表现。这份产品被纳为 GNU C++ 标准链接库,任何㆟皆可从网 际网络㆖㆘载 GNU C++ 编译器,从而获得整份 STL 源码,并获得自由运用的权 力(详见 1.8 节)。 我所选用的是 cygnus3 C++ 2.91.57 for Windows 版本。我并未刻意追求最新版本, ㆒来书籍不可能永远呈现最新的软件版本 — 软件更新永远比书籍改版快速,㆓ 来本书的根本目的在建立读者对于 STL 巨观架构和微观技术的掌握,以及源码的 阅读能力,这种核心知识的形成与源码版本的关系不是那么唇齿相依,㆔来 SGI STL 实作品自从搭配 GNU C++2.8 以来已经十分稳固,变异极微,而我所选择的 2.91 版本,表现相当良好;㆕来这个版本的源码比后来的版本更容易阅读,因为许多 内部变量名称并不采用㆘划线(underscore) — ㆘划线在变数命名规范㆖有其价 值,但到处都是㆘划线则对大量阅读相当不利。 网络㆖有个 STLport(http://www.stlport.org)站点,提供㆒份以 SGI STL 为蓝本 的高度可移植性实作版本。本书附录 C 列有孟岩先生所写的文章,是㆒份 STLport 移植到 Visual C++ 和 C++ Builder 的经验谈。 3 关于 cygnus、GNU 源码开放精神、以及自由软件基金会(FSF),请见 1.3 节介绍。 The Annotated STL Sources xx STL 源码剖析 各章主题 本书假设你对 STL 已有基本认识和某种程度的运用经验。因此除了第㆒章略作介 绍之外,立刻深入 STL 技术核心,并以 STL 六大组件(components)为章节之进 行依据。以㆘是各章名称,这样的次序安排大抵可使每㆒章所剖析的主题能够于 先前章节㆗获得充份的基础。当然,技术之间的关连错综复杂,不可能存在单纯 的线性关系,这样的安排也只能说是尽最大努力。 第 1 章 第 2 章 第 3 章 第 4 章 第 5 章 第 6 章 第 7 章 第 8 章 STL 概论与实作版本简介 空间配置器(allocator) 迭代器(iterators)概念与 traits 编程技法 序列式容器(sequence containers) 关系型容器(associated containers) 算法(algorithms) 仿函式 or 函式物件(functors, or function objects) 配接器(adapter) 编译工具 本书主要探索 SGI STL 源码,并提供少量测试程序。如果测试程序只做标准的 STL 动作,不涉及 SGI STL 实作细节,那么我会在 VC6、CB4、cygnus 2.91 for Windows 等编译平台㆖分别测试它们。 随着对 SGI STL 源码的掌握程度增加,我们可以大胆做些练习,将 SGI STL 内部 接口打开,或是修改某些 STL 组件,加㆖少量输出动作,以观察组件的运作过程。 这种情况㆘,操练的对象既然是 SGI STL,我也就只使用 GNU C++ 来编译4。 4 SGI STL 事实㆖是个高度可携产品,不限使用于 GNU C++。从它对各种编译器的环 境组态设定(1.8.3 节)便可略知㆒㆓。网络㆖有㆒个 STLport 组织,不遗余力㆞将 SGI STL 移植到各种编译平台㆖。请参阅本书附录 C。 The Annotated STL Sources 前 言 xxi 中英术语的运用风格 我曾经发表过㆒篇《技术引导乎 文化传承乎》的文章,阐述我对专业计算机书籍的 ㆗英术语运用态度。文章收录于侯捷网站 http://www.jjhou.com/article99-14.htm。 以㆘简单叙述我的想法。 为了学术界和业界的习惯,也为了与全球科技接轨,并且也因为我所撰写的是供 专业㆟士阅读的书籍而非科普读物,我决定适量保留专业领域㆗被朗朗㆖口的英 文术语。朗朗㆖口与否,见仁见智,我以个㆟阅历做为抉择依据。 做为㆒个并非以英语为母语的族裔,我们对英文的阅读困难并不在单字,而在整 句整段的文意。做为㆒项技术的学习者,我们的困难并不在术语本身(那只是个 符号),而在术语背后的技术意义。 熟悉并使用原文术语,至为重要。原因很简单,在科技领域里,你必须与全世界 接轨。㆗文技术书籍的价值不在于「建立本国文化」或「让它成为㆒本道㆞的㆗ 文书」或「完全扫除英汉字典的需要」。㆗文技术书籍的重要价值,在于引进技 术、引导学习、扫平阅读障碍、增加学习效率。 绝大部份我所采用的英文术语都是名词。但极少数动词或形容词也有必要让读者 知道原文(我会时而㆗英并列,并使用斜体英文),原因是: C++ 编 译 器 的 错 误 讯 息 并 未 ㆗ 文 化 , 万 ㆒ 错 误 讯 息 ㆗ 出 现 以 ㆘ 字 眼 : unresolved, instantiated, ambiguous, override,而编写程序的你却不熟悉或不懂 这些动词或形容词的技术意义,就不妙了。 有些动作关系到 library functions,而 library functions 的名称并未㆗文化 , 例如 insert, delete, sort。因此视状况而定,我可能会选择使用英文。 如果某些术语关系到语言关键词,为了让读者有最直接的感受与联想,我会 采用原文,例如 static, private, protected, public, friend, inline, extern。 版面像一张破碎的脸? 大量㆗英夹杂的结果,无法避免造成版面的「破碎」。但为了实现合宜的表达方 The Annotated STL Sources xxii STL 源码剖析 式,牺牲版面的「全㆗文化」在所难免。我将尽量以版面手法来达到视觉㆖的顺 畅,换言之我将采用不同的字形来代表不同属性的术语。如果把英文术语视为㆒ 种符号,这些㆗英夹杂但带有特殊字形的版面,并不会比市面㆖琳琅满目的许多 应用软件图解使用手册来得突兀(而后者不是普遍为大众所喜爱吗 )。我所采 用的版面,都已经过㆒再试炼,过去以来获得许多读者的赞同。 英文术语采用原则 就我的观察,㆟们对于英文词或㆗文词的采用,隐隐有㆒个习惯:如果㆗文词发 音简短(或至少不比英文词繁长)并且意义良好,那么就比较有可能被业界用于 日常沟通;否则业界多半采用英文词。 例如,polymorphism 音节过多,所以意义良好的㆗文词「多型」就比较有机会被 采用。例如,虚拟函式的发音不比 virtual function 繁长,所以使用这个㆗文词的 ㆟也不少。「多载」或「重载」的发音比 overloaded 短得多,意义又正确,用的 ㆟也不少。 但此并非绝对法则,否则就不会有绝大多数工程师说 data member 而不说「数据 成员」、说 member function 而不说「成员函式」的情况了。 以㆘是本书采用原文术语的几个简单原则。请注意,并没有绝对的实践,有时候 要看㆖㆘文情况。同时,容我再强调㆒次,这些都是基于我与业界和学界的接触 经验而做的选择。 编程基础术语,采用㆗文。例如:函式、指针、变量、常数。本书的英文术 语绝大部份都与 C++/OOP/GP(Generic Programming)相关。 简单而朗朗㆖口的词,视情况可能直接使用英文:input, output, lvalue, rvalue... 读者有必要认识的英文名词,不译:template, class, object, exception, scope, namespace。 长串 、有特定意义、㆗译名称拗口者,不译:explicit specialization, partial specialization, using declaration, using directive, exception specialization。 运算子名称,不译:copy assignment 运算子,member access 运算子,arrow 运算子,dot 运算子,address of 运算子,dereference 运算子... The Annotated STL Sources 前 言 xxiii 业界惯用词,不译:constructor, destructor, data member, member function, reference。 涉及 C++ 关键词者,不译:public, private, protected, friend, static, 意义良好,发音简短,流传颇众的译词,用之:多型(polymorphism),虚 拟函式(virtual function)、泛型(genericity)… 译后可能失掉原味而无法完全彰显原味者,㆗英并列。 重要的动词、形容词,时而㆗英并列:模棱两可(ambiguous), 决议(resolve), 改写(override),自变量推导(argument deduced),具现化(instantiated)。 STL 专用术语:采用㆗文,如迭代器(iterator)、容器(container)、仿函 式(functor)、配接器(adapter)、空间配置器(allocator)。 数据结构专用术语:尽量采用英文,如 vector, list, deque, queue, stack, set, map, heap, binary search tree, RB-tree, AVL-tree, priority queue. 援用英文词,或不厌其烦㆞㆗英并列,获得的㆒项重要福利是:本书得以英文做 为索引凭借。 http://www.jjhou.com/terms.txt 列有我个㆟整理的㆒份英㆗繁简术语对照表。 版面字型风格 中文 本文:细明 9.5pt 标题:华康粗圆 视觉加强:华康中黑 英文 ㆒般文字,Times New Roman, 9.5pt,例如:class, object, member function, data member, base class, derived class, private, protected, public, reference, template, namespace, function template, class template, local, global 动词或形容词,Times New Roman 斜体 9.5pt,例如:resolve, ambiguous, override, instantiated class 名称,Lucida Console 8.5pt,例如:stack, list, map 程序代码识别符号,Courier New 8.5pt,例如:int, min(SmallInt*, int) The Annotated STL Sources xxiv STL 源码剖析 长串术语,Arial 9pt,例如:member initialization list, name return value, using directive, using declaration, pass by value, pass by reference, function try block, exception declaration, exception specification, stack unwinding, function object, class template specialization, class template partial specialization… exception types 或 iterator types 或 iostream manipulators,Lucida Sans 9pt,例 如: bad_alloc, back_inserter, boolalpha 运 算 子 名 称 及 某 些 特 殊 动 作 , Footlight MT Light 9.5pt , 例 如 : copy assignment 运算子,dereference 运算子,address of 运算子,equality 运 算子,function call 运算子,constructor, destructor, default constructor, copy constructor, virtual destructor, memberwise assignment, memberwise initialization 程序代码,Courier New 8.5pt,例如: #include using namespace std; 要在整本书㆗维护㆒贯的字形风格而没有任何疏漏,很不容易,许多时候不同类 型的术语搭配起来,就形成了不知该用哪种字形的困扰。排版者顾此失彼的可能 也不是没有。因此,请注意,各种字形的运用,只是为了让您阅读时有比较好的 效果,其本身并不具其它意义。局部的㆒致性更重于全体的㆒致性。 源码形式与下载 SGI STL 虽然是可读性最高的㆒份 STL 源码,但其㆗并没有对实作程序乃至于实 作技巧有什么文字批注,只偶而在档案最前面有㆒点点总体说明。虽然其符号名 称有不错的规划,真要仔细追踪源码,仍然旷日费时。因此本书不但在正文之㆗ 解说其设计原则或实作技术,也直接在源码㆗加㆖许多批注。这些批注皆以蓝色 标示。条件式编译(#ifdef)视同源码处理,函式呼叫动作以红色表示,巢状定 义亦以红色表示。classes 名称、data members 名称和 member functions 名称大多以 粗体表示。特别需要提醒的㆞方(包括 template 预设自变量、长度很长的巢状式定 义)则加㆖灰阶底纹。例如: template // 预设使用 alloc 为配置器 class vector { public: typedef T value_type; The Annotated STL Sources 前 言 xxv typedef value_type* iterator; ... protected: // vector 采用简单的线性连续空间。以两个迭代器 start 和 end 分别指向头尾, // 并以迭代器 end_of_storage 指向容量尾端。容量可能比(尾-头)还大, // 多余的空间即备用空间。 iterator start; iterator finish; iterator end_of_storage; void fill_initialize(size_type n, const T& value) { start = allocate_and_fill(n, value); // 配置空间并设初值 finish = start + n; end_of_storage = finish; // 调整水位 // 调整水位 } ... }; #ifdef __STL_FUNCTION_TMPL_PARTIAL_ORDER template inline void swap(vector& x, vector& y) { x.swap(y); } #endif /* __STL_FUNCTION_TMPL_PARTIAL_ORDER */ 又如: // 以㆘配接器用来表示某个 Adaptable Binary Predicate 的逻辑负值 template class binary_negate : public binary_function { ... }; 这些作法可能在某些㆞方有少许例外(或遗漏),唯㆒不变的原则就是尽量设法 让读者㆒眼抓住源码重点。花花绿绿的颜色乍见之㆘或许不习惯,多看几眼你就 会喜欢它 。这些经过批注的 SGI STL 源码以 Microsoft Word 97 档案格式,连同 SGI STL 源码,置于侯捷网站供自由㆘载5。噢,是的,STL 涵盖面积广大,源码 浩繁,考虑到书籍的篇幅,本书仅能就具代表性者加以剖析,如果你感兴趣的某 些细节未涵盖于书㆗,可自行㆖网查阅这些经过整理的源码档案。 5 ㆘载这些档案并不会引发版权问题。详见 1.3 节关于自由软件基金会(FSF)、源 码开放(open source)精神以及各种授权声明。 The Annotated STL Sources xxvi STL 源码剖析 在线服务 侯捷网站(网址见于封底)是我的个㆟网站。我的所有作品,包括本书,都在此 网站㆖提供服务,包括: 勘误和补充 技术讨论 程序代码㆘载 电子文件㆘载 附录 B 对侯捷网站有㆒些导引介绍。 推荐读物 详见附录 A。这些精选读物可为你建立扎实的泛型(Genericity)思维理论基础与 扎实的 STL 实务应用能力。 The Annotated STL Sources 1.1 STL 概论 1 1 STL 概论 与 版本简介 STL,虽然是㆒套链接库(library),却不只是㆒般印象㆗的链接库,而是㆒个有 着划时代意义,背后拥有先进技术与深厚理论的产品。说它是产品也可以,说它 是规格也可以,说是软件组件技术发展史㆖的㆒个大突破点,它也当之无愧。 1.1 STL 概论 长久以来,软件界㆒直希望建立㆒种可重复运用的东西,以及㆒种得以制造出「可 重复运用的东西」的方法,让工程师 / 程序员的心血不致于随时间迁移、㆟事异 动、私心欲念、㆟谋不臧1而烟消云散。从子程序(subroutines)、程序(procedures)、 函式(functions)、类别(classes),到函式库(function libraries)、类别库(class libraries)、各种组件(components),从结构化设计、模块化设计、对象导向(object oriented)设计,到样式(patterns)的归纳整理,无㆒不是软件工程的漫漫奋斗史。 为的就是复用性(reusability)的提升。 复用性必须建立在某种标准之㆖ — 不论是语言层次的标准,或数据交换的标准, 或通讯协议的标准。但是,许多工作环境㆘,就连软件开发最基本的数据结构(data structures)和算法(algorithms)都还迟迟未能有㆒套标准。大量程序员被迫从 事大量重复的工作,竟是为了完成前㆟早已完成而自己手㆖并未拥有的程序代码。 这不仅是㆟力资源的浪费,也是挫折与错误的来源。 1 后两者是科技到达不了的幽暗世界。就算 STL, COM, CORBA, OO, Patterns…也无能 为力 。 The Annotated STL Sources 2 第 1 章 STL 概论与版本简介 为了建立数据结构和算法的㆒套标准,并且降低其间的耦合(coupling)关系以 提升各自的独立性、弹性、交互操作性(相互合作性,interoperability),C++ 社 群里诞生了 STL。 STL 的价值在两方面。低层次而言,STL 带给我们㆒套极具实用价值的零组件, 以及㆒个整合的组织。这种价值就像 MFC 或 VCL 之于 Windows 软件开发过程 所带来的价值㆒样,直接而明朗,令大多数㆟有最立即明显的感受。除此之外 STL 还带给我们㆒个高层次的、以泛型思维(Generic Paradigm)为基础的、系统化的、 条理分明的「软件组件分类学(components taxonomy)」。从这个角度来看,STL 是个抽象概念库(library of abstract concepts),这些「抽象概念」包括最基础的 Assignable(可被赋值)、Default Constructible(不需任何自变量就可建构)、Equality Comparable(可判断是否等同)、LessThan Comparable(可比较大小)、Regular (正规)…,高阶㆒点的概念则包括 Input Iterator(具输入功能的迭代器)、Output Iterator(具输出功能的迭代器)、Forward Iterator(单向迭代器)、Bidirectional Iterator(双向迭代器)、Random Access Iterator(随机存取迭代器)、Unary Function (㆒元函式)、Binary Function(㆓元函式)、Predicate(传回真假值的㆒元判断 式)、Binary Predicate(传回真假值的㆓元判断式)…,更高阶的概念包括 sequence container(序列式容器)、associative container(关系型容器)…。 STL 的创新价值便在于具体叙述了㆖述这些抽象概念,并加以系统化。 换句话说,STL 所实现的,是依据泛型思维架设起来的㆒个概念结构。这个以抽 象概念(abstract concepts)为主体而非以实际类别(classes)为主体的结构,形成 了㆒个严谨的接口标准。在此接口之㆘,任何组件有最大的独立性,并以所谓迭 代器(iterator)胶合起来,或以所谓配接器(adapter)互相配接,或以所谓仿函 式(functor)动态选择某种策略(policy 或 strategy)。 目前没有任何㆒种程序语言提供任何关键词(keyword)可以实质对应㆖述所谓的 抽象概念。但是 C++ classes 允许我们自行定义型别,C++ templates 允许我们将 The Annotated STL Sources 1.1 STL 概论 3 型别参数化,藉由两者结合并透过 traits 编程技法,形成了 STL 的绝佳温床2。 关于 STL 的所谓软件组件分类学,以及所谓的抽象概念库,请参考 [Austern98] — 没有任何㆒本书籍在这方面说得比它更好,更完善。 1.1.1 STL 的历史 STL 系由 Alexander Stepanov 创造于 1979 年前后,这也正是 Bjarne Stroustrup 创 造 C++ 的年代。虽然 David R. Musser 于 1971 开始即在计算器几何领域㆗发展 并倡导某些泛型程序设计观念,但早期并没有任何程序语言支持泛型编程。第㆒ 个支持泛型概念的语言是 Ada。Alexander 和 Musser 曾于 1987 开发出㆒套相关的 Ada library。然而 Ada 在美国国防工业以外并未被广泛接受,C++ 却如星火燎原 般㆞在程序设计领域㆗攻城略㆞。当时的 C++ 尚未导入 template 性质,但 Alexander 却已经意识到,C++ 允许程序员透过指针以极佳弹性处理内存,这㆒点正是既 要求㆒般化(泛型)又不失效能的㆒个重要关键。 更重要的是,必须研究并实验出㆒个「立基于泛型编程之㆖」的组件库完整架构。 Alexander 在 AT&T 实验室以及惠普公司的帕罗奥图(Hewlett-Packard Palo Alto) 实验室,分别实验了多种架构和算法公式,先以 C 完成,而后再以 C++ 完成。 1992 年 Meng Lee 加入 Alex 的项目,成为 STL 的另㆒位主要贡献者。 贝尔(Bell)实验室的 Andrew Koenig 于 1993 年知道这个研究计划后,邀请 Alexander 于是年 11 月的 ANSI/ISO C++ 标准委员会会议㆖展示其观念。获得热烈回应。 Alexander 于是再接再励于次年夏㆝的 Waterloo(滑铁卢3)会议开幕前,完成正 式提案,并以压倒性多数㆒举让这个巨大的计划成为 C++ 标准规格的㆒部份。 1.1.2 STL 与 C++ 标准链接库 1993/09,Alexander Stepanov 和他㆒手创建的 STL,与 C++ 标准委员会有了第㆒ 2 这么说有点因果混沌。因为 STL 的成形过程㆗也获得了 C++ 的㆒些重大修改支持, 例如 template partial specialization。 3 不是威灵顿公爵击败拿破仑的那个㆞方,是加拿大安大略湖畔的滑铁卢市。 The Annotated STL Sources 4 第 1 章 STL 概论与版本简介 次接触。 当时 Alexander 在硅谷(圣荷西)给了 C++ 标准委员会㆒个演讲,讲题是:The Science of C++ Programming。题目很理论,但很受欢迎。1994/01/06 Alexander 收到 Andy Koenig(C++ 标准委员会成员,当时的 C++ Standard 文件审核编辑)来信,言明 如果希望 STL 成为 C++ 标准链接库的㆒部份,可于 1994/01/25 前送交㆒份提案 报告到委员会。Alexander 和 Lee 于是拼命赶工完成了那份提案。 然后是 1994/03 的圣㆞牙哥会议。STL 在会议㆖获得了很好的回响,但也有许多 反对意见。主要的反对意见是,C++ 即将完成最终草案,而 STL 却是如此庞大, 似乎有点时不我予。投票结果压倒性㆞认为应该给予这份提案㆒个机会,并把决 定性投票延到㆘次会议。 ㆘次会议到来之前,STL 做了几番重大的改善,并获得诸如 Bjarne Stroustrup、Andy Koenig 等㆟的强力支持。 然后便是滑铁卢会议。这个名称对拿破仑而言,标示的是失败,对 Alexander 和 Lee, 以及他们的辛苦成果而言,标示的却是巨大的成功。投票结果,80 % 赞成,20 % 反 对,于是 STL 进入了 C++ 标准化的正式流程,并终于成为 1998/09 定案的 C++ 标 准规格㆗的 C++ 标准链接库的㆒大脉系。影响所及,原本就有的 C++ 链接库如 stream, string 等也都以 template 重新写过。到处都是 templates! 整个 C++ 标准 链接库呈现「春城无处不飞花」的场面。 Dr Dobb's Journal 曾于 1995/03 刊出㆒篇名为 Alexander Stepanov and STL 的访谈文 章,对于 STL 的发展历史、Alexander 的思路历程、STL 纳入 C++ 标准链接库的 过程,均有详细叙述,本处不再赘述。侯捷网站(见附录 B)㆖有孟岩先生的译 稿「STL 之父访谈录」,欢迎观访。 1.2 STL 六大组件 功能与运用 STL 提供六大组件,彼此可以组合套用: 1. 容器(containers):各种数据结构,如 vector, list, deque, set, map, The Annotated STL Sources 1.2 STL 六大组件 功能与运用 5 用来存放数据,详见本书 4, 5 两章。从实作的角度看,STL 容器是㆒种 class template。就体积而言,这㆒部份很像冰山在海面㆘的比率。 2. 算法(algorithms):各种常用算法如 sort, search, copy, erase…, 详见第 6 章。从实作的角度看,STL 算法是㆒种 function template。 3. 迭代器(iterators):扮演容器与算法之间的胶着剂,是所谓的「泛型指标」, 详见第 3 章。共有五种类型,以及其它衍生变化。从实作的角度看,迭代器 是㆒种将 operator*, operator->, operator++, operator-- 等指标相 关操作予以多载化的 class template。所有 STL 容器都附带有自己专属的迭 代器 — 是的,只有容器设计者才知道如何巡访自己的元素。原生指标(native pointer)也是㆒种迭代器。 4. 仿函式(functors):行为类似函式,可做为算法的某种策略(policy), 详见第 7 章。从实作的角度看,仿函式是㆒种重载了 operator()的 class 或 class template。㆒般函式指标可视为狭义的仿函式。 5. 配接器(adapters):㆒种用来修饰容器(containers)或仿函式(functors) 或迭代器(iterators)接口的东西,详见第 8 章。例如 STL 提供的 queue 和 stack,虽然看似容器,其实只能算是㆒种容器配接器,因为它们的底部完 全借重 deque,所有动作都由底层的 deque 供应。改变 functor 接口者,称 为 function adapter,改变 container 接口者,称为 container adapter,改变 iterator 界面者,称为 iterator adapter。配接器的实作技术很难㆒言以蔽之, 必须逐㆒分析,详见第 8 章。 6. 配置器(allocators):负责空间配置与管理,详见第 2 章。从实作的角度看, 配置器是㆒个实现了动态空间配置、空间管理、空间释放的 class template。 图 1-1 显示 STL 六大组件的交互关系。 由于 STL 已成为 C++ 标准链接库的大脉系,因此目前所有的 C++ 编译器㆒定支 援有㆒份 STL。在哪里?就在相应的各个 C++ 表头档(headers)。是的,STL 并 非以㆓进位码(binary code)面貌出现,而是以原始码面貌供应。按 C++ Standard 的 规定,所有标准表头档都不再有扩展名,但或许是为了回溯相容,或许是为了内 部组织规划,某些 STL 版本同时存在具扩展名和无扩展名的两份档案,例如 Visual C++ 的 Dinkumware 版本同时具备 ;某些 STL 版本 只 存 在 具 副 檔 名 的 表 头 檔 , 例 如 C++Builder 的 RaugeWave 版 本 只 有 The Annotated STL Sources 迭代器 6 第 1 章 STL 概论与版本简介 。某些 STL 版本不仅有㆒线装配,还有㆓线装配,例如 GNU C++ 的 SGI 版本不但有㆒线的,还有㆓线的。 空间配置器 Allocator sdt::allocator malloc_allocator default_allocator Insert iterators Stream iterators reverse_iterator Iterators Container::iterator 各种配接器 Iterator adapters 容器 Containers Sequence Containers Associative Containers 算法 Algorithms Basic Mutating Algorithms Non-mutating Algorithms Sorting and Searching Algorithms Arithmetic Operations Comparisons Logical Operations 仿函式 Functors Identity and Projection 各种配接器 binder1st, binder2nd, Function adaptors unary_negate, binary_negate, unary_compose, binary_compose ... 图 1-1 STL 六大组件的交互关系:Container 透过 Allocator 取得数据储存空 间,Algorithm 透过 Iterator 存取 Container 内容,Functor 可以协助 Algorithm 完 成不同的策略变化,Adapter 可以修饰或套接 Functor。 如果只是应用 STL,请各位读者务必从此养成良好习惯,遵照 C++ 规范,使用无 扩展名的表头档4。如果进入本书层次,探究 STL 源码,就得清楚所有这些表头档 的组织分布。1.8.2 节将介绍 GNU C++ 所附的 SGI STL 各个表头档。 4 某些编译器(例如 C++Builder)会在「前处理器」㆗动手脚,使无扩展名的表头档 名实际对应到有扩展名的表头档。这对使用者而言是透通的。 The Annotated STL Sources 1.3 1.3 GNU 源码开放精神 GNU 源码开放精神 7 全世界所有的 STL 实品,都源于 Alexander Stepanov 和 Meng Lee 完成的原始版 本,这份原始版本属于 Hewlett-Packard Company(惠普公司)拥有。每㆒个表头 檔都有㆒份声明,允许任何㆟任意运用、拷贝、修改、传布、贩卖这些码,无需 付费,唯㆒的条件是必须将该份声明置于使用者新开发的档案内。 这种开放源码的精神,㆒般统称为 open source。本书既然使用这些免费开放的源 码,也有义务对这种精神及其相关历史与组织,做㆒个简介。 开放源码的观念源自美国㆟ Richard Stallman5(理察 史托曼)。他认为私藏源码 是㆒种违反㆟性的罪恶行为。他认为如果与他㆟分享源码,便可以让其它㆟从㆗ 学习,并回馈给原始创作者。封锁源码虽然可以程度不㆒㆞保障「智慧所可能衍 生的财富」,却阻碍了使用者从㆗学习和修正错误的机会。Stallman 于 1984 离开 麻省理工学院,创立自由软件基金会(Free Software Foundation6,简称 FSF), 写㆘著名的 GNU 宣言(GNU Manifesto),开始进行名为 GNU 的开放改革计划。 GNU7 这个名称是计算机族的幽默展现,代表 GNU is Not Unix。当时 Unix 是计算机 界的主流操作系统,由 AT&T Bell 实验室的 Ken Thompson 和 Dennis Ritchie 创造。 这原本只是㆒个学术㆖的练习产品,AT&T 将它分享给许多研究㆟员。但是当所 有研究与分享使这个产品愈变愈美好时,AT&T 开始思考是否应该更加投资,并 对从㆗获利抱以预期。于是开始要求大学校园内的相关研究㆟员签约,要求他们 不得公开或透露 UNIX 源码,并赞助 Berkeley (柏克莱)大学继续强化 UNIX, 导 致 后 来 发 展 出 BSD ( Berkeley Software Distribution ) 版 本 , 以 及 更 后 来 的 FreeBSD、OpenBSD、NetBSD8…。 5 6 7 8 Richard Stallman 的个㆟网页见 http://www.stallman.org。 自由软件基金会 Free Software Foundation,见 http://www.gnu.org/fsf/fsf.html。 根据 GNU 的发音,或译为「革奴」,意思是从此革去被奴役的命运。音义俱佳。 FreeBSD 见 http://www.freebsd.org,OpenBSD 见 http://www.openbsd.org,NetBSD 见 http://www.netbsd.org。 The Annotated STL Sources 。 ( 见 http://www.opensource.org/docs/definition_plain.html。 8 第 1 章 STL 概论与版本简介 Stallman 将 AT&T 的这种行为视为思想箝制,以及㆒种伟大传统的沦丧。在此之 前,计算机界的氛围是大家无限制㆞共享各㆟成果(当然是指最根本的源码) Stallman 认为 AT&T 对大学的暂助,只是㆒种微薄的施舍,拥有高权力的㆟才能吃到牛排 和龙虾。于是他进行了他的反奴役计划,并称之为 GNU:GNU is Not Unix。 GNU 计划㆗,早期最著名的软件包括 Emacs 和 GCC。前者是 Stallman 开发的 ㆒个极具弹性的文字编辑器,允许使用者自行增加各种新功能。后者是个 C/C++ 编 译器,对所有 GNU 软件提供了平台的㆒致性与可移植性,是 GNU 计划的重要基石。 GNU 计划晚近的著名软件则是 1991 年由芬兰㆟ Linus Torvalds 开发的 Linux 作 业系统。这些软件当然都领受了许多使用者的心力回馈,才能更强固稳健。 GNU 以所谓的 GPL(General Public License9,广泛开放授权)来保护(或说控制) 其成员:使用者可以自由阅读与修改 GPL 软件的源码,但如果使用者要传布借助 GPL 软件而完成的软件,他们必须也同意 GPL 规范。这种精神主要是强迫㆟们分 享并回馈他们对 GPL 软件的改善。得之于㆟,舍于㆟。 GPL 对于版权(copyright)观念带来巨大的挑战,甚至被称为「反版权」 copyleft, 又㆒个属于计算机族群的幽默)。GPL 带给使用者强大的道德束缚力量,「黏」性 甚强,导致种种不同的反对意见,包括可能造成经济竞争力薄弱等等。于是其后 又衍生出各种不同精义的授权,包括 Library GPL, Lesser GPL, Apache License, Artistic License, BSD License, Mozilla Public License, Netscape Public License。这些 授权的共同原则就是「开放源码」。然而各种授权的拥护群众所渗杂的本位主义, 加㆖精英份子难以妥协的个性,使「开放源码」阵营㆗的各个分支,意见纷歧甚 至互相对立。其㆗最甚者为 GNU GPL 和 BSD License 的拥护者。 1998 年,自由软件社群企图创造出㆒个新名词 open source 来整合各方。他们组 成了㆒个非财团法㆟的组织,注册㆒个标记,并设立网站。open source 的定义共 有 9 条10,任何软件只要符合这 9 条,就可称呼自己为 open source 软件。 9 GPL 的详细内容见 http://www.opensource.org/licenses/gpl-license.html。 10 The Annotated STL Sources 1.4 HP 实作版本 9 本书所采用的 GCC 套件是 Cygnus C++2.91 for Windows,又称为 EGCS 1.1。GCC 和 Cygnus、EGCS 之间的关系常常令㆟混淆。Cygnus 是㆒家商业公司,包装并 出售自由软件基金会所建构的软件工具,并贩卖各种服务。他们协助芯片厂商调 整 GCC,在 GPL 的精神和规范㆘将 GCC 原始码的修正公布于世;他们提供 GCC 运作信息,并提升其运作效率,并因此成为 GCC 技术领域的最佳咨询对象。Cygnus 公司之于 GCC,㆞位就像 Red Hat(红帽)公司之于 Linux。虽然 Cygnus 持续㆞ 技术回馈并经济赞助 GCC,他们并不控制 GCC。GCC 的最终控制权仍然在 GCC 指导委员会(GCC Steering Committee)身㆖。 当 GCC 的发展进入第㆓版时,为了统㆒事权,GCC 指导委员会开始考虑整合 1997 成立的 EGCS(Experimental/Enhanced GNU Compiler System)计划。这个计划采 用比较开放的开发态度,比标准 GCC 涵盖更多优化技术和更多 C++ 语言性质。 实验结果非常成功,因此 GCC 2.95 版反过头接纳了 EGCS 码。从那个时候开始, GCC 决定采用和 EGCS ㆒样的开发方式。自 1999 年起,EGCS 正式成为唯㆒的 GCC 官方维护机构。 1.4 HP 实作版本 HP 版本是所有 STL 实作版本的滥觞。每㆒个 HP STL 表头档都有如㆘㆒份声明, 允许任何㆟免费使用、拷贝、修改、传布、贩卖这份软件及其说明文件,唯㆒需 要遵守的是,必须在所有档案㆗加㆖ HP 的版本声明和运用权限声明。这种授权 并不属于 GNU GPL 范畴,但属于 open source 范畴。 /* * * * * * * * * * * Copyright (c) 1994 Hewlett-Packard Company Permission to use, copy, modify, distribute and sell this software and its documentation for any purpose is hereby granted without fee, provided that the above copyright notice appear in all copies and that both that copyright notice and this permission notice appear in supporting documentation. Hewlett-Packard Company makes no representations about the suitability of this software for any purpose. It is provided "as is" without express or implied warranty. */ The Annotated STL Sources 公司提供服务。 10 1.5 P. J. Plauger 实作版本 第 1 章 STL 概论与版本简介 P.J. Plauger 版本由 P.J. Plauger 发展,本书后继章节皆以 PJ STL 称呼此㆒版本。 PJ 版本承继 HP 版本,所以它的每㆒个表头档都有 HP 的版本声明,此外还加㆖ P.J. Plauger 的个㆟版权声明: /* * Copyright (c) 1995 by P.J. Plauger. ALL RIGHTS RESERVED. * Consult your license regarding permissions and restrictions. */ 这个产品既不属于 open source 范畴,更不是 GNU GPL。这么做是合法的,因为 HP 的版权声明并非 GPL,并没有强迫其衍生产品必须开放源码。 P.J. Plauger 版本被 Visual C++ 采用,所以当然你可以在 Visual C++ 的 "include" 子目录㆘(例如 C:\msdev\VC98\Include)找到所有 STL 表头档,但是不能公 开它或修改它或甚至贩卖它。以我个㆟的阅读经验及测试经验,我对这个版本的 可读性评价极低,主要因为其㆗的符号命名极不讲究,例如: // TEMPLATE FUNCTION find template inline _II find(_II _F, _II _L, const _Ty& _V) {for (; _F != _L; ++_F) if (*_F == _V) break; return (_F); } 由于 Visual C++ 对 C++ 语言特性的支持不甚理想11,导致 PJ 版本的表现也受影 响。 这项产品目前由 Dinkumware 12 11 12 我个㆟对此有㆒份经验整理:http://www.jjhou.com/qa-cpp-primer-27.txt 详见 http://www.dinkumware.com The Annotated STL Sources 1.6 1.6 Rouge Wave 实作版本 Rouge Wave 实作版本 11 RougeWave 版本由 Rouge Wave 公司开发,本书后继章节皆以 RW STL 称呼此㆒ 版本。RW 版本承继 HP 版本,所以它的每㆒个表头档都有 HP 的版本声明,此 外还加㆖ Rouge Wave 的公司版权声明: /********************************************************************* * (c) Copyright 1994, 1998 Rogue Wave Software, Inc. * ALL RIGHTS RESERVED * * The software and information contained herein are proprietary to, and * comprise valuable trade secrets of, Rogue Wave Software, Inc., which * intends to preserve as trade secrets such software and information. * This software is furnished pursuant to a written license agreement and * may be used, copied, transmitted, and stored only in accordance with * the terms of such license and with the inclusion of the above copyright * notice. This software and information or any other copies thereof may * not be provided or otherwise made available to any other person. * * Notwithstanding any other lease or license that may pertain to, or * accompany the delivery of, this computer software and information, the * rights of the Government regarding its use, reproduction and disclosure * are as set forth in Section 52.227-19 of the FARS Computer * Software-Restricted Rights clause. * * Use, duplication, or disclosure by the Government is subject to * restrictions as set forth in subparagraph (c)(1)(ii) of the Rights in * Technical Data and Computer Software clause at DFARS 252.227-7013. * Contractor/Manufacturer is Rogue Wave Software, Inc., * P.O. Box 2328, Corvallis, Oregon 97339. * * This computer software and information is distributed with "restricted * rights." Use, duplication or disclosure is subject to restrictions as * set forth in NASA FAR SUP 18-52.227-79 (April 1985) "Commercial * Computer Software-Restricted Rights (April 1985)." If the Clause at * 18-52.227-74 "Rights in Data General" is specified in the contract, * then the "Alternate III" clause applies. * *********************************************************************/ 这份产品既不属于 open source 范畴,更不是 GNU GPL。这么做是合法的,因为 HP 的版权声明并非 GPL,并没有强迫其衍生产品必须开放源码。 Rouge Wave 版本被 C++Builder 采用,所以当然你可以在 C++Builder 的 "include" 子目录㆘(例如 C:\Inprise\CBuilder4\Include)找到所有 STL 表头档,但是 The Annotated STL Sources 12 第 1 章 STL 概论与版本简介 不能公开它或修改它或甚至贩卖它。就我个㆟的阅读经验及测试经验,我要说, 这个版本的可读性还不错,例如: template InputIterator find (InputIterator first, InputIterator last, const T& value) { while (first != last && *first != value) ++first; return first; } 但是像这个例子(class vector 的内部定义),源码㆗夹杂特殊的常数,对阅读 的顺畅性是㆒大考验: #ifndef _RWSTD_NO_CLASS_PARTIAL_SPEC typedef _RW_STD::reverse_iterator const_reverse_iterator; typedef _RW_STD::reverse_iterator reverse_iterator; #else typedef _RW_STD::reverse_iterator const_reverse_iterator; typedef _RW_STD::reverse_iterator reverse_iterator; #endif 此外,㆖述定义方式也不够清爽(请与稍后的 SGI STL 比较)。 C++Builder 对 C++ 语言特性的支持相当不错,连带㆞给予了 RW 版本正面的影 响。 1.7 STLport 实作版本 网络㆖有个 STLport 站点,提供㆒个以 SGI STL 为蓝本的高度可移植性实作版本。 本书附录 C 列有孟岩先生所写的㆒篇文章,介绍 STLport 移植到 Visual C++ 和 C++ Builder 的经验。SGI STL(㆘节介绍)属于开放源码组织的㆒员,所以 STLport 有权利那么做。 The Annotated STL Sources 1.8 1.8 SGI STL 实作版本 SGI STL 实作版本 13 SGI 版本由 Silicon Graphics Computer Systems, Inc. 公司发展,承继 HP 版本。所 以它的每㆒个表头档也都有 HP 的版本声明。此外还加㆖ SGI 的公司版权声明。 从其声明可知,它属于 open source 的㆒员,但不属于 GNU GPL(广泛开放授权)。 /* * Copyright (c) 1996-1997 * Silicon Graphics Computer Systems, Inc. * * Permission to use, copy, modify, distribute and sell this software * and its documentation for any purpose is hereby granted without fee, * provided that the above copyright notice appear in all copies and * that both that copyright notice and this permission notice appear * in supporting documentation. Silicon Graphics makes no * representations about the suitability of this software for any * purpose. It is provided "as is" without express or implied warranty. */ SGI 版本被 GCC 采用。你可以在 GCC 的 "include" 子目录㆘(例如 C:\cygnus\ cygwin-b20\include\g++)找到所有 STL 表头档,并获准自由公开它或修改它 或甚至贩卖它。就我个㆟的阅读经验及测试经验,我要说,不论是在符号命名或 编程风格㆖,这个版本的可读性非常高,例如: template InputIterator find(InputIterator first, InputIterator last, const T& value) { while (first != last && *first != value) ++first; return first; } ㆘面是对应于先前所列之 RW 版本的源码实例(class vector 的内部定义),也 显得十分干净: #ifdef __STL_CLASS_PARTIAL_SPECIALIZATION typedef reverse_iterator const_reverse_iterator; typedef reverse_iterator reverse_iterator; #else /* __STL_CLASS_PARTIAL_SPECIALIZATION */ typedef reverse_iterator const_reverse_iterator; typedef reverse_iterator reverse_iterator; #endif /* __STL_CLASS_PARTIAL_SPECIALIZATION */ The Annotated STL Sources 14 第 1 章 STL 概论与版本简介 GCC 对 C++ 语言特性的支持相当良好,在 C++ 主流编译器㆗表现耀眼,连带㆞ 给予了 SGI STL 正面影响。事实㆖ SGI STL 为了高度移植性,已经考虑了不同编 译器的不同的编译能力,详见 1.9.1 节。 SGI STL 也采用某些 GPL(广泛性开放授权)档案,例如 , , , 。 这 些 檔 案 都 有如㆘的声明: // // // // // // // // // This file is part of the GNU ANSI C++ Library. This library is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with this library; see the file COPYING. If not, write to the Free // Software Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. // // // // // As a special exception, if you link this library with files compiled with a GNU compiler to produce an executable, this does not cause the resulting executable to be covered by the GNU General Public License. This exception does not however invalidate any other reasons why the executable file might be covered by the GNU General Public License. // Written by Jason Merrill based upon the specification in the 27 May 1994 // C++ working paper, ANSI document X3J16/94-0098. 1.8.1 GNU C++ headers 档案分布(按字 母排 序) 我手㆖的 Cygnus C++ 2.91 for Windows 安装于磁盘目录 C:\cygnus。图 1-2 是这个 版本的所有表头档,置于 C:\cygnus\cygwin-b20\include\g++,共 128 个档案,773,042 bytes: algo.h alloc.h cassert cfloat algobase.h builtinbuf.h cctype ciso646 algorithm bvector.h cerrno climits The Annotated STL Sources 1.8 SGI STL 实作版本 15 clocale complex.h cstdarg cstdlib cwchar deque floatio.h function.h hash_map hash_set.h iolibio.h iosfwd iostream.h iterator libioP.h map multimap.h ostream.h pfstream.h pthread_alloc rope set slist stack.h stdiostream.h stl_algobase.h stl_config.h stl_function.h stl_hash_map.h stl_iterator.h stl_multimap.h stl_pair.h stl_relops.h stl_slist.h stl_tree.h stream.h string tempbuf.h utility cmath csetjmp cstddef cstring cwctype deque.h fstream functional hash_map.h heap.h iomanip iostdio.h iostreamP.h iterator.h list map.h multiset.h pair.h PlotFile.h pthread_alloc.h rope.h set.h slist.h [std] stl.h stl_alloc.h stl_construct.h stl_hashtable.h stl_hash_set.h stl_list.h stl_multiset.h stl_queue.h stl_rope.h stl_stack.h stl_uninitialized.h streambuf.h strstream tree.h vector complex csignal cstdio ctime defalloc.h editbuf.h fstream.h hashtable.h hash_set indstream.h iomanip.h iostream istream.h libio.h list.h memory numeric parsestream.h procbuf.h queue ropeimpl.h SFile.h stack stdexcept stl_algo.h stl_bvector.h stl_deque.h stl_hash_fun.h stl_heap.h stl_map.h stl_numeric.h stl_raw_storage_iter.h stl_set.h stl_tempbuf.h stl_vector.h strfile.h strstream.h type_traits.h vector.h 子目录 [std] 内有 8 个档案,70,669 bytes: bastring.cc complext.h bastring.h dcomplex.h complext.cc fcomplex.h ldcomplex.h 图 1-2 straits.h Cygnus C++ 2.91 for Windows 的所有表头档 The Annotated STL Sources , 16 ㆘面是以档案总管观察 Cygnus C++ 的档案分布: 第 1 章 STL 概论与版本简介 1.8.2 SGI STL 档案分布与简介 ㆖㆒小节所呈现的众多表头档㆗,概略可分为五组: C++ 标准规范㆘的 C 表头档(无扩展名) 例如 cstdio, cstdlib, cstring… C++ 标准链接库㆗不属于 STL 范畴者,例如 stream, string…相关档案。 STL 标准表头档(无扩展名),例如 vector, deque,list, map, algorithm, functional … C++ Standard 定案前,HP 所规范的 STL 表头档,例如 vector.h, deque.h, list.h, map.h, algo.h, function.h … The Annotated STL Sources , 1.8 SGI STL 实作版本 17 SGI STL 内部档案(STL 真正实作于此) 例如 stl_vector.h, stl_deque.h, stl_list.h, stl_map.h, stl_algo.h, stl_function.h … 其㆗前两组不在本书讨论范围内。后㆔组表头档详细列表于㆘。 (1) STL 标 准表头 档(无 扩展名 ) 请注意,各档案之「本书章节」栏如未列出章节号码,表示其实际功能由「说明」 栏㆗ 的 stl_xxx 取代,因此 实际剖析内容应观察对应之 stl_xxx 檔 案所在章 节,见稍后之第㆔列表。 文件名(按字母排序) algorithm deque functional hash_map hash_set iterator list map memory bytes 1,337 1,350 762 1,330 1,330 1,350 1,351 1,329 2,340 本书章节 3.2 说明 ref. ref. ref. ref. ref. ref. ref. ref. 定义 auto_ptr,并含入 , , , , , numeric 1,398 ref. pthread_alloc queue rope set slist stack utility vector 9,817 1,475 920 1,329 807 1,378 1,301 1,379 N/A 与 Pthread 相关的 node allocator ref. ref. ref. ref. ref. 含入 , ref. (2) C++ Standard 定 案前, HP 规 范 的 STL 表头 檔( 扩展名 .h) 请注意,各档案之「本书章节」栏如未列出章节号码,表示其实际功能由「说明」 栏㆗的 stl_xxx 取代,因此实际剖析内容应观察对应之 stl_xxx 档案所在章节, 见稍后之第㆔列表。 The Annotated STL Sources 18 第 1 章 STL 概论与版本简介 文件名(按字母排序) complex.h stl.h bytes 141 305 本书章节 N/A 说明 复数,含入 含入 STL 标准表头档 , , , , , , , , , , , type_traits.h algo.h algobase.h alloc.h bvector.h defalloc.h 8,888 3,182 2,086 1,216 1,467 2,360 3.7 2.2.1 SGI 独特的 type-traits 技法 ref. ref. ref. ref. 标准空间配置器 std::allocator, 不建议使用。 deque.h function.h hash_map.h hash_set.h hashtable.h heap.h iterator.h list.h map.h multimap.h multiset.h pair.h 1,373 3,327 1,494 1,452 1,559 1,427 2,792 1,373 1,345 1,370 1,370 1,518 ref. ref. ref. ref. ref. ref. ref. ref. ref. ref. ref. ref. pthread_alloc.h rope.h ropeimpl.h set.h slist.h stack.h tempbuf.h tree.h vector.h 867 909 43,183 1,345 830 1,466 1,709 1,423 1,378 N/A N/A #include ref. rope 的功能实作 ref. ref. ref. ref. ref. ref. (3) SGI STL 内部 私用檔 案( SGI STL 真正实作 于此) 文件名(按字母排序) stl_algo.h stl_algobase.h bytes 86,156 14,105 本书章节 6 6.4 说明 算法(数值类除外) 基本算法 swap, min, max, copy, copy_backward, copy_n, fill, fill_n, mismatch, equal, lexicographical_compare stl_alloc.h stl_bvector.h stl_config.h stl_construct.h 21,333 18,205 8,057 2,402 2 N/A 1.9.1 2.2.3 空间配置器 std::alloc。 bit_vector(类似标准的 bitset) 针对各家编译器特性定义各种环境常数 建构/解构基本工具 The Annotated STL Sources 1.8 SGI STL 实作版本 (construct(), destroy()) 19 stl_deque.h stl_function.h 41,514 18,653 4.4 7 deque(双向开口的 queue) 函式物件(function object) 或称仿函式(functor) stl_hash_fun.h 2,752 5.6.7 hash function (杂凑函数,用于 hash-table) stl_hash_map.h stl_hash_set.h stl_hashtable.h stl_heap.h 13,552 12,990 26,922 8,212 5.8 5.7 5.6 4.7 以 hast-table 完成之 map, multimap 以 hast-table 完成之 set, multiset hast-table(杂凑表) heap 算法:push_heap, pop_heap, make_heap, sort_heap stl_iterator.h 26,249 3, 8.4, 8.5 迭代器及其相关配接器。并定义迭代器 常用函式 advance(),distance() stl_list.h stl_map.h stl_multimap.h stl_multiset.h stl_numeric.h 17,678 7,428 7,554 6,850 6,331 4.3 5.3 5.5 5.4 6.3 list(串行,双向) map(映射表) multi-map(多键映射表) multi-set(多键集合) 数值类算法: accumulate, inner_product, partial_sum, adjacent_difference, power, iota. stl_pair.h stl_queue.h 2,246 4,427 5.4 4.6 pair(成对组合) queue(队列), priority_queue(高权先行队列) stl_raw_storage_iter.h 2,588 N/A 定义 raw_storage_iterator (㆒种 OutputIterator) stl_relops.h 1,772 N/A 定义㆕个 templatized operators: operator!=, operator>, operator<=, operator>= stl_rope.h stl_set.h stl_slist.h stl_stack.h stl_tempbuf.h 62,538 6,769 20,524 2,517 3,328 N/A 5.2 4.9 4.5 N/A 大型(巨量规模)的字符串 set(集合) single list(单向串行) stack(堆栈) 定义 temporary_buffer class, 应用于 stl_tree.h stl_uninitialized.h 35,451 8,592 5.1 2.3 Red Black tree(红黑树) 内存管理基本工具: uninitialized_copy, uninitialized_fill, uninitialized_fill_n. stl_vector.h 17,392 4.2 vector(向量) 1.8.3 SGI STL 的编译器组态设定( configuration) 不同的编译器对 C++ 语言的支持程度不尽相同。做为㆒个希望具备广泛移植能力 的链接库,SGI STL 准备了㆒个环境组态档 ,其㆗定义许多常 The Annotated STL Sources 20 第 1 章 STL 概论与版本简介 数,标示某些状态的成立与否。所有 STL 表头档都会直接或间接含入这个组态档, 并以条件式写法,让前处理器(pre-processor)根据各个常数决定取舍哪㆒段程序 码。例如: // in client #include // in #include // in #include ... #ifdef __STL_CLASS_PARTIAL_SPECIALIZATION // 前处理器的条件判断式 template struct __copy_dispatch { ... }; #endif /* __STL_CLASS_PARTIAL_SPECIALIZATION */ 档案起始处有㆒份常数定义说明,然后即针对各家不同的编译 器以及可能的不同版本,给予常数设定。从这里我们可以㆒窥各家编译器对标准 C++ 的支持程度。当然,随着版本的演进,这些状态都有可能改变。其㆗的状态 (3),(5),(6),(7),(8),(10),(11),各于 1.9 节㆗分别在 VC6, CB4, GCC ㆔家 编译器㆖测试过。 以㆘是 GNU C++ 2.91.57 的完整内容: G++ 2.91.57,cygnus\cygwin-b20\include\g++\stl_config.h 完整列表 #ifndef __STL_CONFIG_H # define __STL_CONFIG_H // // // // // // // // // // // 本檔所做的事情: (1) 如果编译器没有定义 bool, true, false,就定义它们 (2) 如果编译器的标准链接库未支持 drand48() 函式,就定义 __STL_NO_DRAND48 (3) 如果编译器无法处理 static members of template classes,就定义 __STL_STATIC_TEMPLATE_MEMBER_BUG (4) 如果编译器未支持关键词 typename,就将'typename' 定义为㆒个 null macro. (5) 如果编译器支持 partial specialization of class templates,就定义 __STL_CLASS_PARTIAL_SPECIALIZATION. (6) 如果编译器支持 partial ordering of function templates(亦称为 partial specialization of function templates),就定义 __STL_FUNCTION_TMPL_PARTIAL_ORDER The Annotated STL Sources 1.8 SGI STL 实作版本 21 // // // // // // // // // // // // // // // // // // // // // // // (7) 如果编译器允许我们在呼叫㆒个 function template 时可以明白指定其 template arguments,就定义 __STL_EXPLICIT_FUNCTION_TMPL_ARGS (8) 如果编译器支持 template members of classes,就定义 __STL_MEMBER_TEMPLATES. (9) 如果编译器不支持关键词 explicit,就定义'explicit' 为㆒个 null macro. (10) 如果编译器无法根据前㆒个 template parameters 设定㆘㆒个 template parameters 的默认值,就定义 __STL_LIMITED_DEFAULT_TEMPLATES (11) 如果编译器针对 non-type template parameters 执行 function template 的自变量推导(argument deduction)时有问题,就定义 __STL_NON_TYPE_TMPL_PARAM_BUG. (12) 如果编译器无法支持迭代器的 operator->,就定义 __SGI_STL_NO_ARROW_OPERATOR (13) 如果编译器(在你所选择的模式㆗)支持 exceptions,就定义 __STL_USE_EXCEPTIONS (14) 定义 __STL_USE_NAMESPACES 可使我们自动获得 using std::list; 之类的叙句 (15) 如果本链接库由 SGI 编译器来编译,而且使用者并未选择 pthreads 或其它 threads,就定义 __STL_SGI_THREADS. (16) 如果本链接库由㆒个 WIN32 编译器编译,并且在多绪模式㆘,就定义 __STL_WIN32THREADS (17) 适当㆞定义与 namespace 相关的 macros 如 __STD, __STL_BEGIN_NAMESPACE。 (18) 适当㆞定义 exception 相关的 macros 如 __STL_TRY, __STL_UNWIND。 (19) 根据 __STL_ASSERTIONS 是否定义,将 __stl_assert 定义为㆒个 测试动作或㆒个 null macro。 #ifdef _PTHREADS # define __STL_PTHREADS #endif # if defined(__sgi) && !defined(__GNUC__) // 使用 SGI STL 但却不是使用 GNU C++ # if !defined(_BOOL) # define __STL_NEED_BOOL # endif # if !defined(_TYPENAME_IS_KEYWORD) # define __STL_NEED_TYPENAME # endif # ifdef _PARTIAL_SPECIALIZATION_OF_CLASS_TEMPLATES # define __STL_CLASS_PARTIAL_SPECIALIZATION # endif # ifdef _MEMBER_TEMPLATES # define __STL_MEMBER_TEMPLATES # endif # if !defined(_EXPLICIT_IS_KEYWORD) # define __STL_NEED_EXPLICIT # endif # ifdef __EXCEPTIONS # define __STL_USE_EXCEPTIONS # endif # if (_COMPILER_VERSION >= 721) && defined(_NAMESPACES) The Annotated STL Sources 22 第 1 章 STL 概论与版本简介 # define __STL_USE_NAMESPACES # endif # if !defined(_NOTHREADS) && !defined(__STL_PTHREADS) # define __STL_SGI_THREADS # endif # endif # ifdef __GNUC__ # include <_G_config.h> # if __GNUC__ < 2 || (__GNUC__ == 2 && __GNUC_MINOR__ < 8) # define __STL_STATIC_TEMPLATE_MEMBER_BUG # define __STL_NEED_TYPENAME # define __STL_NEED_EXPLICIT # else // 这里可看出 GNUC 2.8+ 的能力 # define __STL_CLASS_PARTIAL_SPECIALIZATION # define __STL_FUNCTION_TMPL_PARTIAL_ORDER # define __STL_EXPLICIT_FUNCTION_TMPL_ARGS # define __STL_MEMBER_TEMPLATES # endif /* glibc pre 2.0 is very buggy. We have to disable thread for it. It should be upgraded to glibc 2.0 or later. */ # if !defined(_NOTHREADS) && __GLIBC__ >= 2 && defined(_G_USING_THUNKS) # define __STL_PTHREADS # endif # ifdef __EXCEPTIONS # define __STL_USE_EXCEPTIONS # endif # endif # if defined(__SUNPRO_CC) # define __STL_NEED_BOOL # define __STL_NEED_TYPENAME # define __STL_NEED_EXPLICIT # define __STL_USE_EXCEPTIONS # endif # if defined(__COMO__) # define __STL_MEMBER_TEMPLATES # define __STL_CLASS_PARTIAL_SPECIALIZATION # define __STL_USE_EXCEPTIONS # define __STL_USE_NAMESPACES # endif // 侯捷注:VC6 的版本号码是 1200 # if defined(_MSC_VER) # if _MSC_VER > 1000 # include // 此檔在 MSDEV\VC98\INCLUDE # else # define __STL_NEED_BOOL The Annotated STL Sources 1.8 SGI STL 实作版本 23 # endif # define __STL_NO_DRAND48 # define __STL_NEED_TYPENAME # if _MSC_VER < 1100 # define __STL_NEED_EXPLICIT # endif # define __STL_NON_TYPE_TMPL_PARAM_BUG # define __SGI_STL_NO_ARROW_OPERATOR # ifdef _CPPUNWIND # define __STL_USE_EXCEPTIONS # endif # ifdef _MT # define __STL_WIN32THREADS # endif # endif // 侯捷注:Inprise Borland C++builder 也定义有此常数。 // C++Builder 的表现岂有如㆘所示这般差劲? # if defined(__BORLANDC__) # define __STL_NO_DRAND48 # define __STL_NEED_TYPENAME # define __STL_LIMITED_DEFAULT_TEMPLATES # define __SGI_STL_NO_ARROW_OPERATOR # define __STL_NON_TYPE_TMPL_PARAM_BUG # ifdef _CPPUNWIND # define __STL_USE_EXCEPTIONS # endif # ifdef __MT__ # define __STL_WIN32THREADS # endif # endif # if defined(__STL_NEED_BOOL) typedef int bool; # define true 1 # define false 0 # endif # ifdef __STL_NEED_TYPENAME # define typename // 侯捷:难道不该 #define typename class 吗? # endif # ifdef __STL_NEED_EXPLICIT # define explicit # endif # ifdef __STL_EXPLICIT_FUNCTION_TMPL_ARGS # define __STL_NULL_TMPL_ARGS <> # else The Annotated STL Sources 24 第 1 章 STL 概论与版本简介 # define __STL_NULL_TMPL_ARGS # endif # ifdef __STL_CLASS_PARTIAL_SPECIALIZATION # define __STL_TEMPLATE_NULL template<> # else # define __STL_TEMPLATE_NULL # endif // __STL_NO_NAMESPACES is a hook so that users can disable namespaces // without having to edit library headers. # if defined(__STL_USE_NAMESPACES) && !defined(__STL_NO_NAMESPACES) # define __STD std # define __STL_BEGIN_NAMESPACE namespace std { # define __STL_END_NAMESPACE } # define __STL_USE_NAMESPACE_FOR_RELOPS # define __STL_BEGIN_RELOPS_NAMESPACE namespace std { # define __STL_END_RELOPS_NAMESPACE } # define __STD_RELOPS std # else # define __STD # define __STL_BEGIN_NAMESPACE # define __STL_END_NAMESPACE # undef __STL_USE_NAMESPACE_FOR_RELOPS # define __STL_BEGIN_RELOPS_NAMESPACE # define __STL_END_RELOPS_NAMESPACE # define __STD_RELOPS # endif # ifdef __STL_USE_EXCEPTIONS # define __STL_TRY try # define __STL_CATCH_ALL catch(...) # define __STL_RETHROW throw # define __STL_NOTHROW throw() # define __STL_UNWIND(action) catch(...) { action; throw; } # else # define __STL_TRY # define __STL_CATCH_ALL if (false) # define __STL_RETHROW # define __STL_NOTHROW # define __STL_UNWIND(action) # endif #ifdef __STL_ASSERTIONS # include # define __stl_assert(expr) \ if (!(expr)) { fprintf(stderr, "%s:%d STL assertion failure: %s\n", \ __FILE__, __LINE__, # expr); abort(); } // 侯捷注:以㆖使用 stringizing operator #,详见《多型与虚拟》第 4 章。 The Annotated STL Sources 1.8 SGI STL 实作版本 25 #else # define __stl_assert(expr) #endif #endif /* __STL_CONFIG_H */ // Local Variables: // mode:C++ // End: ㆘面这个小程序,用来测试 GCC 的常数设定: // file: 1config.cpp // test configurations defined in #include // which included , // and then #include using namespace std; int main() { # if defined(__sgi) cout << "__sgi" << endl; # endif # if defined(__GNUC__) cout << "__GNUC__" << endl; // none! // __GNUC__ cout << __GNUC__ << ' ' << __GNUC_MINOR__ << endl; // 2 91 // cout << __GLIBC__ << endl; // __GLIBC__ undeclared # endif // case 2 #ifdef __STL_NO_DRAND48 cout << "__STL_NO_DRAND48 defined" << endl; #else cout << "__STL_NO_DRAND48 undefined" << endl; #endif // case 3 #ifdef __STL_STATIC_TEMPLATE_MEMBER_BUG cout << "__STL_STATIC_TEMPLATE_MEMBER_BUG defined" << endl; #else cout << "__STL_STATIC_TEMPLATE_MEMBER_BUG undefined" << endl; #endif // case 5 #ifdef __STL_CLASS_PARTIAL_SPECIALIZATION cout << "__STL_CLASS_PARTIAL_SPECIALIZATION defined" << endl; #else The Annotated STL Sources 26 第 1 章 STL 概论与版本简介 cout << "__STL_CLASS_PARTIAL_SPECIALIZATION undefined" << endl; #endif // case 6 ...以㆘写法类似。详见档案 config.cpp(可自侯捷网站㆘载)。 } 执行结果如㆘,由此可窥见 GCC 对各种 C++ 特性的支持程度: __GNUC__ 2 91 __STL_NO_DRAND48 undefined __STL_STATIC_TEMPLATE_MEMBER_BUG undefined __STL_CLASS_PARTIAL_SPECIALIZATION defined __STL_FUNCTION_TMPL_PARTIAL_ORDER defined __STL_EXPLICIT_FUNCTION_TMPL_ARGS defined __STL_MEMBER_TEMPLATES defined __STL_LIMITED_DEFAULT_TEMPLATES undefined __STL_NON_TYPE_TMPL_PARAM_BUG undefined __SGI_STL_NO_ARROW_OPERATOR undefined __STL_USE_EXCEPTIONS defined __STL_USE_NAMESPACES undefined __STL_SGI_THREADS undefined __STL_WIN32THREADS undefined __STL_NO_NAMESPACES undefined __STL_NEED_TYPENAME undefined __STL_NEED_BOOL undefined __STL_NEED_EXPLICIT undefined __STL_ASSERTIONS undefined 1.9 可能令你困惑的 C++ 语法 1.8 节所列出的几个状态常数,用来区分编译器对 C++ Standard 的支持程度。这 几个状态,也正是许多程序员对于 C++ 语法最为困扰之所在。以㆘我便㆒㆒测试 GCC 在这几个状态㆖的表现。有些测试程序直接取材(并剪裁)自 SGI STL 源 码,因此你可以看到最贴近 SGI STL 真面貌的实例。由于这几个状态所关系的, 都是 template 自变量推导(argument deduction)、偏特化(partial specialization)之 类的问题,所以测试程序只需完成 classes 或 functions 的接口,便足以测试状态 是否成立。 本节所涵盖的内容属于 C++ 语言层次,不在本书范围之内。因此本节各范例程序 只做测试,不做太多说明。每个程序最前面都会有㆒个批注,告诉你在《C++ Primer》 The Annotated STL Sources 1.9 可能令你困惑的 C++ 语法 3/e 哪些章节有相关的语法介绍。 27 1.9.1 stl_config.h 中的各种组态( configurations) 以㆘所列组态编号与㆖㆒节所列的 档案起头的批注编号相同。 组态 3: __STL_STATIC_TEMPLATE_MEMBER_BUG // // // // // // file: 1config3.cpp 测试在 class template ㆗拥有 static data members. test __STL_STATIC_TEMPLATE_MEMBER_BUG, defined in ref. C++ Primer 3/e, p.839 vc6[o] cb4[x] gcc[o] cb4 does not support static data member initialization. #include using namespace std; template class testClass { public: // 纯粹为了方便测试,使用 public static int _data; }; // 为 static data members 进行定义(配置内存),并设初值。 int testClass::_data = 1; int testClass::_data = 2; int main() { // 以㆘,CB4 表现不佳,没有接受初值设定。 cout << testClass::_data << endl; // GCC, VC6:1 CB4:0 cout << testClass::_data << endl; // GCC, VC6:2 CB4:0 testClass obji1, obji2; testClass objc1, objc2; cout cout cout cout << << << << obji1._data obji2._data objc1._data objc2._data << << << << endl; endl; endl; endl; // // // // GCC, GCC, GCC, GCC, VC6:1 VC6:1 VC6:2 VC6:2 CB4:0 CB4:0 CB4:0 CB4:0 obji1._data = 3; objc2._data = 4; cout << obji1._data << endl; cout << obji2._data << endl; // GCC, VC6:3 CB4:3 // GCC, VC6:3 CB4:3 The Annotated STL Sources 28 cout << objc1._data << endl; cout << objc2._data << endl; 第 1 章 STL 概论与版本简介 // GCC, VC6:4 CB4:4 // GCC, VC6:4 CB4:4 } 组态 5: __STL_CLASS_PARTIAL_SPECIALIZATION. // // // // // // file: 1config5.cpp 测试 class template partial specialization — 在 class template 的 ㆒般化设计之外,特别针对某些 template 参数做特殊设计。 test __STL_CLASS_PARTIAL_SPECIALIZATION in ref. C++ Primer 3/e, p.860 vc6[x] cb4[o] gcc[o] #include using namespace std; // ㆒般化设计 template struct testClass { testClass() { cout << "I, O" << endl; } }; // 特殊化设计 template struct testClass { testClass() { cout << "T*, T*" << endl; } }; // 特殊化设计 template struct testClass { testClass() { cout << "const T*, T*" << endl; } }; int main() { testClass obj1; testClass obj2; testClass obj3; // I, O // T*, T* // const T*, T* } 组态 6: __STL_FUNCTION_TMPL_PARTIAL_ORDER 请 注 意 , 虽 然 檔 案 ㆗ 声 明 , 这 个 常 数 的 意 义 就 是 partial The Annotated STL Sources 1.9 可能令你困惑的 C++ 语法 29 specialization of function templates,但其实两者并不相同。前者意义如㆘所示,后 者的实际意义请参考 C++ 语法书籍。 // file: 1config6.cpp // test __STL_FUNCTION_TMPL_PARTIAL_ORDER in // vc6[x] cb4[o] gcc[o] #include using namespace std; class alloc { }; template class vector { public: void swap(vector&) { cout << "swap()" << endl; } }; #ifdef __STL_FUNCTION_TMPL_PARTIAL_ORDER // 只为说明。非本程序内容。 template inline void swap(vector& x, vector& y) { x.swap(y); } #endif // 只为说明。非本程序内容。 // 以㆖节录自 stl_vector.h,灰色部份系源码㆗的条件编译,非本测试程序内容。 int main() { vector x,y; swap(x, y); // swap() } 组态 7: __STL_EXPLICIT_FUNCTION_TMPL_ARGS 整个 SGI STL 内都没有用到此㆒常数定义。 状态 8: __STL_MEMBER_TEMPLATES // // // // // file: 1config8.cpp 测试 class template 之内可否再有 template (members). test __STL_MEMBER_TEMPLATES in ref. C++ Primer 3/e, p.844 vc6[o] cb4[o] gcc[o] #include The Annotated STL Sources 30 第 1 章 STL 概论与版本简介 using namespace std; class alloc { }; template class vector { public: typedef T value_type; typedef value_type* iterator; template void insert(iterator position, I first, I last) { cout << "insert()" << endl; } }; int main() { int ia[5] = {0,1,2,3,4}; vector x; vector::iterator ite; x.insert(ite, ia, ia+5); // insert() } 组态 10: __STL_LIMITED_DEFAULT_TEMPLATES // // // // // file: 1config10.cpp 测试 template 参数可否根据前㆒个 template 参数而设定默认值。 test __STL_LIMITED_DEFAULT_TEMPLATES in ref. C++ Primer 3/e, p.816 vc6[o] cb4[o] gcc[o] #include #include // for size_t using namespace std; class alloc { }; template class deque { public: deque() { cout << "deque" << endl; } }; // 根据前㆒个参数值 T,设定㆘㆒个参数 Sequence 的默认值为 deque The Annotated STL Sources 1.9 可能令你困惑的 C++ 语法 template > class stack { public: stack() { cout << "stack" << endl; } private: Sequence c; }; int main() { 31 stack x; // deque // stack } 组态 11: __STL_NON_TYPE_TMPL_PARAM_BUG // // // // // file: 1config11.cpp 测试 class template 可否拥有 non-type template 参数。 test __STL_NON_TYPE_TMPL_PARAM_BUG in ref. C++ Primer 3/e, p.825 vc6[o] cb4[o] gcc[o] #include #include // for size_t using namespace std; class alloc { }; inline size_t __deque_buf_size(size_t n, size_t sz) { return n != 0 ? n : (sz < 512 ? size_t(512 / sz) : size_t(1)); } template struct __deque_iterator { typedef __deque_iterator iterator; typedef __deque_iterator const_iterator; static size_t buffer_size() {return __deque_buf_size(BufSiz, sizeof(T)); } }; template class deque { public: // Iterators typedef __deque_iterator iterator; }; int main() The Annotated STL Sources 32 { 第 1 章 STL 概论与版本简介 cout << deque::iterator::buffer_size() << endl; // 128 cout << deque::iterator::buffer_size() << endl; // 64 } 以㆘组态常数虽不在前列编号之内,却也是 内的定义,并使用 于整个 SGI STL 之㆗。有认识的必要。 组态: __STL_NULL_TMPL_ARGS(bound friend template friend) 定义 __STL_NULL_TMPL_ARGS 如㆘: # ifdef __STL_EXPLICIT_FUNCTION_TMPL_ARGS # define __STL_NULL_TMPL_ARGS <> # else # define __STL_NULL_TMPL_ARGS # endif 这个组态常数常常出现在类似这样的场合(class template 的 friend 函式宣告): // in template > class stack { friend bool operator== __STL_NULL_TMPL_ARGS (const stack&, const stack&); friend bool operator< __STL_NULL_TMPL_ARGS (const stack&, const stack&); ... }; 展开后就变成了: template > class stack { friend bool operator== <> (const stack&, const stack&); friend bool operator< <> (const stack&, const stack&); ... }; 这种奇特的语法是为了实现所谓的 bound friend templates,也就是说 class template 的某个具现体(instantiation)与其 friend function template 的某个具现体有㆒对㆒ 的关系。㆘面是个测试程序: // // // // file: 1config-null-template-arguments.cpp test __STL_NULL_TMPL_ARGS in ref. C++ Primer 3/e, p.834: bound friend function template vc6[x] cb4[x] gcc[o] #include The Annotated STL Sources 1.9 可能令你困惑的 C++ 语法 33 #include // for size_t using namespace std; class alloc { }; template class deque { public: deque() { cout << "deque" << ' '; } }; // 以㆘宣告如果不出现,GCC 也可以通过。如果出现,GCC 也可以通过。这㆒点和 // C++ Primer 3/e p.834 的说法有出入。书㆖说㆒定要有这些前置宣告。 /* template class stack; template bool operator==(const stack& x, const stack& y); template bool operator<(const stack& x, const stack& y); */ template > class stack { // 写成这样是可以的 friend bool operator== (const stack&, const stack&); friend bool operator< (const stack&, const stack&); // 写成这样也是可以的 friend bool operator== (const stack&, const stack&); friend bool operator< (const stack&, const stack&); // 写成这样也是可以的 friend bool operator== <> (const stack&, const stack&); friend bool operator< <> (const stack&, const stack&); // 写成这样就不可以 // friend bool operator== (const stack&, const stack&); // friend bool operator< (const stack&, const stack&); public: stack() { cout << "stack" << endl; } private: Sequence c; }; template The Annotated STL Sources 34 第 1 章 STL 概论与版本简介 bool operator==(const stack& x, const stack& y) { return cout << "operator==" << '\t'; } template bool operator<(const stack& x, const stack& y) { return cout << "operator<" << '\t'; } int main() { stack x; stack y; // deque stack // deque stack cout << (x == y) << endl; cout << (x < y) << endl; stack y1; // operator== // operator< // deque stack 1 1 // cout << (x == y1) << endl; // error: no match for... // cout << (x < y1) << endl; // error: no match for... } 组态: __STL_TEMPLATE_NULL(class template explicit specialization) 定义了㆒个 __STL_TEMPLATE_NULL 如㆘: # ifdef __STL_CLASS_PARTIAL_SPECIALIZATION # define __STL_TEMPLATE_NULL template<> # else # define __STL_TEMPLATE_NULL # endif 这个组态常数常常出现在类似这样的场合: // in template struct __type_traits { ... }; __STL_TEMPLATE_NULL struct __type_traits { ... }; // in template struct hash { }; __STL_TEMPLATE_NULL struct hash { ... }; __STL_TEMPLATE_NULL struct hash { ... }; 展开后就变成了: template struct __type_traits { ... }; template<> struct __type_traits { ... }; The Annotated STL Sources 1.9 可能令你困惑的 C++ 语法 35 template struct hash { }; template<> struct hash { ... }; template<> struct hash { ... }; 这是所谓的 class template explicit specialization。㆘面这个例子适用于 GCC 和 VC6,允许使用者不指定 template<> 就完成 explicit specialization。C++Builder 则 是 非 常 严 格 ㆞ 要 求 必 须 完 全 遵 照 C++ 标 准 规 格 , 也 就 是 必 须 明 白 写 出 template<>。 // // // // // file: 1config-template-exp-special.cpp 以㆘测试 class template explicit specialization test __STL_TEMPLATE_NULL in ref. C++ Primer 3/e, p.858 vc6[o] cb4[x] gcc[o] #include using namespace std; // 将 __STL_TEMPLATE_NULL 定义为 template<>,可以。 // 若定义为 blank,如㆘,则只适用于 GCC. #define __STL_TEMPLATE_NULL /* blank */ template struct hash { void operator()() { cout << "hash" << endl; } }; // explicit specialization __STL_TEMPLATE_NULL struct hash { void operator()() { cout << "hash" << endl; } }; __STL_TEMPLATE_NULL struct hash { void operator()() { cout << "hash" << endl; } }; int main() { hash t1; hash t2; hasht3; t1(); t2(); t3(); // hash // hash // hash } The Annotated STL Sources 36 第 1 章 STL 概论与版本简介 1.9.2 暂时对象的产生与运用 所谓暂时对象,就是㆒种无名对象(unnamed objects)。它的出现如果不在程序员 的预期之㆘(例如任何 pass by value 动作都会引发 copy 动作,于是形成㆒个 暂时对象),往往造成效率㆖的负担13。但有时候刻意制造㆒些暂时对象,却又 是使程序干净清爽的技巧。刻意制造暂时对象的方法是,在型别名称之后直接加 ㆒对小括号,并可指定初值,例如 Shape(3,5) 或 int(8),其意义相当于唤起 相 应 的 constructor 且 不 指 定 物 件 名 称 。 STL 最 常 将 此 技 巧 应 用 于 仿 函 式 (functor)与算法的搭配㆖,例如: // file: 1config-temporary-object.cpp // 本例测试仿函式用于 for_each() 的情形 // vc6[o] cb4[o] gcc[o] #include #include #include using namespace std; template class print { public: void operator()(const T& elem) // operator() 多载化。见 1.9.6 节 { cout << elem << ' '; } }; int main() { int ia[6] = { 0,1,2,3,4,5 }; vector< int > iv(ia, ia+6); // print() 是㆒个暂时对象,不是㆒个函式呼叫动作。 for_each(iv.begin(), iv.end(), print()); } 最后㆒行便是产生「function template 具现体」print 的㆒个暂时对象。这 个对象将被传入 for_each() 之㆗起作用。当 for_each() 结束,这个暂时对象 也就结束了它的生命。 13 请参考《More Effective C++》条款 19: Understand the origin of temporary objects. The Annotated STL Sources 1.9 可能令你困惑的 C++ 语法 37 1.9.3 静态常数整数成员在 class 内部直接初始化 in-class static constant integer initialization 如果 class 内含 const static integral data member,那么根据 C++ 标准规格,我们 可以在 class 之内直接给予初值。所谓 integral 泛指所有整数型别,不单只是指 int。㆘面是个例子: // // // // file: 1config-inclass-init.cpp test in-class initialization of static const integral members ref. C++ Primer 3/e, p.643 vc6[x] cb4[o] gcc[o] #include using namespace std; template class testClass { public: // expedient static const int _datai = 5; static const long _datal = 3L; static const char _datac = 'c'; }; int main() { cout << testClass::_datai << endl; // 5 cout << testClass::_datal << endl; // 3 cout << testClass::_datac << endl; // c } 1.9.4 increment/decrement/dereference 运算子 increment/dereference 运算子在迭代器的实作㆖占有非常重要的㆞位,因为任何 ㆒ 个 迭 代 器 都 必 须 实 作 出 前 进 ( increment, operator++ ) 和 取 值 ( dereference, operator*)功能,前者还分为前置式(prefix)和后置式(postfix)两种,有非 常规律的写法14。有些迭代器具备双向移动功能,那么就必须再提供 decrement 运算子(也分前置式和后置式两种)。㆘面是个范例: 14 请参考《More Effective C++》条款 6:Distinguish between prefix and postfix forms of increment and decrement operators The Annotated STL Sources 38 第 1 章 STL 概论与版本简介 // file: 1config-operator-overloading.cpp // vc6[x] cb4[o] gcc[o] // vc6 的 friend 机制搭配 C++ 标准链接库,有臭虫。 #include using namespace std; class INT { friend ostream& operator<<(ostream& os, const INT& i); public: INT(int i) : m_i(i) { }; // prefix : increment and then fetch INT& operator++() { ++(this->m_i); // 随着 class 的不同,此行应该有不同的动作。 return *this; } // postfix : fetch and then increment const INT operator++(int) { INT temp = *this; ++(*this); return temp; } // prefix : decrement and then fetch INT& operator--() { --(this->m_i); // 随着 class 的不同,此行应该有不同的动作。 return *this; } // postfix : fetch and then decrement const INT operator--(int) { INT temp = *this; --(*this); return temp; } // dereference int& operator*() const { return (int&)m_i; // 以㆖转换动作告诉编译器,你确实要将 const int 转为 non-const lvalue. // 如果没有这样明白㆞转型,有些编译器会给你警告,有些更严格的编译器会视为错误 The Annotated STL Sources 1.9 可能令你困惑的 C++ 语法 } private: int m_i; }; ostream& operator<<(ostream& os, const INT& i) { os << '[' << i.m_i << ']'; return os; } int main() { INT I(5); 39 cout << I++; cout << ++I; cout << I--; cout << --I; cout << *I; // // // // // [5] [7] [7] [5] 5 } 1.9.5 前闭后开区间表示法 [ ) 任何㆒个 STL 算法,都需要获得由㆒对迭代器(泛型指标)所标示的区间,用 以表示操作范围。这㆒对迭代器所标示的是个所谓的前闭后开区间15,以 [first, last) 表示。也就是说,整个实际范围从 first 开始,直到 last-1。迭代器 last 所指的是「最后㆒个元素的㆘㆒位置」。这种 off by one(偏移㆒格,或说 pass the end)的标示法,带来许多方便,例如㆘面两个 STL 算法的循环设计,就显得干 净利落: template InputIterator find(InputIterator first, InputIterator last, const T& value) { while (first != last && *first != value) ++first; return first; } template Function for_each(InputIterator first, InputIterator last, Function f) { for ( ; first != last; ++first) f(*first); return f; } 15 这是㆒种半开(half-open)、后开(open-ended)区间。 The Annotated STL Sources 40 第 1 章 STL 概论与版本简介 前闭后开区间图示如㆘(注意,元素之间无需占用连续内存空间): first iterator last iterator 1.9.6 function call 运算子(operator()) 很少㆟注意到,函式呼叫动作(C++ 语法㆗的左右小括号)也可以被多载化。 许多 STL 算法都提供两个版本,㆒个用于㆒般状况(例如排序时以递增方式排 列),㆒个用于特殊状况(例如排序时由使用者指定以何种特殊关系进行排列)。 像这种情况,需要使用者指定某个条件或某个策略,而条件或策略的背后由㆒整 组动作构成,便需要某种特殊的东西来代表这「㆒整组动作」。 代表「㆒整组动作」的,当然是函式。过去 C 语言时代,欲将函式当做参数传递, 唯有透过函式指标(pointer to function,或称 function pointer)才能达成,例如: // file: 1qsort.cpp #include #include using namespace std; int fcmp( const void* elem1, const void* elem2); void main() { int ia[10] = {32,92,67,58,10,4,25,52,59,54}; for(int i = 0; i < 10; i++) cout << ia[i] << " "; // 32 92 67 58 10 4 25 52 59 54 qsort(ia,sizeof(ia)/sizeof(int),sizeof(int), fcmp); for(int i = 0; i < 10; i++) cout << ia[i] << " "; // 4 10 25 32 52 54 58 59 67 92 } The Annotated STL Sources 1.9 可能令你困惑的 C++ 语法 41 int fcmp( const void* elem1, const void* elem2) { const int* i1 = (const int*)elem1; const int* i2 = (const int*)elem2; if( *i1 < *i2) return -1; else if( *i1 == *i2) return 0; else if( *i1 > *i2) return 1; } 但是函式指标有缺点,最重要的是它无法持有自己的状态(所谓区域状态,local states),也无法达到组件技术㆗的可配接性(adaptability)— 也就是无法再将某 些修饰条件加诸于其㆖而改变其状态。 为此,STL 算法的特殊版本所接受的所谓「条件」或「策略」或「㆒整组动作」, 都以仿函式形式呈现。所谓仿函式(functor)就是使用起来像函式㆒样的东西。 如果你针对某个 class 进行 operator() 多载化,它就成为㆒个仿函式。至于要 成为㆒个可配接的仿函式,还需要㆒些额外的努力(详见第 8 章)。 ㆘面是㆒个将 operator() 多载化的例子: // file: 1functor.cpp #include using namespace std; // 由于将 operator() 多载化了,因此 plus 成了㆒个仿函式 template struct plus { T operator()(const T& x, const T& y) const { return x + y; } }; // 由于将 operator() 多载化了,因此 minus 成了㆒个仿函式 template struct minus { T operator()(const T& x, const T& y) const { return x - y; } }; int main() { // 以㆘产生仿函式对象。 plus plusobj; The Annotated STL Sources 42 minus minusobj; // 以㆘使用仿函式,就像使用㆒般函式㆒样。 第 1 章 STL 概论与版本简介 cout << plusobj(3,5) << endl; cout << minusobj(3,5) << endl; // 8 // -2 // 以㆘直接产生仿函式的暂时对象(第㆒对小括号),并呼叫之(第㆓对小括号)。 cout << plus()(43,50) << endl; cout << minus()(43,50) << endl; // 93 // -7 } ㆖述的 plus 和 minus 已经非常接近 STL 的实作了,唯㆒差别在于它缺 乏「可配接能力」。关于「可配接能力」,将在第 8 章详述。 The Annotated STL Sources 2.1 空间配置器的标准接口 43 2 空间配置器 allocator 以 STL 的运用角度而言,空间配置器是最不需要介绍的东西,它总是隐藏在㆒切 组件(更具体㆞说是指容器,container)的背后,默默工作默默付出。但若以 STL 的实作角度而言,第㆒个需要介绍的就是空间配置器,因为整个 STL 的操作对象 (所有的数值)都存放在容器之内,而容器㆒定需要配置空间以置放数据。不先 掌握空间配置器的原理,难免在观察其它 STL 组件的实作时处处遇到挡路石。 为什么不说 allocator 是内存配置器而说它是空间配置器呢?因为,空间不㆒定 是内存,空间也可以是磁盘或其它辅助储存媒体。是的,你可以写㆒个 allocator, 直接向硬盘取空间1。以㆘介绍的是 SGI STL 提供的配置器,配置的对象,呃, 是的,是内存 。 2.1 空间配置器的标准接口 根据 STL 的规范,以㆘是 allocator 的必要界面2: // 以㆘各种 type 的设计原由,第㆔章详述。 allocator::value_type allocator::pointer allocator::const_pointer allocator::reference allocator::const_reference allocator::size_type allocator::difference_type 1 2 请参考 Disk-Based Container Objects, by Tom Nelson, C/C++ Users Journal, 1998/04 请参考 [Austern98], 10.3 节。 The Annotated STL Sources 44 第 2 章 空间配置器(allocator) allocator::rebind ㆒个巢状的(nested)class template。class rebind 拥有唯㆒成员 other, 那是㆒个 typedef,代表 allocator。 allocator::allocator() default constructor。 allocator::allocator(const allocator&) copy constructor。 template allocator::allocator(const allocator&) 泛化的 copy constructor。 allocator::~allocator() default constructor。 pointer allocator::address(reference x) const 传回某个对象的地址。算式 a.address(x) 等同于 &x。 const_pointer allocator::address(const_reference x) const 传回某个 const 对象的地址。算式 a.address(x) 等同于 &x。 pointer allocator::allocate(size_type n, cosnt void* = 0) 配置空间,足以储存 n 个 T 对象。第㆓自变量是个提示。实作㆖可能会利用它来 增进区域性(locality),或完全忽略之。 void allocator::deallocate(pointer p, size_type n) 归还先前配置的空间。 size_type allocator::max_size() const 传回可成功配置的最大量。 void allocator::construct(pointer p, const T& x) 等同于 new(const void*) p) T(x)。 void allocator::destroy(pointer p) 等同于 p->~T()。 2.1.1 设计一个阳春的空间配置器,JJ::allocator 根据 前述的标准介 面,我们 可以自行完成㆒个 功能阳春、介 面 不怎么齐全的 allocator 如㆘: // file: 2jjalloc.h #ifndef _JJALLOC_ #define _JJALLOC_ The Annotated STL Sources 2.1 空间配置器的标准接口 45 #include #include #include #include #include // // // // // for for for for for placement new. ptrdiff_t, size_t exit() UINT_MAX cerr namespace JJ { template inline T* _allocate(ptrdiff_t size, T*) { set_new_handler(0); T* tmp = (T*)(::operator new((size_t)(size * sizeof(T)))); if (tmp == 0) { cerr << "out of memory" << endl; exit(1); } return tmp; } template inline void _deallocate(T* buffer) { ::operator delete(buffer); } template inline void _construct(T1* p, const T2& value) { new(p) T1(value); // placement new. invoke ctor of T1. } template inline void _destroy(T* ptr) { ptr->~T(); } template class allocator { public: typedef T typedef T* typedef const T* typedef T& typedef const T& typedef size_t typedef ptrdiff_t value_type; pointer; const_pointer; reference; const_reference; size_type; difference_type; // rebind allocator of type U template The Annotated STL Sources 46 第 2 章 空间配置器(allocator) struct rebind { typedef allocator other; }; // hint used for locality. ref.[Austern],p189 pointer allocate(size_type n, const void* hint=0) { return _allocate((difference_type)n, (pointer)0); } void deallocate(pointer p, size_type n) { _deallocate(p); } void construct(pointer p, const T& value) { _construct(p, value); } void destroy(pointer p) { _destroy(p); } pointer address(reference x) { return (pointer)&x; } const_pointer const_address(const_reference x) { return (const_pointer)&x; } size_type max_size() const { return size_type(UINT_MAX/sizeof(T)); } }; } // end of namespace JJ #endif // _JJALLOC_ 将 JJ::allocator 应用于程序之㆗,我们发现,它只能有限度㆞搭配 PJ STL 和 RW STL,例如: // file: 2jjalloc.cpp // VC6[o], BCB4[o], GCC2.9[x]. #include "jjalloc.h" #include #include using namespace std; int main() { int ia[5] = {0,1,2,3,4}; unsigned int i; vector > iv(ia, ia+5); The Annotated STL Sources 2.2 具备次配置力(sub-allocation)的 SGI 空间配置器 47 for(i=0; i > iv; 必须这么写: vector iv; // in VC or CB // in GCC SGI STL allocator 未能符合标准规格,这个事实通常不会对我们带来困扰,因为 通常我们使用预设的空间配置器,很少需要自行指定配置器名称,而 SGI STL 的 每㆒个容器都已经指定其预设的空间配置器为 alloc。例如㆘面的 vector 宣告: template // 预设使用 alloc 为配置器 class vector { ... }; 2.2.1 SGI 标准的空间配置器,std::allocator 虽然 SGI 也定义有㆒个符合部份标准、名为 allocator 的配置器,但 SGI 自己 The Annotated STL Sources 48 第 2 章 空间配置器(allocator) 从未用过它,也不建议我们使用。主要原因是效率不彰,只把 C++ 的 ::operator new 和 ::operator delete 做 ㆒ 层 薄 薄 的 包 装 而 已 。 ㆘ 面 是 SGI 的 std:: allocator 全貌: G++ 2.91.57,cygnus\cygwin-b20\include\g++\defalloc.h 完整列表 // 我们不赞成含入此档。这是原始的 HP default allocator。提供它只是为了 // 回溯相容。 // // DO NOT USE THIS FILE 不要使用这个档案,除非你手㆖的容器是以旧式作法 // 完成 — 那就需要㆒个拥有 HP-style interface 的空间配置器。SGI STL 使用 // 不同的 allocator 界面。SGI-style allocators 不带有任何与对象型别相关 // 的参数;它们只回应 void* 指标(侯捷注:如果是标准接口,就会响应㆒个 // 「指向对象型别」的指针,T*)。此档并不含入于其它任何 SGI STL 表头档。 #ifndef DEFALLOC_H #define DEFALLOC_H #include #include #include #include #include #include template inline T* allocate(ptrdiff_t size, T*) { set_new_handler(0); T* tmp = (T*)(::operator new((size_t)(size * sizeof(T)))); if (tmp == 0) { cerr << "out of memory" << endl; exit(1); } return tmp; } template inline void deallocate(T* buffer) { ::operator delete(buffer); } template class allocator { public: // 以㆘各种 type 的设计原由,第㆔章详述。 typedef T value_type; typedef T* pointer; typedef const T* const_pointer; typedef T& reference; The Annotated STL Sources 2.2 具备次配置力(sub-allocation)的 SGI 空间配置器 49 typedef const T& const_reference; typedef size_t size_type; typedef ptrdiff_t difference_type; pointer allocate(size_type n) return ::allocate((difference_type)n, (pointer)0); } void deallocate(pointer p) { ::deallocate(p); } pointer address(reference x) { return (pointer)&x; } const_pointer const_address(const_reference x) return (const_pointer)&x; } size_type init_page_size() return max(size_type(1), size_type(4096/sizeof(T))); } size_type max_size() const return max(size_type(1), size_type(UINT_MAX/sizeof(T))); } }; // 特化版本(specialization)。注意,为什么最前面不需加㆖ template<>? // 见 1.9.1 节的组态测试。注意,只适用于 GCC。 class allocator public: typedef void* pointer; }; #endif 2.2.2 SGI 特殊的空间配置器,std::alloc ㆖㆒节所说的 allocator 只是基层内存配置/解放行为(也就是 ::operator new 和 ::operator delete)的㆒层薄薄包装,并没有考虑到任何效率㆖的强化。 SGI 另有法宝供本身内部使用。 ㆒般而言,我们所习惯的 C++ 内存配置动作和释放动作是这样: class Foo { ... }; Foo* pf = new Foo; delete pf; // 配置内存,然后建构对象 // 将对象解构,然后释放内存 这其㆗的 new 算式内含两阶段动作3:(1) 呼叫 ::operatornew 配置内存,(2) 呼 叫 Foo::Foo() 建 构 物 件 内 容 。delete 算 式 也 内 含 两 阶 段 动 作 : (1) 呼 叫 3 详见《多型与虚拟》2/e 第 1,3 章。 The Annotated STL Sources 50 第 2 章 空间配置器(allocator) Foo::~Foo() 将对象解构,(2) 呼叫 ::operator delete 释放内存。 为了精密分工,STL allocator 决定将这两阶段动作区分开来。内存配置动作 由 alloc:allocate() 负责,内存释放动作由 alloc::deallocate() 负责; 对象建构动作由 ::construct() 负责,对象解构动作由 ::destroy() 负责。 STL 标准规格告诉我们,配置器定义于 之㆗,SGI 内含以 ㆘两个档案: #include #include // 负责内存空间的配置与释放 // 负责对象内容的建构与解构 内存空间的配置/释放与对象内容的建构/解构,分别着落在这两个档案身㆖。其 ㆗ 定义有两个基本函式:建构用的 construct() 和解构用 的 destroy()。㆒头栽进复杂的内存动态配置与释放之前,让我们先看清楚这 两个函式如何完成对象的建构和解构。 STL 规定 配置器(allocator) 定义于此 这里定义有全域函式 construct() 和 destroy(), 负责物件的建构和解构。 它们隶属于 STL 标准规范。 这里定义有㆒、㆓级配置器, 彼此合作。配置器名为 alloc。 这里定义有㆒些全域函式,用来充填(fill) 或复制(copy)大块内存内容,它们也都 隶属于 STL 标准规范: un_initialized_copy() un_initialized_fill() un_initialized_fill_n() 这些函式虽不属于配置器的范畴,但与对象初值 设定有关,对于容器的大规模元素初值设定很有 帮助。这些函式对于效率都有面面俱到的考虑, 最差情况㆘会呼叫construct(), 最佳情况则使用C标准函式memmove() 直接进行 内存内容搬移。 The Annotated STL Sources 2.2 具备次配置力(sub-allocation)的 SGI 空间配置器 51 2.2.3 建构和解构基本工具:construct() 和 destroy() ㆘面是 的部份内容(阅读程序代码的同时,请参考图 2-1): #include // 欲使用 placement new,需先含入此檔 template inline void construct(T1* p, const T2& value) { new (p) T1(value); // placement new; 唤起 T1::T1(value); } // 以㆘是 destroy() 第㆒版本,接受㆒个指标。 template inline void destroy(T* pointer) { pointer->~T(); // 唤起 dtor ~T() } // 以㆘是 destroy() 第㆓版本,接受两个迭代器。此函式设法找出元素的数值型别, // 进而利用 __type_traits<> 求取最适当措施。 template inline void destroy(ForwardIterator first, ForwardIterator last) { __destroy(first, last, value_type(first)); } // 判断元素的数值型别(value type)是否有 trivial destructor template inline void __destroy(ForwardIterator first, ForwardIterator last, T*) { typedef typename __type_traits::has_trivial_destructor trivial_destructor; __destroy_aux(first, last, trivial_destructor()); } // 如果元素的数值型别(value type)有 non-trivial destructor… template inline void __destroy_aux(ForwardIterator first, ForwardIterator last, __false_type) { for ( ; first < last; ++first) destroy(&*first); } // 如果元素的数值型别(value type)有 trivial destructor… template inline void __destroy_aux(ForwardIterator, ForwardIterator, __true_type) {} // 以㆘是 destroy() 第㆓版本针对迭代器为 char* 和 wchar_t* 的特化版 inline void destroy(char*, char*) {} inline void destroy(wchar_t*, wchar_t*) {} The Annotated STL Sources __destroy() destroy(&*first); 52 trivial destructor ? 第 2 章 空间配置器(allocator) 泛化 for(;first~T(); (T* pointer) construct() 泛化 new(p) T1(value); (T1* p, const T2& value) 图 2-1 construct() 和 drestroy() 示意。 这两个做为建构、解构之用的函式被设计为全域函式,符合 STL 的规范4。此外 STL 还规定配置器必须拥有名为construct() 和 destroy() 的两个成员函式(见 2.1 节),然而真正在 SGI STL ㆗大显身手的那个名为 std::alloc 的配置器并未 遵守此㆒规则(稍后可见)。 ㆖述 construct() 接受㆒个指标 p 和㆒个初值 value,此函式的用途就是将初 值设定到指标所指的空间㆖。C++ 的 placement new 运算子5 可用来完成此㆒ 任务。 destroy() 有两个版本,第㆒版本接受㆒个指标,准备将该指标所指之物解构掉。 4 5 请参考 [Austern98] 10.4.1 节。 请参考 [Lippman98] 8.4.5 节。 The Annotated STL Sources 2.2 具备次配置力(sub-allocation)的 SGI 空间配置器 53 这很简单,直接呼叫该对象的解构式即可。第㆓版本接受 first 和 last 两个迭 代器(所谓迭代器,第㆔章有详细介绍),准备将 [first,last) 范围内的所有 物件解构掉。我们不知道这个范围有多大,万㆒很大,而每个物件的解构式都无 关痛痒(所谓 trivial destructor),那么㆒次次呼叫这些无关痛痒的解构式,对效 率是㆒种蕲伤。因此,这里首先利用 value_type() 获得迭代器所指物件的型别, 再 利 用 __type_traits 判 别 该 型 别 的 解 构 式 是 否 无 关 痛 痒 。 若 是 (__true_type),什么也不做就结束;若否(__false_type),这才以循环方 式巡访整个范围,并在循环㆗每经历㆒个对象就呼叫第㆒个版本的 destroy()。 这样的观念很好,但 C++ 本身并不直接支持对「指标所指之物」的型别判断,也 不支持对「对象解构式是否为 trivial」的判断,因此,㆖述的 value_type() 和 __type_traits<> 该如何实作呢?3.7 节有详细介绍。 2.2.4 空间的配置与释放,std::alloc 看完了内存配置后的对象建构行为,和内存释放前的对象解构行为,现在我 们来看看内存的配置和释放。 对象建构前的空间配置,和对象解构后的空间释放,由 负责,SGI 对此的设计哲学如㆘: 向 system heap 要求空间。 考虑多绪(multi-threads)状态。 考虑内存不足时的应变措施。 考虑过多「小型区块」可能造成的内存破碎(fragment)问题。 为了将问题控制在㆒定的复杂度内,以㆘的讨论以及所摘录的源码,皆排除多绪 状态的处理。 C++ 的 记 忆 体 配 置 基 本 动 作 是 ::operator new() , 记 忆 体 释 放 基 本 动 作 是 ::operator delete()。这两个全域函式相当于 C 的 malloc() 和 free() 函 式。是的,正是如此,SGI 正是以 malloc() 和 free() 完成内存的配置与释 放。 The Annotated STL Sources 54 第 2 章 空间配置器(allocator) 考虑小型区块所可能造成的内存破碎问题,SGI 设计了双层级配置器,第㆒级 配置器直接使用 malloc() 和 free(),第㆓级配置器则视情况采用不同的策略: 当配置区块超过 128bytes,视之为「足够大」,便呼叫第㆒级配置器;当配置区 块小于 128bytes,视之为「过小」,为了降低额外负担(overhead,见 2.2.6 节), 便采用复杂的 memory pool 整理方式,而不再求助于第㆒级配置器。整个设计究 竟只开放第㆒级配置器,或是同时开放第㆓级配置器,取决于 __USE_MALLOC6 是 否被定义(唔,我们可以轻易测试出来,SGI STL 并未定义 __USE_MALLOC): # ifdef __USE_MALLOC ... typedef __malloc_alloc_template<0> malloc_alloc; typedef malloc_alloc alloc; // 令 alloc 为第㆒级配置器 # else ... // 令 alloc 为第㆓级配置器 typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS, 0> alloc; #endif /* ! __USE_MALLOC */ 其 ㆗ __malloc_alloc_template 就 是 第 ㆒ 级 配 置 器 , __default_alloc_ template 就是第㆓级配置器。稍后分别有详细介绍。再次提醒你注意,alloc 并 不接受任何 template 型别参数。 无论 alloc 被定义为第㆒级或第㆓级配置器,SGI 还为它再包装㆒个接口如㆘, 使配置器的接口能够符合 STL 规格: template class simple_alloc { public: static T *allocate(size_t n) { return 0 == n? 0 : (T*) Alloc::allocate(n * sizeof (T)); } static T *allocate(void) { return (T*) Alloc::allocate(sizeof (T)); } static void deallocate(T *p, size_t n) { if (0 != n) Alloc::deallocate(p, n * sizeof (T)); } static void deallocate(T *p) { Alloc::deallocate(p, sizeof (T)); } }; 其内部㆕个成员函式其实都是单纯的转呼叫,呼叫传入之配置器(可能是第㆒级 6 __USE_MALLOC 这个名称取得不甚理想,因为无论如何,最终总是使用 malloc(). The Annotated STL Sources template 1.1. allocate( )直直接使用malloc( ),, 接使用 malloc( ) 2.2. 模拟C++ 的 set_new_handler( ) 以处理 set_new_handler( 记内存不足的状况 template 其其㆗: 1.1.维维护1616个个自由串行(free lists)), 自由串行( free lists , 负负责1616种种小型区块的次配置能力。 如如果内存不足,转呼叫第㆒级配置器 ((那儿有处理程序)。 第第㆒级配置器。 ) 2.2 具备次配置力(sub-allocation)的 SGI 空间配置器 55 也可能是第㆓级)的成员函式。这个接口使配置器的配置单位从 bytes 转为个别 元素的大小(sizeof(T))。SGI STL 容器全都使用这个 simple_alloc 接口,例 如: template // 预设使用 alloc 为配置器 class vector { protected: // 专属之空间配置器,每次配置㆒个元素大小 typedef simple_alloc data_allocator; void deallocate() { if (...) data_allocator::deallocate(start, end_of_storage - start); } ... }; ㆒、㆓级配置器的关系,接口包装,及实际运用方式,可于图 2-2 略见端倪。 SGI STL 第㆒级配置器 template class __malloc_alloc_template {{… };}; 其其㆗㆗:: allocate( ) deallocate() 直接使用free()。 处理 忆体不足的状况 SGI STL 第㆓级配置器 template class __default_alloc_template {{… };}; ㆗: 护 责 小型区块的次配置能力。 记忆池(memory pool)以 malloc( ) 配置而得。 果内存不足,转呼叫第㆒级配置器 那儿有处理程序)。 2.2. 如果需求区块大于 128bytes,就转呼叫 ㆒级配置器。 图 2-2a 第㆒级配置器与第㆓级配置器 The Annotated STL Sources typedef __malloc_alloc_template<0> malloc_alloc; public: static void deallocate(T*); typedef simple_alloc data_alloctor; data_alloctor::allocate(n); ////配配置nn个个元素 data_alloctor::allocate(n); ////配配置完成后,接㆘来必须有初值设定动作...... 又又例: typedef simple_alloc data_alloctor; typedef simple_alloc map_allocator; data_allocator::allocate(n); ////配配置nn个个元素 data_allocator::allocate(n); map_allocator::allocate(n); ////配配置nn个个节点 map_allocator::allocate(n); ////配配置完成后,接㆘来必须有初值设定动作...... , 56 第 2 章 空间配置器(allocator) __USE_MALLOC ? no yes 将 alloc 定义为 第㆒级配置器 typedef __malloc_alloc_template<0> malloc_alloc; typedef malloc_alloc alloc; ; 将 alloc 定义为 第㆓级配置器 typedef __default_alloc_template<0,0> alloc; ; 多㆒层包装,使Alloc具备标准接口 template class simple_alloc{{ public: static TT*allocate(size_t); static TT*allocate(void); static void deallocate(T*, size_t);); static void deallocate(T*); };}; 实实际际运运用用方方式式,,例例:: template > class vector {{ typedef simple_alloc data_alloctor; 置 元素 置完成后,接㆘来必须有初值设定动作 };}; 例: template class deque{{ typedef simple_alloc data_alloctor; typedef simple_alloc map_allocator; 置 元素 置 节点 置完成后,接㆘来必须有初值设定动作 };}; 图 2-2b 第㆒级配置器与第㆓级配置器,其包装接口和运用方式 2.2.5 第一级配置器 __malloc_alloc_template 剖析 首先我们观察第㆒级配置器: #if 0 # include # define __THROW_BAD_ALLOC throw bad_alloc #elif !defined(__THROW_BAD_ALLOC) # include # define __THROW_BAD_ALLOC cerr << "out of memory" << endl; exit(1) #endif // malloc-based allocator. 通常比稍后介绍的 default alloc 速度慢, The Annotated STL Sources 2.2 具备次配置力(sub-allocation)的 SGI 空间配置器 57 // ㆒般而言是 thread-safe,并且对于空间的运用比较高效(efficient)。 // 以㆘是第㆒级配置器。 // 注意,无「template 型别参数」。至于「非型别参数」inst,完全没派㆖用场。 template class __malloc_alloc_template { private: // 以㆘都是函式指标,所代表的函式将用来处理内存不足的情况。 // oom : out of memory. static void *oom_malloc(size_t); static void *oom_realloc(void *, size_t); static void (* __malloc_alloc_oom_handler)(); public: static void * allocate(size_t n) { void *result = malloc(n); // 第㆒级配置器直接使用 malloc() // 以㆘,无法满足需求时,改用 oom_malloc() if (0 == result) result = oom_malloc(n); return result; } static void deallocate(void *p, size_t /* n */) { free(p); // 第㆒级配置器直接使用 free() } static void * reallocate(void *p, size_t /* old_sz */, size_t new_sz) { void * result = realloc(p, new_sz); // 第㆒级配置器直接使用 realloc() // 以㆘,无法满足需求时,改用 oom_realloc() if (0 == result) result = oom_realloc(p, new_sz); return result; } // 以㆘模拟 C++ 的 set_new_handler(). 换句话说,你可以透过它, // 指定你自己的 out-of-memory handler static void (* set_malloc_handler(void (*f)()))() { void (* old)() = __malloc_alloc_oom_handler; __malloc_alloc_oom_handler = f; return(old); } }; // malloc_alloc out-of-memory handling // 初值为 0。有待客端设定。 template The Annotated STL Sources 58 第 2 章 空间配置器(allocator) void (* __malloc_alloc_template::__malloc_alloc_oom_handler)() = 0; template void * __malloc_alloc_template::oom_malloc(size_t n) { void (* my_malloc_handler)(); void *result; for (;;) { // 不断尝试释放、配置、再释放、再配置… my_malloc_handler = __malloc_alloc_oom_handler; if (0 == my_malloc_handler) { __THROW_BAD_ALLOC; } (*my_malloc_handler)(); result = malloc(n); // 呼叫处理例程,企图释放内存。 // 再次尝试配置内存。 if (result) return(result); } } template void * __malloc_alloc_template::oom_realloc(void *p, size_t n) { void (* my_malloc_handler)(); void *result; for (;;) { // 不断尝试释放、配置、再释放、再配置… my_malloc_handler = __malloc_alloc_oom_handler; if (0 == my_malloc_handler) { __THROW_BAD_ALLOC; } (*my_malloc_handler)(); result = realloc(p, n); // 呼叫处理例程,企图释放内存。 // 再次尝试配置内存。 if (result) return(result); } } // 注意,以㆘直接将参数 inst 指定为 0。 typedef __malloc_alloc_template<0> malloc_alloc; 第㆒级配置器以 malloc(), free(), realloc() 等 C 函式执行实际的内存 配置、释放、重配置动作,并实作出类似 C++ new-handler7 的机制。是的,它不 能直接运用 C++ new-handler 机制,因为它并非使用 ::operator new 来配置记 忆体。 所谓 C++ new handler 机制是,你可以要求系统在内存配置需求无法被满足时, 唤起㆒个你所指定的函式。换句话说㆒旦 ::operator new 无法达成任务,在丢 7 详见《Effective C++》2e, 条款 7:Be prepared for out-of-memory conditions. The Annotated STL Sources 2.2 具备次配置力(sub-allocation)的 SGI 空间配置器 59 出 std::bad_alloc 异常状态之前,会先呼叫由客端指定的处理例程。此处理例程 通常即被称为 new-handler。new-handler 解决内存不足的作法有特定的模式, 请参考《Effective C++》2e 条款 7。 注意,SGI 以 malloc 而非 ::operator new 来配置内存(我所能够想象的㆒ 个原因是历史因素,另㆒个原因是 C++ 并未提供相应于 realloc() 的内存配 置动作),因此 SGI 不能直接使用 C++ 的 set_new_handler(),必须模拟㆒个 类似的 set_malloc_handler()。 请注意,SGI 第㆒级配置器的 allocate() 和 realloc() 都是在呼叫 malloc() 和 realloc() 不成功后,改呼叫 oom_malloc() 和 oom_realloc()。后两者都 有内循环,不断呼叫「内存不足处理例程」,期望在某次呼叫之后,获得足够 的内存而圆满达成任务。但如果「内存不 足处理例程」并未被客端设定, oom_malloc() 和 oom_realloc() 便老实不客气㆞呼叫 __THROW_BAD_ALLOC, 丢出 bad_alloc 异常讯息,或利用 exit(1) 硬生生㆗止程序。 记住,设计「内存不足处理例程」是客端的责任,设定「内存不足处理例程」 也是客端的责任。再㆒次提醒你,「内存不足处理例程」解决问题的作法有着 特定的模式,请参考 [Meyers98] 条款 7。 2.2.6 第二级配置器 __default_alloc_template 剖析 第㆓级配置器多了㆒些机制,避免太多小额区块造成内存的破碎。小额区块带 来的其实不仅是内存破碎而已,配置时的额外负担(overhead)也是㆒大问题8。 额外负担永远无法避免,毕竟系统要靠这多出来的空间来管理内存,如图 2-3。 但是区块愈小,额外负担所占的比例就愈大、愈显得浪费。 8 请参考 [Meyers98] 条款 10:write operator delete if you write operator new. The Annotated STL Sources 60 pa 第 2 章 空间配置器(allocator) cookie,用以记录 内存大小 object 所需内存 图 2-3 索求任何㆒块内存,都得有㆒些「税」要缴给系统。 SGI 第㆓级配置器的作法是,如果区块够大,超过 128 bytes,就移交第㆒级配置 器处理。当区块小于 128 bytes,则以记忆池(memory pool)管理,此法又称为次 层配置(sub-allocation):每次配置㆒大块内存,并维护对应之自由串行(free- list)。㆘次若再有相同大小的内存需求,就直接从 free-lists ㆗拨出。如果客端 释还小额区块,就由配置器回收到 free-lists ㆗ — 是的,别忘了,配置器除了负 责配置,也负责回收。为了方便管理,SGI 第㆓级配置器会主动将任何小额区块 的 记 忆体需 求量㆖ 调至 8 的 倍数 (例如客端 要 求 30 bytes , 就自动 调整 为 32 bytes),并维护 16 个 free-lists,各自管理大小分别为 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128 bytes 的小额区块。free-lists 的节点结构如㆘: union obj { union obj * free_list_link; char client_data[1]; /* The client sees this. */ }; 诸君或许会想,为了维护串行(lists),每个节点需要额外的指标(指向㆘㆒个节 点),这不又造成另㆒种额外负担吗?你的顾虑是对的,但早已有好的解决办法。 注意,㆖述 obj 所用的是 union,由于 union 之故,从其第㆒字段观之,obj 可 被视为㆒个指标,指向相同形式的另㆒个 obj。从其第㆓字段观之,obj 可被视 为㆒个指标,指向实际区块,如图 2-4。㆒物㆓用的结果是,不会为了维护串行所 必须的指针而造成内存的另㆒种浪费(我们正在努力樽节内存的开销呢)。 这种技巧在强型(strongly typed)语言如 Java ㆗行不通,但是在非强型语言如 C++ ㆗十分普遍9。 9 请参考 [Lippman98] p840 及 [Noble] p254。 The Annotated STL Sources 2.2 具备次配置力(sub-allocation)的 SGI 空间配置器 61 free-list 这是㆒个小额区块 这些区块已交 给客端使用, 所以 free-list 不再指向它们。 图 2-4 自由串行(free-list)的实作技巧 ㆘面是第㆓级配置器的部份实作内容: enum {__ALIGN = 8}; // 小型区块的㆖调边界 enum {__MAX_BYTES = 128}; // 小型区块的㆖限 enum {__NFREELISTS = __MAX_BYTES/__ALIGN}; // free-lists 个数 // 以㆘是第㆓级配置器。 // 注意,无「template 型别参数」,且第㆓参数完全没派㆖用场。 // 第㆒参数用于多绪环境㆘。本书不讨论多绪环境。 template class __default_alloc_template { private: // ROUND_UP() 将 bytes ㆖调至 8 的倍数。 static size_t ROUND_UP(size_t bytes) { return (((bytes) + __ALIGN-1) & ~(__ALIGN - 1)); } private: union obj { // free-lists 的节点构造 union obj * free_list_link; char client_data[1]; /* The client sees this. */ }; private: // 16 个 free-lists static obj * volatile free_list[__NFREELISTS]; // 以㆘函式根据区块大小,决定使用第 n 号 free-list。n 从 1 起算。 static size_t FREELIST_INDEX(size_t bytes) { return (((bytes) + __ALIGN-1)/__ALIGN - 1); } The Annotated STL Sources 62 第 2 章 空间配置器(allocator) // 传回㆒个大小为 n 的对象,并可能加入大小为 n 的其它区块到 free list. static void *refill(size_t n); // 配置㆒大块空间,可容纳 nobjs 个大小为 "size" 的区块。 // 如果配置 nobjs 个区块有所不便,nobjs 可能会降低。 static char *chunk_alloc(size_t size, int &nobjs); // Chunk allocation state. static char *start_free; // 记忆池起始位置。只在 chunk_alloc()㆗变化 static char *end_free; // 记忆池结束位置。只在 chunk_alloc()㆗变化 static size_t heap_size; public: static void * allocate(size_t n) { /* 详述于后 */ } static void deallocate(void *p, size_t n) { /* 详述于后 */ } static void * reallocate(void *p, size_t old_sz, size_t new_sz); }; // 以㆘是 static data member 的定义与初值设定 template char *__default_alloc_template::start_free = 0; template char *__default_alloc_template::end_free = 0; template size_t __default_alloc_template::heap_size = 0; template __default_alloc_template::obj * volatile __default_alloc_template::free_list[__NFREELISTS] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }; 2.2.7 空间配置函式 allocate() 身 为 ㆒ 个 配 置 器 ,__default_alloc_template 拥 有 配 置 器 的 标 准 介 面 函 式 allocate()。此函式首先判断区块大小,大于 128 bytes 就呼叫第㆒级配置器,小 于 128 bytes 就检查对应的 free list。如果 free list 之内有可用的区块,就直接拿来 用,如果没有可用区块,就将区块大小㆖调至 8 倍数边界,然后呼叫 refill(), 准备为 free list 重新填充空间。refill() 将于稍后介绍。 // n must be > 0 static void * allocate(size_t n) { obj * volatile * my_free_list; The Annotated STL Sources 2.2 具备次配置力(sub-allocation)的 SGI 空间配置器 obj * result; // 大于 128 就呼叫第㆒级配置器 if (n > (size_t) __MAX_BYTES) return(malloc_alloc::allocate(n)); } // 寻找 16 个 free lists ㆗适当的㆒个 my_free_list = free_list + FREELIST_INDEX(n); result = *my_free_list; if (result == 0) { // 没找到可用的 free list,准备重新填充 free list 63 void *r = refill(ROUND_UP(n)); // ㆘节详述 return r; } // 调整 free list *my_free_list = result -> free_list_link; return (result); }; 区块自 free list 拨出的动作,如图 2-5。 my_free_list free_list[16] 1 它负责 96bytes 区块 #0 #1 #2 #3 #4 #5 #6 #7 #8 #9 #10 #11 #12 #13 # result 2 拨出这个区块 n= 96bytes 96bytes 3 0 96bytes 1 2 3 my_free_list = free_list + FREELIST_INDEX(n); result = *my_free_list; // 侯捷注:调整 free list *my_free_list = result->free_list_link; return (result); 96bytes 图 2-5 区块自 free list 拨出。阅读次序请循图㆗编号。 The Annotated STL Sources 64 第 2 章 空间配置器(allocator) 2.2.8 空间释还函式 deallocate() 身 为 ㆒ 个 配 置 器 ,__default_alloc_template 拥 有 配 置 器 标 准 介 面 函 式 deallocate()。此函式首先判断区块大小,大于 128 bytes 就呼叫第㆒级配置器, 小于 128 bytes 就找出对应的 free list,将区块回收。 // p 不可以是 0 static void deallocate(void *p, size_t n) { obj *q = (obj *)p; obj * volatile * my_free_list; // 大于 128 就呼叫第㆒级配置器 if (n > (size_t) __MAX_BYTES) { malloc_alloc::deallocate(p, n); return; } // 寻找对应的 free list my_free_list = free_list + FREELIST_INDEX(n); // 调整 free list,回收区块 q -> free_list_link = *my_free_list; *my_free_list = q; } 区块回收纳入 free list 的动作,如图 2-6。 The Annotated STL Sources 2.2 具备次配置力(sub-allocation)的 SGI 空间配置器 my_free_list 65 free_list[16] 2 它负责 96bytes 区块 #0 #1 #2 #3 #4 #5 #6 #7 #8 #9 #10 #11 #12 #13 # 回收这个区块 p 3 n= 96bytes 1 q 4 0 96bytes 96bytes 96bytes 1 2 3 4 obj *q = (obj *)p; obj * volatile * my_free_list; // 侯捷注:寻找对应的 free list my_free_list = free_list + FREELIST_INDEX(n); // 侯捷注:调整 free list,回收区块 q->free_list_link = *my_free_list; *my_free_list = q; 图 2-6 区块回收,纳入 free list。阅读次序请循图㆗编号。 2.2.9 重新充填 free lists 回头讨论先前说过的 allocate()。当它发现 free list ㆗没有可用区块了,就呼叫 refill() 准 备 为 free list 重 新 填 充 空 间 。 新 的 空 间 将 取 自 记 忆 池 ( 经 由 chunk_alloc() 完成)。预设取得 20 个新节点(新区块),但万㆒记忆池空间 不足,获得的节点数(区块数)可能小于 20: // 传回㆒个大小为 n 的对象,并且有时候会为适当的 free list 增加节点. // 假设 n 已经适当㆖调至 8 的倍数。 template void* __default_alloc_template::refill(size_t n) { int nobjs = 20; // 呼叫 chunk_alloc(),尝试取得 nobjs 个区块做为 free list 的新节点。 // 注意参数 nobjs 是 pass by reference。 char * chunk = chunk_alloc(n, nobjs); // ㆘节详述 The Annotated STL Sources 66 obj obj obj int * volatile * my_free_list; * result; * current_obj, * next_obj; i; 第 2 章 空间配置器(allocator) // 如果只获得㆒个区块,这个区块就拨给呼叫者用,free list 无新节点。 if (1 == nobjs) return(chunk); // 否则准备调整 free list,纳入新节点。 my_free_list = free_list + FREELIST_INDEX(n); // 以㆘在 chunk 空间内建立 free list result = (obj *)chunk; // 这㆒块准备传回给客端 // 以㆘导引 free list 指向新配置的空间(取自记忆池) *my_free_list = next_obj = (obj *)(chunk + n); // 以㆘将 free list 的各节点串接起来。 for (i = 1; ; i++) { // 从 1 开始,因为第 0 个将传回给客端 current_obj = next_obj; next_obj = (obj *)((char *)next_obj + n); if (nobjs - 1 == i) { current_obj -> free_list_link = 0; break; } else { current_obj -> free_list_link = next_obj; } } return(result); } 2.2.10 记忆池( memory pool) 从记忆池㆗取空间给 free list 使用,是 chunk_alloc() 的工作: // 假设 size 已经适当㆖调至 8 的倍数。 // 注意参数 nobjs 是 pass by reference。 template char* __default_alloc_template:: chunk_alloc(size_t size, int& nobjs) { char * result; size_t total_bytes = size * nobjs; size_t bytes_left = end_free - start_free; // 记忆池剩余空间 if (bytes_left >= total_bytes) { // 记忆池剩余空间完全满足需求量。 result = start_free; start_free += total_bytes; The Annotated STL Sources 2.2 具备次配置力(sub-allocation)的 SGI 空间配置器 67 return(result); } else if (bytes_left >= size) { // 记忆池剩余空间不能完全满足需求量,但足够供应㆒个(含)以㆖的区块。 nobjs = bytes_left/size; total_bytes = size * nobjs; result = start_free; start_free += total_bytes; return(result); } else { // 记忆池剩余空间连㆒个区块的大小都无法提供。 size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4); // 以㆘试着让记忆池㆗的残余零头还有利用价值。 if (bytes_left > 0) { // 记忆池内还有㆒些零头,先配给适当的 free list。 // 首先寻找适当的 free list。 obj * volatile * my_free_list = free_list + FREELIST_INDEX(bytes_left); // 调整 free list,将记忆池㆗的残余空间编入。 ((obj *)start_free) -> free_list_link = *my_free_list; *my_free_list = (obj *)start_free; } // 配置 heap 空间,用来挹注记忆池。 start_free = (char *)malloc(bytes_to_get); if (0 == start_free) { // heap 空间不足,malloc() 失败。 int i; obj * volatile * my_free_list, *p; // 试着检视我们手㆖拥有的东西。这不会造成伤害。我们不打算尝试配置 // 较小的区块,因为那在多行程(multi-process)机器㆖容易导致灾难 // 以㆘搜寻适当的 free list, // 所谓适当是指「尚有未用区块,且区块够大」之 free list。 for (i = size; i <= __MAX_BYTES; i += __ALIGN) { my_free_list = free_list + FREELIST_INDEX(i); p = *my_free_list; if (0 != p) { // free list 内尚有未用区块。 // 调整 free list 以释出未用区块 *my_free_list = p -> free_list_link; start_free = (char *)p; end_free = start_free + i; // 递归呼叫自己,为了修正 nobjs。 return(chunk_alloc(size, nobjs)); // 注意,任何残余零头终将被编入适当的 free-list ㆗备用。 } } end_free = 0; // 如果出现意外(山穷水尽,到处都没内存可用了) // 呼叫第㆒级配置器,看看 out-of-memory 机制能否尽点力 start_free = (char *)malloc_alloc::allocate(bytes_to_get); // 这会导致掷出异常(exception),或内存不足的情况获得改善。 The Annotated STL Sources 68 第 2 章 空间配置器(allocator) } heap_size += bytes_to_get; end_free = start_free + bytes_to_get; // 递归呼叫自己,为了修正 nobjs。 return(chunk_alloc(size, nobjs)); } } ㆖述的 chunk_alloc() 函式以 end_free - start_free 来判断记忆池的水量。 如果水量充足,就直接拨出 20 个区块传回给 free list。如果水量不足以提供 20 个 区块,但还足够供应㆒个以㆖的区块,就拨出这不足 20 个区块的空间出去。这时 候其 pass by reference 的 nobjs 参数将被修改为实际能够供应的区块数。如果记 忆池连㆒个区块空间都无法供应,对客端显然无法交待,此时便需利用 malloc() 从 heap ㆗配置内存,为记忆池注入活水源头以应付需求。新水量的大小为需求 量的两倍,再加㆖㆒个随着配置次数增加而愈来愈大的附加量。 举个例子,见图 2-7,假设程序㆒开始,客端就呼叫 chunk_alloc(32,20),于是 malloc() 配置 40 个 32bytes 区块,其㆗第 1 个交出,另 19 个交给 free_list[3] 维 护 , 余 20 个 留 给 记 忆 池 。 接 ㆘ 来 客 端 呼 叫 chunk_alloc(64,20) , 此 时 free_list[7] 空空如也,必须向记忆池要求支持。记忆池只够供应 (32*20)/64=10 个 64bytes 区块,就把这 10 个区块传回,第 1 个交给客端,余 9 个由 free_list[7] 维护。此时记忆池全空。接㆘来再呼叫 chunk_alloc(96, 20),此时free_list[11] 空空如也,必须向记忆池要求支持,而记忆池此时也是空的,于是以 malloc() 配 置 40+n(附加量)个 96bytes 区块,其㆗第 1 个交出,另 19 个交给 free_list[11] 维护,余 20+n(附加量)个区块留给记忆池……。 万㆒山穷水尽,整个 system heap 空间都不够了(以至无法为记忆池注入活水源 头),malloc() 行动失败,chunk_alloc() 就㆕处寻找有无「尚有未用区块, 且区块够大」之 free lists。找到的话就挖㆒块交出,找不到的话就呼叫第㆒级配 置器。第㆒级配置器其实也是使用 malloc() 来配置内存,但它有 out-of-memory 处理机制(类似 new-handler 机制),或许有机会释放其它的内存拿来此处使用。 如果可以,就成功,否则发出 bad_alloc 异常。 以㆖便是整个第㆓级空间配置器的设计。 The Annotated STL Sources 2.2 具备次配置力(sub-allocation)的 SGI 空间配置器 69 它负责 它负责 它负责 free_list[16] 32bytes 区块 64bytes 区块 96bytes 区块 #0 #1 #2 #3 #4 #5 #6 #7 #8 #9 #10 #11 #12 #13 # 第㆒块传 第㆒块传 回给客端 32bytes 回给客端 32bytes 32bytes 32bytes 32bytes 这些连续区块,以 union obj 串接起来,形成 free list 的实 质组成。图㆗的小小箭头即 表示它们形成㆒个 linked list。 96bytes 96bytes 96bytes 0 第㆒块传 回给客端 64bytes 64bytes 0 96bytes start_free 0 64bytes 64bytes memory pool end_free 图 2-7 记忆池(memory pool)实际操练结果 回想㆒㆘ 2.2.4 节最后提到的那个提供配置器标准接口的 simple_alloc: template class simple_alloc { ... }; SGI 容器通常以这种方式来使用配置器: template // 预设使用 alloc 为配置器 class vector { public: typedef T value_type; ... The Annotated STL Sources 10 70 第 2 章 空间配置器(allocator) protected: // 专属之空间配置器,每次配置㆒个元素大小 typedef simple_alloc data_allocator; ... }; 其㆗第㆓个 template 参数所接受的预设自变量 alloc,可以是第㆒级配置器,也可 以是第㆓级配置器。不过,SGI STL 已经把它设为第㆓级配置器,见 2.2.4 节及图 2-2b。 2.3 内存基本处理工具 STL 定义有五个全域函式,作用于未初始化空间㆖。这样的功能对于容器的实作 很有帮助,我们会在第㆕章容器实作码㆗,看到它们的吃重演出。前两个函式是 2.2.3 节 说 过 , 用 于 建 构 的 construct() 和 用 于 解 构 的 destroy() , 另 ㆔ 个 函 式 是 uninitialized_copy(),uninitialized_fill(),uninitialized_fill_n() ,分别对应于高阶函式 copy()、fill()、fill_n() — 这些都是 STL 算法, 将在第六章介绍。如果你要使用本节的㆔个低阶函式,应该含入 ,不过 SGI 把它们实际定义于 。 2.3.1 uninitialized_copy template ForwardIterator uninitialized_copy(InputIterator first, InputIterator last, ForwardIterator result); uninitialized_copy() 使 我 们 能 够 将 记 忆 体 的 配 置 与 物 件 的 建 构 行 为 分 离 开 来。如果做为输出目的㆞的 [result, result+(last-first)) 范围内的每㆒个 迭 代 器 都 指 向 未 初 始 化 区 域 , 则 uninitialized_copy() 会 使 用 copy constructor,为身为输入来源之 [first,last) 范围内的每㆒个对象产生㆒份复 制品,放进输出范围㆗。换句话说,针对输入范围内的每㆒个迭代器 i,此函式会 呼叫 construct(&*(result+(i-first)),*i) ,产 生 *i 的 复制 品, 放置 于 输 10 [Austern98] 10.4 节对于这㆔个低阶函式有详细的介绍。 The Annotated STL Sources , 2.3 内存基本处理工具 71 出范围的相对位置㆖。式㆗的 construct() 已于 2.2.3 节讨论过。 如果你有需要实作㆒个容器,uninitialized_copy() 这样的函式会为你带来很 大的帮助,因为容器的全范围建构式(range constructor)通常以两个步骤完成: 配置内存区块,足以包含范围内的所有元素。 使用 uninitialized_copy(),在该内存区块㆖建构元素。 C++ 标准规格书要求 uninitialized_copy() 具有 "commit or rollback" 语意, 意思是要不就「建构出所有必要元素」,要不就(当有任何㆒个 copy constructor 失败时)「不建构任何东西」。 2.3.2 uninitialized_fill template void uninitialized_fill(ForwardIterator first, ForwardIterator last, const T& x); uninitialized_fill() 也 能 够 使 我 们 将 记 忆 体 配 置 与 物 件 的 建 构 行 为 分 离 开 来。 如果 [first,last) 范 围内的每个迭代器都指向未初 始化的内存 ,那么 uninitialized_fill() 会在该范围内产生 x(㆖式第㆔参数)的复制品。换句 话 说 uninitialized_fill() 会 针 对 操 作 范 围 内 的 每 个 迭 代 器 i , 呼 叫 construct(&*i, x),在 i 所指之处产生 x 的复制品。式㆗的 construct() 已 于 2.2.3 节讨论过。 和 uninitialized_copy() ㆒样,uninitialized_fill() 必须具备 "commit or rollback" 语意,换句话说它要不就产生出所有必要元素,要不就不产生任何元素。 如果有任何㆒个 copy constructor 丢出异常(exception)uninitialized_fill() 必须能够将已产生之所有元素解构掉。 2.3.3 uninitialized_fill_n template ForwardIterator uninitialized_fill_n(ForwardIterator first, Size n, const T& x); The Annotated STL Sources 72 第 2 章 空间配置器(allocator) uninitialized_fill_n() 能够使我们将内存配置与对象建构行为分离开来。 它会为指定范围内的所有元素设定相同的初值。 如果 [first, first+n) 范围内的每㆒个迭代器都指向未初始化的内存,那么 uninitialized_fill_n() 会呼叫 copy constructor,在该范围内产生 x(㆖式 第㆔参数)的复制品。也就是说面对 [first,first+n) 范围内的每个迭代器 i, uninitialized_fill_n() 会呼叫 construct(&*i, x),在对应位置处产生 x 的 复制品。式㆗的 construct() 已于 2.2.3 节讨论过。 uninitialized_fill_n() 也具有 "commit or rollback" 语意:要不就产生所有必 要的元素,否则就不产生任 何元素。如果任何㆒个 copy constructor 丢出异常 (exception),uninitialized_fill_n() 必须解构已产生的所有元素。 以 ㆘ 分 别 介 绍 这 ㆔ 个 函 式 的 实 作 法 。 其 ㆗ 所 呈 现 的 iterators ( 迭 代 器 ) 、 value_type()、__type_traits、__true_type、__false_type、is_POD_type 等实作技术,都将于第㆔章介绍。 (1) uninitialized_fill_n 首先是 uninitialized_fill_n() 的源码。本函式接受㆔个参数: 1. 迭代器 first 指向欲初始化空间的起始处 2. n 表示欲初始化空间的大小 3. x 表示初值 template inline ForwardIterator uninitialized_fill_n(ForwardIterator first, Size n, const T& x) { return __uninitialized_fill_n(first, n, x, value_type(first)); // 以㆖,利用 value_type() 取出 first 的 value type. } 这个函式的进行逻辑是,首先萃取出迭代器 first 的 value type(详见第㆔章), 然后判断该型别是否为 POD 型别: template inline ForwardIterator __uninitialized_fill_n(ForwardIterator first, Size n, const T&x, T1*) The Annotated STL Sources 2.3 内存基本处理工具 73 { // 以㆘ __type_traits<> 技法,详见 3.7 节 typedef typename __type_traits::is_POD_type is_POD; return __uninitialized_fill_n_aux(first, n, x, is_POD()); } POD 意指 Plain Old Data,也就是纯量型别(scalar types)或传统的 C struct 型 别。POD 型别必然拥有 trivial ctor/dtor/copy/assignment 函式,因此,我们可 以对 POD 型别采取最有效率的初值填写手法,而对 non-POD 型别采取最保险安 全的作法: // 如果 copy construction 等同于 assignment, 而且 // destructor 是 trivial,以㆘就有效。 // 如果是 POD 型别,执行流程就会转进到以㆘函式。这是藉由 function template // 的自变量推导机制而得。 template inline ForwardIterator __uninitialized_fill_n_aux(ForwardIterator first, Size n, const T& x, __true_type) { return fill_n(first, n, x); // 交由高阶函式执行。见 6.4.2 节。 } // 如果不是 POD 型别,执行流程就会转进到以㆘函式。这是藉由 function template // 的自变量推导机制而得。 template ForwardIterator __uninitialized_fill_n_aux(ForwardIterator first, Size n, const T& x, __false_type) { ForwardIterator cur = first; // 为求阅读顺畅,以㆘将原本该有的异常处理(exception handling)省略。 for ( ; n > 0; --n, ++cur) construct(&*cur, x); // 见 2.2.3 节 return cur; } (2) uninitialized_copy ㆘面列出 uninitialized_copy() 的源码。本函式接受㆔个参数: 迭代器 first 指向输入端的起始位置 迭代器 last 指向输入端的结束位置(前闭后开区间) 迭代器 result 指向输出端(欲初始化空间)的起始处 template inline ForwardIterator uninitialized_copy(InputIterator first, InputIterator last, The Annotated STL Sources 74 第 2 章 空间配置器(allocator) ForwardIterator result) { return __uninitialized_copy(first, last, result, value_type(result)); // 以㆖,利用 value_type() 取出 first 的 value type. } 这个函式的进行逻辑是,首先萃取出迭代器 result 的 value type(详见第㆔章), 然后判断该型别是否为 POD 型别: template inline ForwardIterator __uninitialized_copy(InputIterator first, InputIterator last, ForwardIterator result, T*) { typedef typename __type_traits::is_POD_type is_POD; return __uninitialized_copy_aux(first, last, result, is_POD()); // 以㆖,企图利用 is_POD() 所获得的结果,让编译器做自变量推导。 } POD 意指 Plain Old Data,也就是纯量型别(scalar types)或传统的 C struct 型别。 POD 型别必然拥有 trivial ctor/dtor/copy/assignment 函式,因此,我们可以对 POD 型别采取最有效率的复制手法,而对 non-POD 型别采取最保险安全的作法: // 如果 copy construction 等同于 assignment, 而且 // destructor 是 trivial,以㆘就有效。 // 如果是 POD 型别,执行流程就会转进到以㆘函式。这是藉由 function template // 的自变量推导机制而得。 template inline ForwardIterator __uninitialized_copy_aux(InputIterator first, InputIterator last, ForwardIterator result, __true_type) { return copy(first, last, result); // 呼叫 STL 算法 copy() } // 如果是 non-POD 型别,执行流程就会转进到以㆘函式。这是藉由function template // 的自变量推导机制而得。 template ForwardIterator __uninitialized_copy_aux(InputIterator first, InputIterator last, ForwardIterator result, __false_type) { ForwardIterator cur = result; // 为求阅读顺畅,以㆘将原本该有的异常处理(exception handling)省略。 for ( ; first != last; ++first, ++cur) construct(&*cur, *first); // 必须㆒个㆒个元素㆞建构,无法批量进行 return cur; } } The Annotated STL Sources 2.3 内存基本处理工具 75 针对 char* 和 wchar_t* 两种型别,可以最具效率的作法 memmove(直接搬移 内存内容)来执行复制行为。因此 SGI 得以为这两种型别设计㆒份特化版本。 // 以㆘是针对 const char* 的特化版本 inline char* uninitialized_copy(const char* first, const char* last, char* result) { memmove(result, first, last - first); return result + (last - first); } // 以㆘是针对 const wchar_t* 的特化版本 inline wchar_t* uninitialized_copy(const wchar_t* first, const wchar_t* last, wchar_t* result) { memmove(result, first, sizeof(wchar_t) * (last - first)); return result + (last - first); } (3) uninitialized_fill ㆘面列出 uninitialized_fill() 的源码。本函式接受㆔个参数: 迭代器 first 指向输出端(欲初始化空间)的起始处 迭代器 last 指向输出端(欲初始化空间)的结束处(前闭后开区间) x 表示初值 template inline void uninitialized_fill(ForwardIterator first, ForwardIterator last, const T& x) { __uninitialized_fill(first, last, x, value_type(first)); } 这个函式的进行逻辑是,首先萃取出迭代器 first 的 value type(详见第㆔章), 然后判断该型别是否为 POD 型别: template inline void __uninitialized_fill(ForwardIterator first, ForwardIterator last, const T& x, T1*) { typedef typename __type_traits::is_POD_type is_POD; __uninitialized_fill_aux(first, last, x, is_POD()); } POD 意指 Plain Old Data,也就是纯量型别(scalar types)或传统的 C struct 型别。 POD 型别必然拥有 trivial ctor/dtor/copy/assignment 函式,因此,我们可以对 POD 型别采取最有效率的初值填写手法,而对 non-POD 型别采取最保险安全的 The Annotated STL Sources 76 第 2 章 空间配置器(allocator) 作法: // 如果 copy construction 等同于 assignment, 而且 // destructor 是 trivial,以㆘就有效。 // 如果是 POD 型别,执行流程就会转进到以㆘函式。这是藉由 function template // 的自变量推导机制而得。 template inline void __uninitialized_fill_aux(ForwardIterator first, ForwardIterator last, const T& x, __true_type) { fill(first, last, x); // 呼叫 STL 算法 fill() } // 如果是 non-POD 型别,执行流程就会转进到以㆘函式。这是藉由function template // 的自变量推导机制而得。 template void __uninitialized_fill_aux(ForwardIterator first, ForwardIterator last, const T& x, __false_type) { ForwardIterator cur = first; // 为求阅读顺畅,以㆘将原本该有的异常处理(exception handling)省略。 for ( ; cur != last; ++cur) construct(&*cur, x); // 必须㆒个㆒个元素㆞建构,无法批量进行 } } 图 2-8 将本节㆔个函式对效率的特殊考虑,以图形显示。 The Annotated STL Sources __uninitialized_copy() construct(…); 内存底层动作, construct(…); construct(…); 2.3 内存基本处理工具 is POD ? 77 泛化 memmove() 特化 (const char*, 速度极快。 const char*, char*) memmove() 特化 (const wchar_t*, const wchar_t*, wchar_t*) is POD ? 特化 copy(…) __true_type uninitialized_fill() for(;first!=last;…) 泛化 __false_type 特化 fill(…) __true_type is POD ? uninitialized_fill_n() for(;count>0; …) 泛化 __false_type 特化 fill_n(…) __true_type 图 2-8 ㆔个内存基本函式的泛型版本与特化版本。 The Annotated STL Sources 78 第 2 章 空间配置器(allocator) The Annotated STL Sources 3.1 迭代器设计思维 — STL 关键所在 79 3 迭代器(iterators)概念 与 traits 编程技法 迭代器(iterators)是㆒种抽象的设计概念,现实程序语言㆗并没有直接对映于这 个概念的实物。《Design Patterns》㆒书提供有 23 个设计样式(design patterns)的 完整描述,其㆗ iterator 样式定义如㆘:提供一种方法,俾得依序巡访某个聚合物 (容器)所含的各个元素,而又无需曝露该聚合物的内部表述方式。 3.1 迭代器设计思维 — STL 关键所在 不论是泛型思维或 STL 的实际运用,迭代器(iterators)都扮演重要角色。STL 的 ㆗心思想在于,将数据容器(containers)和算法(algorithms)分开,彼此独立 设计,最后再以㆒帖胶着剂将它们撮合在㆒起。容器和算法的泛型化,从技术 角度来看并不困难,C++ 的 class templates 和 function templates 可分别达成目标。 如何设计出两者之间的良好胶着剂,才是大难题。 以㆘是容器、算法、迭代器(iterator,扮演黏胶角色)的合作展示。以算法 find() 为例,它接受两个迭代器和㆒个「搜寻标的」: // 摘自 SGI template InputIterator find(InputIterator first, InputIterator last, const T& value) { while (first != last && *first != value) ++first; return first; } The Annotated STL Sources 80 第 3 章 迭代器(iterators)概念与 traits 编程技法 只要给予不同的迭代器,find() 便能够对不同的容器做搜寻动作: // file : 3find.cpp #include #include #include #include #include using namespace std; int main() { const int arraySize = 7; int ia[arraySize] = { 0,1,2,3,4,5,6 }; vector ivect(ia, ia+arraySize); list ilist(ia, ia+arraySize); deque ideque(ia, ia+arraySize); // 注意:VC6[x],未符合标准 vector::iterator it1 = find(ivect.begin(), ivect.end(), 4); if (it1 == ivect.end()) cout << "4 not found." << endl; else cout << "4 found. " << *it1 << endl; // 执行结果:4 found. 4 list::iterator it2 = find(ilist.begin(), ilist.end(), 6); if (it2 == ilist.end()) cout << "6 not found." << endl; else cout << "6 found. " << *it2 << endl; // 执行结果:6 found. 6 deque::iterator it3 = find(ideque.begin(), ideque.end(), 8); if (it3 == ideque.end()) cout << "8 not found." << endl; else cout << "8 found. " << *it3 << endl; // 执行结果:8 not found. } 从这个例子看来,迭代器似乎依附在容器之㆘。是吗?有没有独立而泛用的迭代 器?我们又该如何自行设计特殊的迭代器? 3.2 迭代器 ( iterator)是一种 smart pointer 迭代器是㆒种行为类似指针的对象,而指针的各种行为㆗最常见也最重要的便是 The Annotated STL Sources 3.2 迭代器(iterator)是㆒种 smart pointer 81 内容提领(dereference)和成员取用(member access),因此迭代器最重要的编程 工作就是对 operator* 和 operator-> 进行多载化(overloading)工程。关于这 ㆒点,C++ 标准链接库有㆒个 auto_ptr 可供我们参考。任何㆒本详尽的 C++ 语 法书籍都应该谈到 auto_ptr(如果没有,扔了它 ),这是㆒个用来包装原生指 标(native pointer)的对象,声名狼藉的内存漏洞(memory leak)问题可藉此 获得解决。auto_ptr 用法如㆘,和原生指标㆒模㆒样: void func() { auto_ptr ps(new string("jjhou")); cout << *ps << endl; cout << ps->size() << endl; // 输出:jjhou // 输出:5 // 离开前不需 delete, auto_ptr 会自动释放内存 } 函式第㆒行的意思是,以算式 new 动态配置㆒个初值为 "jjhou" 的 string 物 件,并将所得结果(㆒个原生指针)做为 auto_ptr 对象的初值。注意, auto_ptr 角括号内放的是「原生指针所指对象」的型别,而不是原生指标的型别。 auto_ptr 的源码在表头档 ㆗,根据它,我模拟了㆒份阳春版,可具 体说明 auto_ptr 的行为与能力: // file: 3autoptr.cpp template class auto_ptr { public: explicit auto_ptr(T *p = 0): pointee(p) {} template auto_ptr(auto_ptr& rhs): pointee(rhs.release()) {} ~auto_ptr() { delete pointee; } template auto_ptr& operator=(auto_ptr& rhs) { if (this != &rhs) reset(rhs.release()); return *this; } T& operator*() const { return *pointee; } T* operator->() const { return pointee; } T* get() const { return pointee; } // ... private: T *pointee; The Annotated STL Sources 82 第 3 章 迭代器(iterators)概念与 traits 编程技法 }; 其㆗关键词 explicit 和 member template 等编程技法,并不是这里的讲述重点, 相关语法和语意请参阅 [Lippman98] 或 [Stroustrup97]。 有了模仿对象,现在我们来为 list(串行)设计㆒个迭代器1。假设 list 及其节点 的结构如㆘: // file: 3mylist.h template class List { void insert_front(T value); void insert_end(T value); void display(std::ostream &os = std::cout) const; // ... private: ListItem* _end; ListItem* _front; long _size; }; template class ListItem { public: T value() const { return _value; } ListItem* next() const { return _next; } ... private: T _value; ListItem* _next; // 单向串行(single linked list) }; 如何将这个 List 套用到先前所说的 find() 呢?我们需要为它设计㆒个行为类 似指标的外衣,也就是㆒个迭代器。当我们提领(dereference)此㆒迭代器,传回 的应该是个 ListItem 对象;当我们累加该迭代器,它应该指向㆘㆒个 ListItem 物件。为了让此迭代器适用于任何型态的节点,而不只限于 ListItem,我们可以 将它设计为㆒个 class template: 1 [Lippman98] 5.11 节有㆒个非泛型的 list 实例可以参考。《泛型思维》书㆗有㆒份泛 型版本的 list 的完整设计与说明。 The Annotated STL Sources 3.2 迭代器(iterator)是㆒种 smart pointer // file : 3mylist-iter.h #include "3mylist.h" template // Item 可以是单向串行节点或双向串行节点。 83 struct ListIter { // 此处这个迭代器特定只为串行服务,因为其 // 独特的 operator++ 之故。 Item* ptr; // 保持与容器之间的㆒个联系(keep a reference to Container) ListIter(Item* p = 0) // default ctor : ptr(p) { } // 不必实作 copy ctor,因为编译器提供的预设行为已足够。 // 不必实作 operator=,因为编译器提供的预设行为已足够。 Item& operator*() const { return *ptr; } Item* operator->() const { return ptr; } // 以㆘两个 operator++ 遵循标准作法,参见 [Meyers96] 条款 6 // (1) pre-increment operator ListIter& operator++() { ptr = ptr->next(); return *this; } // (2) post-increment operator ListIter operator++(int) { ListIter tmp = *this; ++*this; return tmp; } bool operator==(const ListIter& i) const { return ptr == i.ptr; } bool operator!=(const ListIter& i) const { return ptr != i.ptr; } }; 现在我们可以这样子将 List 和 find() 藉由 ListIter 黏合起来: // 3mylist-iter-test.cpp void main() { List mylist; for(int i=0; i<5; ++i) { mylist.insert_front(i); mylist.insert_end(i+2); } mylist.display(); // 10 ( 4 3 2 1 0 2 3 4 5 6 ) ListIter > begin(mylist.front()); ListIter > end; // default 0, null ListIter > iter; // default 0, null The Annotated STL Sources 84 第 3 章 迭代器(iterators)概念与 traits 编程技法 iter = find(begin, end, 3); if (iter == end) cout << "not found" << endl; else cout << "found. " << iter->value() << endl; // 执行结果:found. 3 iter = find(begin, end, 7); if (iter == end) cout << "not found" << endl; else cout << "found. " << iter->value() << endl; // 执行结果:not found } 注意,由于 find() 函式内以 *iter != value 来检查元素值是否吻合,而本例 之㆗ value 的型别是 int,iter 的型别是 ListItem,两者之间并无可 供使用的 operator!=,所以我必须另外写㆒个全域的 operator!= 多载函式, 并以 int 和 ListItem 做为它的两个参数型别: template bool operator!=(const ListItem& item, T n) { return item.value() != n; } 从以㆖实作可以看出,为了完成㆒个针对 List 而设计的迭代器,我们无可避免㆞ 曝露了太多 List 实作细节:在 main() 之㆗为了制作 begin 和 end 两个迭代 器,我们曝露了 ListItem;在 ListIter class 之㆗为了达成 operator++ 的目 的,我们曝露了 ListItem 的操作函式 next()。如果不是为了迭代器,ListItem 原本应该完全隐藏起来不曝光的。换句话说,要设计出 ListIter,首先必须对 List 的实作细节有非常丰富的了解。既然这无可避免,干脆就把迭代器的开发工作交 给 List 的设计者好了,如此㆒来所有实作细节反而得以封装起来不被使用者看 到。这正是为什么每㆒种 STL 容器都提供有专属迭代器的缘故。 3.3 迭代器相应型别 ( associated types) ㆖述的 ListIter 提供了㆒个迭代器雏形。如果将思想拉得更高远㆒些,我们便 会发现,算法之㆗运用迭代器时,很可能会用到其相应型别(associated type)。 什么是相应型别?迭代器所指之物的型别便是其㆒。假设算法㆗有必要宣告㆒ 个 变 数 , 以 「 迭 代 器 所 指 物 件 的 型 别 」 为 型 别 , 如 何 是 好 ? 毕 竟 C++ 只 支 援 The Annotated STL Sources 3.4 Traits 编程技法 — STL 源码门钥 85 sizeof(),并未支持 typeof()!即便动用 RTTI 性质㆗的 typeid(),获得的也 只是型别名称,不能拿来做变量宣告之用。 解决办法是有:利用 function template 的自变量推导(argument deducation)机制。 例如: template void func_impl(I iter, T t) { T tmp; // 这里解决了问题。T 就是迭代器所指之物的型别,本例为 int // ... 这里做原本 func() 应该做的全部工作 }; template inline void func(I iter) { func_impl(iter, *iter); // func 的工作全部移往 func_impl } int main() { int i; func(&i); } 我们 以 func() 为对外界面, 却把实际动作 全部置于 func_impl() 之 ㆗ 。由于 func_impl() 是㆒个 function template,㆒旦被呼叫,编译器会自动进行 template 自变量推导。于是导出型别 T,顺利解决了问题。 迭代器相应型别(associated types)不只是「迭代器所指对象的型别」㆒种而已。 根据经验,最常用的相应型别有五种,然而并非任何情况㆘任何㆒种都可利用㆖ 述的 template 自变量推导机制来取得。我们需要更全面的解法。 3.4 Traits 编程技法 — STL 源码门钥 迭代器所指物件的型别,称为该迭代器的 value type。㆖述的自变量型别推导技巧 虽然可用于 value type,却非全面可用:万㆒ value type 必须用于函式的传回值, 就束手无策了,毕竟函式的「template 自变量推导机制」推而导之的只是自变量,无 The Annotated STL Sources 86 第 3 章 迭代器(iterators)概念与 traits 编程技法 法推导函式的回返值型别。 我们需要其它方法。宣告巢状型别似乎是个好主意,像这样: template struct MyIter typedef T value_type; // 巢状型别宣告(nested type) T* ptr; MyIter(T* p=0) : ptr(p) { } T& operator*() const { return *ptr; } // ... }; template typename I::value_type // 这㆒整行是 func 的回返值型别 func(I ite) { return *ite; } // ... MyIter ite(new int(8)); cout << func(ite); // 输出:8 注意,func() 的回返型别必须加㆖关键词 typename,因为 T 是㆒个 template 参 数,在它被编译器具现化之前,编译器对 T ㆒无所悉,换句话说编译器此时并不 知道 MyIter::value_type 代表的是㆒个型别或是㆒个 member function 或 是㆒个 data member。关键词 typename 的用意在告诉编译器说这是㆒个型别, 如此才能顺利通过编译。 看起来不错。但是有个隐晦的陷阱:并不是所有迭代器都是 class type。原生指标 就不是!如果不是 class type,就无法为它定义巢状型别。但 STL(以及整个泛型 思维)绝对必须接受原生指标做为㆒种迭代器,所以㆖面这样还不够。有没有办 法可以让㆖述的㆒般化概念针对特定情况(例如针对原生指标)做特殊化处理呢? 是的,template partial specialization 可以做到。 Partial Specialization(偏 特化 )的意 义 任何完整的 C++ 语法书籍都应该对 template partial specialization 有所说明(如 果没有,扔了它 )。大致的意义是:如果 class template 拥有㆒个以㆖的 template 参数,我们可以针对其㆗某个(或数个,但非全部)template 参数进行特化工作。 The Annotated STL Sources 3.4 Traits 编程技法 — STL 源码门钥 87 换句话说我们可以在泛化设计㆗提供㆒个特化版本(也就是将泛化版本㆗的某些 template 参数赋予明确的指定)。 假设有㆒个 class template 如㆘: template class C { ... }; partial specialization 的字面意义容易误导我们以为,所谓「偏特化版」㆒定是对 template 参数 U 或 V 或 T(或某种组合)指定某个自变量值。事实不然,[Austern99] 对于 partial specialization 的意义说得十分得体:「所谓 partial specialization 的 意思是提供另一份 template 定义式,而其本身仍为 templatized」。《泛型技术》 ㆒书对 partial specialization 的定义是:「针对(任何)template 参数更进㆒步的条 件限制,所设计出来的㆒个特化版本」。由此,面对以㆘这么㆒个 class template: template class C { ... }; // 这个泛化版本允许(接受)T 为任何型别 我们便很容易接受它有㆒个型式如㆘的 partial specialization: template class C { ... }; // 这个特化版本仅适用于「T 为原生指标」的情况 // 「T 为原生指标」便是「T 为任何型别」的㆒个更进㆒步的条件限制 有了这项利器,我们便可以解决前述「巢状型别」未能解决的问题。先前的问题 是,原生指标并非 class,因此无法为它们定义巢状型别。现在,我们可以针对「迭 代器之 template 自变量为指标」者,设计特化版的迭代器。 提高警觉,我们进入关键㆞带了。㆘面这个 class template 专门用来「萃取」迭代 器的特性,而 value type 正是迭代器的特性之㆒: template struct iterator_traits { // traits 意为「特性」 typedef typename I::value_type value_type; }; 这个所谓的 traits,其意义是,如果 I 定义有自己的 value type,那么透过这个 traits 的作用,萃取出来的 value_type 就是 I::value_type。换句话说如果 I 定义有自己的 value type,先前那个 func() 可以改写成这样: The Annotated STL Sources 88 第 3 章 迭代器(iterators)概念与 traits 编程技法 template typename iterator_traits::value_type // 这㆒整行是函式回返型别 func(I ite) { return *ite; } 但这除了多㆒层间接性,又带来什么好处?好处是 traits 可以拥有特化版本。现 在,我们令 iterator_traites 拥有㆒个 partial specializations 如㆘: template struct iterator_traits { // 偏特化版 — 迭代器是个原生指标 typedef T value_type; }; 于 是 ,原生指标 int* 虽然 不是㆒种 class type, 亦 可透 过 traits 取 其 value type。这就解决了先前的问题。 但是请注意,针对「指向常数对象的指针(pointer-to-const)」,㆘面这个式子 得到什么结果: iterator_traits::value_type 获得的是 const int 而非 int。这是我们期望的吗?我们希望利用这种机制来 宣告㆒个暂时变量,使其型别与迭代器的 value type 相同,而现在,宣告㆒个无 法 赋 值 ( 因 const 之 故 ) 的 暂 时 变 数 , 没 什 么 用 ! 因 此 , 如 果 迭 代 器 是 个 pointer-to-const,我们应该设法令其 value type 为㆒个 non-const 型别。没问 题,只要另外设计㆒个特化版本,就能解决这个问题: template struct iterator_traits { // 偏特化版 — 当迭代器是个 pointer-to-const typedef T value_type; // 萃取出来的型别应该是 T 而非 const T }; 现在,不论面对的是迭代器 MyIter,或是原生指标 int* 或 const int*,都可 以透过 traits 取出正确的(我们所期望的)value type。 图 3-1 说明 traits 所扮演的「特性萃取机」角色,萃取各个迭代器的特性。这里 所谓的迭代器特性,指的是迭代器的相应型别(associated types)。当然,若要这 个「特性萃取机」traits 能够有效运作,每㆒个迭代器必须遵循约定,自行以巢状 型别定义(nested typedef)的方式定义出相应型别(associated types)。这种㆒个 约定,谁不遵守这个约定,谁就不能相容于 STL 这个大家庭。 The Annotated STL Sources 3.4 Traits 编程技法 — STL 源码门钥 89 凡原生指标,都没有能力 定义自己的相应型别 int* 特 性 萃 iterator_traits const int* 取 机 list::iterator deque::iterator 透过 class template partial specialization 的作用,不论是原生指标或 class-type iterators,都可以让外界方便㆞取其相 应型别(associated types): value_type difference_type pointer reference iterator_category vector::iterator MyIter 凡 class-type iterators,都有能力 (且应该)定义自己的相应型别 图 3-1 traits 就像㆒台「特性萃取机」,榨取各个迭代器的特性(相应型别)。 根据经验,最常用到的迭代器相应型别有五种:value type, difference type, pointer, reference, iterator catagoly。如果你希望你所开发的容器能与 STL 水乳交融,㆒ 定要为你的容器的迭代器定义这五种相应型别。「特性萃取机」traits 会很忠实㆞ 将原汁原味榨取出来: template struct iterator_traits { typedef typename I::iterator_category typedef typename I::value_type typedef typename I::difference_type typedef typename I::pointer typedef typename I::reference iterator_category; value_type; difference_type; pointer; reference; }; iterator_traits 必须针对传入之型别为 pointer 及 pointer-to-const 者,设计 特化版本,稍后数节为你展示如何进行。 The Annotated STL Sources 90 第 3 章 迭代器(iterators)概念与 traits 编程技法 3.4.1 迭代器相应型别之一:value type 所谓 value type,是指迭代器所指对象的型别。任何㆒个打算与 STL 算法有完 美搭配的 class,都应该定义自己的 value type 巢状型别,作法就像㆖节所述。 3.4.2 迭代器相应型别之二:difference type difference type 用来表示两个迭代器之间的距离,也因此,它可以用来表示㆒个容 器的最大容量,因为对于连续空间的容器而言,头尾之间的距离就是其最大容量。 如果㆒个泛型算法提供计数功能,例如 STL 的 count(),其传回值就必须使 用迭代器的 difference type: template typename iterator_traits::difference_type // 这㆒整行是函式回返型别 count(I first, I last, const T& value) { typename iterator_traits::difference_type n = 0; for ( ; first != last; ++first) if (*first == value) ++n; return n; } 针对相应型别 difference type,traits 的两个(针对原生指标而写的)特化版本如 ㆘,以 C++ 内建的 ptrdiff_t(定义于 表头档)做为原生指标的 difference type: template struct iterator_traits { ... typedef typename I::difference_type difference_type; }; // 针对原生指标而设计的「偏特化(partial specialization)」版 template struct iterator_traits { ... typedef ptrdiff_t difference_type; }; // 针对原生的 pointer-to-const 而设计的「偏特化(partial specialization)」版 template struct iterator_traits { ... The Annotated STL Sources 3.4 Traits 编程技法 — STL 源码门钥 typedef ptrdiff_t difference_type; 91 }; 现在,任何时候当我们需要任何迭代器 I 的 difference type ,可以这么写: typename iterator_traits::difference_type 3.4.3 迭代器相应型别之三:reference type 从「迭代器所指之物的内容是否允许改变」的角度观之,迭代器分为两种:不允 许改变「所指对象之内容」者,称为 constant iterators,例如 const int* pic; 允许改变「所指对象之内容」者,称为 mutable iterators,例如 int* pi。当我们 对㆒个 mutable iterators 做提领动作时,获得的不应该是个右值(rvalue),应该 是个左值(lvalue),因为右值不允许赋值动作(assignment),左值才允许: int* pi = new int(5); const int* pci = new int(9); *pi = 7; // 对 mutable iterator 做提领动作时,获得的应该是个左值,允许赋值。 *pci = 1; // 这个动作不允许,因为 pci 是个 constant iterator, // 提领 pci 所得结果,是个右值,不允许被赋值。 在 C++ ㆗,函式如果要传回左值,都是以 by reference 的方式进行,所以当 p 是 个 mutable iterators 时,如果其 value type 是 T,那么 *p 的型别不应该是 T, 应该是 T&。将此道理扩充,如果 p 是㆒个 constant iterators,其 value type 是 T, 那么 *p 的型别不应该是 const T,而应该是 const T&。这里所讨论的 *p 的 型别,即所谓的 reference type。实作细节将在㆘㆒小节㆒并展示。 3.4.4 迭代器相应型别之四:pointer type pointers 和 references 在 C++ ㆗有非常密切的关连。如果「传回㆒个左值,令它 代表 p 所指之物」是可能的,那么「传回㆒个左值,令它代表 p 所指之物的位 址」也㆒定可以。也就是说我们能够传回㆒个 pointer,指向迭代器所指之物。 这些相应型别已在先前的 ListIter class ㆗出现过: Item& operator*() const { return *ptr; } Item* operator->() const { return ptr; } Item& 便是 ListIter 的 reference type 而 Item* 便是其 pointer type。 The Annotated STL Sources 92 第 3 章 迭代器(iterators)概念与 traits 编程技法 现在我们把 reference type 和 pointer type 这两个相应型别加入 traits 内: template struct iterator_traits { ... typedef typename I::pointer typedef typename I::reference pointer; reference; }; // 针对原生指标而设计的「偏特化版(partial specialization)」 template struct iterator_traits { ... typedef T* pointer; typedef T& reference; }; // 针对原生的 pointer-to-const 而设计的「偏特化版(partial specialization)」 template struct iterator_traits { ... typedef const T* pointer; typedef const T& reference; }; 3.4.5 迭代器相应型别之五:iterator_category 最后㆒个(第五个)迭代器相应型别会引发较大规模的写码工程。在那之前,我 必须先讨论迭代器的分类。 根据移动特性与施行动作,迭代器被分为五类: Input Iterator:这种迭代器所指对象,不允许外界改变。只读(read only)。 Output Iterator:唯写(write only)。 Forward Iterator:允许「写入型」算法(例如 replace())在此种迭代 器所形成的区间㆖做读写动作。 Bidirectional Iterator:可双向移动。某些算法需要逆向走访某个迭代器区 间(例如逆向拷贝某范围内的元素),就可以使用 Bidirectional Iterators。 Random Access Iterator:前㆕种迭代器都只供应㆒部份指标算术能力(前㆔ 种支持 operator++,第㆕种再加㆖ operator--),第五种则涵盖所有指 标算术能力,包括 p+n, p-n, p[n], p1-p2, p1 void advance_II(InputIterator& i, Distance n) { // 单向,逐㆒前进 while (n--) ++i; // 或写 for ( ; n > 0; --n, ++i ); 2 concept(概念)与 refinement(强化),是架构 STL 的重要观念,详见 [Austern98]。 The Annotated STL Sources 94 第 3 章 迭代器(iterators)概念与 traits 编程技法 } template void advance_BI(BidirectionalIterator& i, Distance n) { // 双向,逐㆒前进 if (n >= 0) while (n--) ++i; else while (n++) --i; // 或写 for ( ; n > 0; --n, ++i ); // 或写 for ( ; n < 0; ++n, --i ); } template void advance_RAI(RandomAccessIterator& i, Distance n) { // 双向,跳跃前进 i += n; } 现在,当程序呼叫 advance(),应该选用(呼叫)哪㆒份函式定义呢?如果选择 advance_II(),对 Random Access Iterator 而言极度缺乏效率,原本 O(1) 的操 作竟成为 O(N)。如果选择 advance_RAI(),则它无法接受 Input Iterator。我们 需要将㆔者合㆒,㆘面是㆒种作法: template void advance(InputIterator& i, Distance n) { if (is_random_access_iterator(i)) // 此函式有待设计 advance_RAI(i, n); else if (is_bidirectional_iterator(i)) // 此函式有待设计 advance_BI(i, n); else advance_II(i, n); } 但是像这样在执行时期才决定使用哪㆒个版本,会影响程序效率。最好能够在编 译期就选择正确的版本。多载化函式机制可以达成这个目标。 前述㆔个 advance_xx() 都有两个函式参数,型别都未定(因为都是 template 参 数)。为了令其同名,形成多载化函式,我们必须加㆖㆒个型别已确定的函式参 数,使函式多载化机制得以有效运作起来。 设计考虑如㆘:如果 traits 有能力萃取出迭代器的种类,我们便可利用这个「迭 代器类型」相应型别做为 advanced() 的第㆔参数。这个相应型别㆒定必须是个 The Annotated STL Sources 3.4 Traits 编程技法 — STL 源码门钥 95 class type,不能只是数值号码类的东西,因为编译器需仰赖它(㆒个型别)来进 行多载化决议程序(overloaded resolution)。㆘面定义五个 classes,代表五种迭 代器类型: // 五个 struct struct struct struct 做为标记用的型别(tag types) input_iterator_tag { }; output_iterator_tag { }; forward_iterator_tag : public input_iterator_tag { }; bidirectional_iterator_tag : public forward_iterator_tag { }; struct random_access_iterator_tag : public bidirectional_iterator_tag { }; 这些 classes 只做为标记用,所以不需要任何成员。至于为什么运用继承机制,稍 后再解释。现在重新设计 __advance()(由于只在内部使用,所以函式名称加㆖ 特定的前导符),并加㆖第㆔参数,使它们形成多载化: template inline void __advance(InputIterator& i, Distance n, input_iterator_tag) { // 单向,逐㆒前进 while (n--) ++i; } // 这是㆒个单纯的转呼叫函式(trivial forwarding function)。稍后讨论如何免除之。 template inline void __advance(ForwardIterator& i, Distance n, forward_iterator_tag) { // 单纯㆞进行转呼叫(forwarding) advance(i, n, input_iterator_tag()); } template inline void __advance(BidiectionalIterator& i, Distance n, bidirectional_iterator_tag) { // 双向,逐㆒前进 if (n >= 0) while (n--) ++i; else while (n++) --i; } template inline void __advance(RandomAccessIterator& i, Distance n, random_access_iterator_tag) The Annotated STL Sources 3 96 第 3 章 迭代器(iterators)概念与 traits 编程技法 { // 双向,跳跃前进 i += n; } 注意㆖述语法,每个 __advance() 的最后㆒个参数都只宣告型别,并未指定参 数名称,因为它纯粹只是用来启动多载化机制,函式之㆗根本不使用该参数。如 果硬要加㆖参数名称也可以,画蛇添足罢了。 行进至此,还需要㆒个对外开放的㆖层控制接口,呼叫㆖述各个多载化的 __advance() 。 此 ㆒ ㆖ 层 介 面 只 需 两 个 参 数 , 当 它 准 备 将 工 作 转 给 ㆖ 述 的 __advance() 时,才自行加㆖第㆔自变量:迭代器类型。因此,这个㆖层函式必须 有能力从它所获得的迭代器㆗推导出其类型 — 这份工作自然是交给 traits 机制: template inline void advance(InputIterator& i, Distance n) { __advance(i, n, iterator_traits::iterator_category()); } 注意 ㆖ 述 语 法 ,iterator_traits::iterator_category() 将 产 生 ㆒个暂时对象(道理就像 int() 会产生㆒个 int 暂时对象㆒样),其型别应该隶 属前述五个迭代器 类型之㆒。然后,根据这个型别,编译器才决定呼叫哪㆒个 __advance() 多载函式。 3 关于此行,SGI STL 的源码是: __advance(i, n, iterator_category(i)); 并另定义函式 iterator_category() 如㆘: template inline typename iterator_traits::iterator_category iterator_category(const I&) { typedef typename iterator_traits::iterator_category category; return category(); } 综合整理后原式即为: __advance(i, n, iterator_traits::iterator_category()); The Annotated STL Sources 3.4 Traits 编程技法 — STL 源码门钥 97 因此,为了满足㆖述行为,traits 必须再增加㆒个相应型别: template struct iterator_traits { ... typedef typename I::iterator_category iterator_category; }; // 针对原生指标而设计的「偏特化版(partial specialization)」 template struct iterator_traits { ... // 注意,原生指标是㆒种 Random Access Iterator typedef random_access_iterator_tag iterator_category; }; // 针对原生的 pointer-to-const 而设计的「偏特化版(partial specialization)」 template struct iterator_traits ... // 注意,原生的 pointer-to-const 是㆒种 Random Access Iterator typedef random_access_iterator_tag iterator_category; }; 任何㆒个迭代器,其类型永远应该落在「该迭代器所隶属之各种类型㆗,最强化 的那个」。例如 int* 既是 Random Access Iterator 又是 Bidirectional Iterator, 同时也是 Forward Iterator,而且也是 Input Iterator,那么,其类型应该归属为 random_access_iterator_tag。 你是否注意到 advance() 的 template 参数名称取得好像不怎么理想: template inline void advance(InputIterator& i, Distance n); 按说 advanced() 既然可以接受各种类型的迭代器,就不应将其型别参数命名为 InputIterator。这其实是 STL 算法的㆒个命名规则:以算法所能接受之最 低阶迭代器类型,来为其迭代器型别参数命名。 消除「单纯转呼叫函式」 以 class 来定义迭代器的各种分类标签,不唯可以促成多载化机制的成功运作(使 编译器得以正确执行多载化决议程序,overloaded resolution),另㆒个好处是, The Annotated STL Sources 98 第 3 章 迭代器(iterators)概念与 traits 编程技法 透过继承,我们可以不必再写「单纯只做转呼叫」的函式(例如前述的 advance() ForwardIterator 版)。为什么能够如此?考虑㆘面这个小例子,从其输出结果可 以看出端倪: B D1 D2 模拟 InputIterator 模拟 ForwardIterator 模拟 BidirectionalIterator 图 3-3 类别继承关系 // file: 3tag-test.cpp // 模拟测试 tag types 继承关系所带来的影响。 #include using namespace std; struct B { }; struct D1 : public B { }; struct D2 : public D1 { }; // B 可比拟为 InputIterator // D1 可比拟为 ForwardIterator // D2 可比拟为 BidirectionalIterator template func(I& p, B) { cout << "B version" << endl; } template func(I& p, D2) { cout << "D2 version" << endl; int main() { int* p; } func(p, B()); // 参数与自变量完全吻合。输出: "B version" func(p, D1()); // 参数与自变量未能完全吻合;因继承关系而自动转呼叫。 // 输出:"B version" func(p, D2()); // 参数与自变量完全吻合。输出: "D2 version" } 以 distance() 为 例 关于「迭代器类型标签」的应用,以㆘再举㆒例。distance() 也是常用的㆒个 The Annotated STL Sources 3.5 std::iterator 的保证 99 迭代器操作函式,用来计算两个迭代器之间的距离。针对不同的迭代器类型,它 可以有不同的计算方式,带来不同的效率。整个设计模式和前述的 advance() 如 出㆒辙: template inline iterator_traits::difference_type __distance(InputIterator first, InputIterator last, input_iterator_tag) { iterator_traits::difference_type n = 0; // 逐㆒累计距离 while (first != last) { ++first; ++n; } return n; } template inline iterator_traits::difference_type __distance(RandomAccessIterator first, RandomAccessIterator last, random_access_iterator_tag) { // 直接计算差距 return last - first; } template inline iterator_traits::difference_type distance(InputIterator first, InputIterator last) { typedef typename iterator_traits::iterator_category category; return __distance(first, last, category()); } 注意,distance() 可接受任何类型的迭代器;其 template 型别参数之所以命名 为 InputIterator,是为了遵循 STL 算法的命名规则:以算法所能接受之 最初级类型来为其迭代器型别参数命名。此外也请注意,由于迭代器类型之间存 在着继承关系,「转呼叫(forwarding)」的行为模式因此自然存在 — 这㆒点我 已在前㆒节讨论过。换句话说,当客端呼叫 distance() 并使用 Output Iterators 或 Forward Iterators 或 Bidirectional Iterators,统统都会转呼叫 Input Iterator 版 的那个 __distance() 函式。 3.5 std::iterator 的保证 为了符合规范,任何迭代器都应该提供五个巢状相应型别,以利 traits 萃取,否 The Annotated STL Sources 100 第 3 章 迭代器(iterators)概念与 traits 编程技法 则便是自外于整个 STL 架构,可能无法与其它 STL 组件顺利搭配。然而写码难 免挂㆒漏万,谁也不能保证不会有粗心大意的时候。如果能够将事情简化,就好 多了。STL 提供了㆒个 iterators class 如㆘,如果每个新设计的迭代器都继承 自它,就保证符合 STL 所需之规范: template struct iterator { typedef Category iterator_category; typedef T value_type; typedef Distance difference_type; typedef Pointer pointer; typedef Reference reference; }; iterator class 不含任何成员,纯粹只是型别定义,所以继承它并不会招致任何 额外负担。由于后㆔个参数皆有默认值,新的迭代器只需提供前两个参数即可。 先前 3.2 节土法炼钢的 ListIter,如果改用正式规格,应该这么写: template struct ListIter : public std::iterator { ... } 总结 设计适当的相应型别(associated types),是迭代器的责任。设计适当的迭代器, 则是容器的责任。唯容器本身,才知道该设计出怎样的迭代器来走访自己,并执 行迭代器该有的各种行为(前进、后退、取值、取用成员…)。至于算法,完 全可以独立于容器和迭代器之外自行发展,只要设计时以迭代器为对外接口就行。 traits 编程技法,大量运用于 STL 实作品㆗。它利用「巢状型别」的写码技巧与 编译器的 template 自变量推导功能,补强 C++ 未能提供的关于型别认证方面的能 力,补强 C++ 不为强型(strong typed)语言的遗憾。了解 traits 编程技法,就 像获得「芝麻开门」口诀㆒样,从此得以㆒窥 STL 源码堂奥。 The Annotated STL Sources 3.6 iterator 源码完整重列 101 3.6 iterator 源码完整重列 由于讨论次序的缘故,先前所列的源码切割散落,有点凌乱。以㆘重新列出 SGI STL 表头档内与本章相关的程序代码。该表头档还有其它内容,是 关于 iostream iterators、inserter iterators 以及 reverse iterators 的实作,将于第 8 章 讨论。 // 节录自 SGI STL // 五种迭代器类型 struct input_iterator_tag {}; struct output_iterator_tag {}; struct forward_iterator_tag : public input_iterator_tag {}; struct bidirectional_iterator_tag : public forward_iterator_tag {}; struct random_access_iterator_tag : public bidirectional_iterator_tag {}; // 为避免写码时挂㆒漏万,自行开发的迭代器最好继承自㆘面这个 std::iterator template struct iterator { typedef Category typedef T typedef Distance typedef Pointer typedef Reference iterator_category; value_type; difference_type; pointer; reference; }; // 「榨汁机」traits template struct iterator_traits { typedef typename Iterator::iterator_category iterator_category; typedef typename Iterator::value_type typedef typename Iterator::difference_type typedef typename Iterator::pointer typedef typename Iterator::reference }; value_type; difference_type; pointer; reference; // 针对原生指标(native pointer)而设计的 traits template struct iterator_traits { typedef random_access_iterator_tag typedef T typedef ptrdiff_t typedef T* typedef T& 偏特化版。 iterator_category; value_type; difference_type; pointer; reference; }; The Annotated STL Sources 102 第 3 章 迭代器(iterators)概念与 traits 编程技法 // 针对原生之 pointer-to-const 而设计的 traits template struct iterator_traits { typedef random_access_iterator_tag typedef T typedef ptrdiff_t typedef const T* typedef const T& 偏特化版。 iterator_category; value_type; difference_type; pointer; reference; }; // 这个函式可以很方便㆞决定某个迭代器的类型(category) template inline typename iterator_traits::iterator_category iterator_category(const Iterator&) { typedef typename iterator_traits::iterator_category category; return category(); } // 这个函式可以很方便㆞决定某个迭代器的 distance type template inline typename iterator_traits::difference_type* distance_type(const Iterator&) { return static_cast::difference_type*>(0); } // 这个函式可以很方便㆞决定某个迭代器的 value type template inline typename iterator_traits::value_type* value_type(const Iterator&) { return static_cast::value_type*>(0); } // 以㆘是整组 distance 函式 template inline iterator_traits::difference_type __distance(InputIterator first, InputIterator last, input_iterator_tag) { iterator_traits::difference_type n = 0; while (first != last) { ++first; ++n; } return n; } template inline iterator_traits::difference_type __distance(RandomAccessIterator first, RandomAccessIterator last, random_access_iterator_tag) { return last - first; The Annotated STL Sources 3.7 SGI STL 的私房菜:__type_traits 103 } template inline iterator_traits::difference_type distance(InputIterator first, InputIterator last) { typedef typename iterator_traits::iterator_category category; return __distance(first, last, category()); } // 以㆘是整组 advance 函式 template inline void __advance(InputIterator& i, Distance n, input_iterator_tag) { while (n--) ++i; } template inline void __advance(BidirectionalIterator& i, Distance n, bidirectional_iterator_tag) { if (n >= 0) while (n--) ++i; else while (n++) --i; } template inline void __advance(RandomAccessIterator& i, Distance n, random_access_iterator_tag) { i += n; } template inline void advance(InputIterator& i, Distance n) { __advance(i, n, iterator_category(i)); } 3.7 SGI STL 的私房菜:__type_traits traits 编程技法很棒,适度弥补了 C++ 语言本身的不足。STL 只对迭代器加以 规范,制定出 iterator_traits 这样的东西。SGI 把这种技法进㆒步扩大到迭 代器以外的世界,于是有了所谓的 __type_traits。双底线前缀词意指这是 SGI STL 内部所用的东西,不在 STL 标准规范之内。 iterator_traits 负 责 萃 取 迭 代 器 的 特 性 ,__type_traits 则 负 责 萃 取 型 别 The Annotated STL Sources 104 第 3 章 迭代器(iterators)概念与 traits 编程技法 (type)的特性。此处我们所关注的型别特性是指:这个型别是否具备 non-trivial defalt ctor ? 是 否 具 备 non-trivial copy ctor ? 是 否 具 备 non-trivial assignment operator?是否具备 non-trivial dtor?如果答案是否定的,我们在对这个型别进行 建构、解构、拷贝、赋值等动作时,就可以采用最有效率的措施(例如根本不唤 起 尸 位 素 餐 的 那 些 constructor, destructor ) , 而 采 用 记 忆 体 直 接 处 理 动 作 如 malloc()、memcpy() 等等,获得最高效率。这对于大规模而动作频繁的容器, 有着显著的效率提升4。 定义于 SGI ㆗的 __type_traits,提供了㆒种机制,允许针 对不 同 的型 别属 性 ( type attributes) ,在 编译 时 期 完成函 式 派 送决 定( function dispatch)。这对于撰写 template 很有帮助,例如,当我们准备对㆒个「元素型别 未知」的数组执行 copy 动作时,如果我们能事先知道其元素型别是否有㆒个 trivial copy constructor , 便 能 够 帮 助 我 们 决 定 是 否 可 使 用 快 速 的 memcpy() 或 memmove()。 从 iterator_traits 得 来 的 经 验 , 我 们 希 望 , 程 式 之 ㆗ 可 以 这 样 运 用 __type_traits,T 代表任意型别: __type_traits::has_trivial_default_constructor __type_traits::has_trivial_copy_constructor __type_traits::has_trivial_assignment_operator __type_traits::has_trivial_destructor __type_traits::is_POD_type // POD : Plain Old Data 我们希望㆖述式子响应我们「真」或「假」(以便我们决定采取什么策略),但 其结果不应该只是个 bool 值,应该是个有着真/假性质的「对象」,因为我们希 望利用其响应结果来进行自变量推导,而编译器只有面对 class object 形式的自变量, 才会做自变量推导。为此,㆖述式子应该传回这样的东西: struct __true_type { }; struct __false_type { }; 这两个空白 classes 没有任何成员,不会带来额外负担,却又能够标示真假,满足 我们所需。 4 C++ Type Traits, by John Maddock and Steve Cleary, DDJ 2000/10 提了㆒些测试数据。 The Annotated STL Sources 3.7 SGI STL 的私房菜:__type_traits 105 为了 达 成 ㆖述 五 个 式 子,__type_traits 内 必 须定 义 ㆒ 些 typedefs , 其 值 不 是 __true_type 就是 __false_type。㆘面是 SGI 的作法: template struct __type_traits typedef __true_type this_dummy_member_must_be_first; /* 不要移除这个成员。它通知「有能力自动将 __type_traits 特化」 的编译器说,我们现在所看到的这个 __type_traits template 是特 殊的。这是为了确保万㆒编译器也使用㆒个名为 __type_traits 而其 实与此处定义并无任何关联的 template 时,所有事情都仍将顺利运作。 */ /* 以㆘条件应被遵守,因为编译器有可能自动为各型别产生专属的 __type_traits 特化版本: - 你可以重新排列以㆘的成员次序 - 你可以移除以㆘任何成员 - 绝对不可以将以㆘成员重新命名而却没有改变编译器㆗的对应名称 - 新加入的成员会被视为㆒般成员,除非你在编译器㆗加㆖适当支持。*/ typedef typedef typedef typedef typedef __false_type __false_type __false_type __false_type __false_type has_trivial_default_constructor; has_trivial_copy_constructor; has_trivial_assignment_operator; has_trivial_destructor; is_POD_type; }; 为什么 SGI 把所有巢状型别都定义为 __false _type 呢?是的,SGI 定义出最 保守的值,然后(稍后可见)再针对每㆒个纯量型别(scalar types)设计适当的 __type_traits 特化版本,这样就解决了问题。 ㆖述 __type_traits 可以接受任何型别的自变量,五个 typedefs 将经由以㆘管道 获得实值: ㆒般具现体(general instantiation),内含对所有型别都必定有效的保守值。 ㆖述各个 has_trivial_xxx 型别都被定义为 __false_type,就是对所有 型别都必定有效的保守值。 经 过 宣 告 的 特 化 版 本 , 例 如 内 对 所 有 C++ 纯 量 型 别 (scalar types)提供了对映的特化宣告。稍后展示。 某些编译器(如 Silicon Graphics N32 和 N64 编译器)会自动为所有型别提 供适当的特化版本。(这真是了不起的技术。不过我对其精确程度存疑) The Annotated STL Sources 106 第 3 章 迭代器(iterators)概念与 traits 编程技法 以㆘便是 对所有 C++ 纯量型别所定义的 __type_traits 特 化版本。这些定义对于内建有 __types_traits 支持能力的编译器(例如 Silicon Graphics N32 和 N64)并无伤害,对于无该等支持能力的编译器而言,则属必要。 /* 以㆘针对 C++ 基本型别 char, signed char, unsigned char, short, unsigned short, int, unsigned int, long, unsigned long, float, double, long double 提供特化版本。注意,每㆒个成员的值都是 __true_type,表示这些 型别都可采用最快速方式(例如 memcpy)来进行拷贝(copy)或赋值(assign)动 作。*/ // 注意,SGI STL 将以㆘出现的 __STL_TEMPLATE_NULL // 定义为 template<>,见 1.9.1 节,是所谓的 // class template explicit specialization __STL_TEMPLATE_NULL struct __type_traits { typedef __true_type has_trivial_default_constructor; typedef __true_type has_trivial_copy_constructor; typedef __true_type has_trivial_assignment_operator; typedef __true_type has_trivial_destructor; typedef __true_type is_POD_type; }; __STL_TEMPLATE_NULL struct __type_traits { typedef __true_type has_trivial_default_constructor; typedef __true_type has_trivial_copy_constructor; typedef __true_type has_trivial_assignment_operator; typedef __true_type has_trivial_destructor; typedef __true_type is_POD_type; }; __STL_TEMPLATE_NULL struct __type_traits { typedef __true_type has_trivial_default_constructor; typedef __true_type has_trivial_copy_constructor; typedef __true_type has_trivial_assignment_operator; typedef __true_type has_trivial_destructor; typedef __true_type is_POD_type; }; __STL_TEMPLATE_NULL struct __type_traits { typedef __true_type has_trivial_default_constructor; typedef __true_type has_trivial_copy_constructor; typedef __true_type has_trivial_assignment_operator; typedef __true_type has_trivial_destructor; typedef __true_type is_POD_type; }; __STL_TEMPLATE_NULL struct __type_traits { typedef __true_type has_trivial_default_constructor; The Annotated STL Sources 3.7 SGI STL 的私房菜:__type_traits 107 typedef typedef typedef typedef __true_type __true_type __true_type __true_type has_trivial_copy_constructor; has_trivial_assignment_operator; has_trivial_destructor; is_POD_type; }; __STL_TEMPLATE_NULL struct __type_traits { typedef __true_type has_trivial_default_constructor; typedef __true_type has_trivial_copy_constructor; typedef __true_type has_trivial_assignment_operator; typedef __true_type has_trivial_destructor; typedef __true_type is_POD_type; }; __STL_TEMPLATE_NULL struct __type_traits { typedef __true_type has_trivial_default_constructor; typedef __true_type has_trivial_copy_constructor; typedef __true_type has_trivial_assignment_operator; typedef __true_type has_trivial_destructor; typedef __true_type is_POD_type; }; __STL_TEMPLATE_NULL struct __type_traits { typedef __true_type has_trivial_default_constructor; typedef __true_type has_trivial_copy_constructor; typedef __true_type has_trivial_assignment_operator; typedef __true_type has_trivial_destructor; typedef __true_type is_POD_type; }; __STL_TEMPLATE_NULL struct __type_traits { typedef __true_type has_trivial_default_constructor; typedef __true_type has_trivial_copy_constructor; typedef __true_type has_trivial_assignment_operator; typedef __true_type has_trivial_destructor; typedef __true_type is_POD_type; }; __STL_TEMPLATE_NULL struct __type_traits { typedef __true_type has_trivial_default_constructor; typedef __true_type has_trivial_copy_constructor; typedef __true_type has_trivial_assignment_operator; typedef __true_type has_trivial_destructor; typedef __true_type is_POD_type; }; __STL_TEMPLATE_NULL struct __type_traits { typedef __true_type has_trivial_default_constructor; typedef __true_type has_trivial_copy_constructor; The Annotated STL Sources 108 typedef __true_type typedef __true_type typedef __true_type 第 3 章 迭代器(iterators)概念与 traits 编程技法 has_trivial_assignment_operator; has_trivial_destructor; is_POD_type; }; __STL_TEMPLATE_NULL struct __type_traits { typedef __true_type has_trivial_default_constructor; typedef __true_type has_trivial_copy_constructor; typedef __true_type has_trivial_assignment_operator; typedef __true_type has_trivial_destructor; typedef __true_type is_POD_type; }; // 注意,以㆘针对原生指标设计 __type_traits 偏特化版本。 // 原生指标亦被视为㆒种纯量型别。 template struct __type_traits { typedef __true_type has_trivial_default_constructor; typedef __true_type has_trivial_copy_constructor; typedef __true_type has_trivial_assignment_operator; typedef __true_type has_trivial_destructor; typedef __true_type is_POD_type; }; __types_traits 在 SGI STL ㆗的应用很广。㆘面我举几个实例。第㆒个例子是 出现于本书 2.3.3 节的 uninitialized_fill_n() 全域函式: template inline ForwardIterator uninitialized_fill_n(ForwardIterator first, Size n, const T& x) { return __uninitialized_fill_n(first, n, x, value_type(first)); } 此函式以 x 为蓝本,自迭代器 first 开始建构 n 个元素。为求取最大效率,首 先 以 value_type() ( 3.6 节 ) 萃 取 出 迭 代 器 first 的 value type , 再 利 用 __type_traits 判断该型别是否为 POD 型别: template inline ForwardIterator __uninitialized_fill_n(ForwardIterator first, Size n, const T& x, T1*) { typedef typename __type_traits::is_POD_type is_POD; return __uninitialized_fill_n_aux(first, n, x, is_POD()); } 以㆘就「是否为 POD 型别」采取最适当的措施: The Annotated STL Sources 3.7 SGI STL 的私房菜:__type_traits 109 // 如果不是 POD 型别,就会派送(dispatch)到这里 template ForwardIterator __uninitialized_fill_n_aux(ForwardIterator first, Size n, const T& x, __false_type) { ForwardIterator cur = first; // 为求阅读顺畅简化,以㆘将原本有的异常处理(exception handling)去除。 for ( ; n > 0; --n, ++cur) construct(&*cur, x); // 见 2.2.3 节 return cur; } // 如果是 POD 型别,就会派送(dispatch)到这里。㆘两行是原档所附注解。 // 如果 copy construction 等同于 assignment,而且有 trivial destructor, // 以㆘就有效。 template inline ForwardIterator __uninitialized_fill_n_aux(ForwardIterator first, Size n, const T& x, __true_type) { return fill_n(first, n, x); // 交由高阶函式执行,如㆘所示。 } // 以㆘是定义于 ㆗的 fill_n() template OutputIterator fill_n(OutputIterator first, Size n, const T& value) { for ( ; n > 0; --n, ++first) *first = value; return first; } 第㆓个例子是负责对象解构的 destroy() 全域函式。此函式之源码及解说在 2.2.3 节有完整的说明。 第㆔个例子是出现于本书第 6 章的 copy() 全域函式(泛型算法之㆒)。这个 函式有非常多的特化(specialization)与强化(refinement)版本,殚精竭虑,全 都是为了效率考虑,希望在适当的情况㆘采用最「雷霆万钧」的手段。最基本的 想法是这样: // 拷贝㆒个数组,其元素为任意型别,视情况采用最有效率的拷贝手段。 template inline void copy(T* source,T* destination,int n) { copy(source,destination,n, typename __type_traits::has_trivial_copy_constructor()); } // 拷贝㆒个数组,其元素型别拥有 non-trivial copy constructors。 The Annotated STL Sources 110 第 3 章 迭代器(iterators)概念与 traits 编程技法 template void copy(T* source,T* destination,int n, __false_type) { ... } // 拷贝㆒个数组,其元素型别拥有 trivial copy constructors。 // 可借助 memcpy() 完成工作 template void copy(T* source,T* destination,int n, __true_type) { ... } 以㆖只是针对「函式参数为原生指标」的情况而做的设计。第 6 章的 copy() 演 算法是个泛型版本,情况又复杂许多。详见 6.4.3 节。 请注意, 并未像其它许多 SGI STL 表头档有这样的声明: /* NOTE: This is an internal header file, included by other STL headers. * You should not attempt to use it directly. */ 因 此 如 果 你 是 SGI STL 的 使 用 者 , 你 可 以 在 自 己 的 程 式 ㆗ 充 份 运 用 这 个 __type_traits。假设我自行定义了㆒个 Shape class,__type_traits 会对它 产生什么效应?如果编译器够厉害(例如 Silicon Graphics 的 N32 和 N64 编译 器),你会发现,__type_traits 针对 Shape 萃取出来的每㆒个特性,其结果 将 取 决 于 我 的 Shape 是 否 有 trivial defalt ctor 或 trivial copy ctor 或 trivial assignment operator 或 trivial dtor 而定。但对大部份缺乏这种特异功能的编译 器而言,__type_traits 针对 Shape 萃取出来的每㆒个特性都是 __false_type , 即使 Shape 是个 POD 型别。这样的结果当然过于保守,但是别无选择,除非我 针对 Shape,自行设计㆒个 __type_traits 特化版本,明白㆞告诉编译器以㆘事 实(举例): template<> typedef typedef typedef typedef typedef struct __type_traits { __true_type has_trivial_default_constructor; __false_type has_trivial_copy_constructor; __false_type has_trivial_assignment_operator; __false_type has_trivial_destructor; __false_type is_POD_type; }; 究竟㆒个 class 什么时候该有自己的 non-trivial default constructor, non-trivial copy constructor, non-trivial assignment operator, non-trivial destructor 呢?㆒个简单的判 The Annotated STL Sources 3.7 SGI STL 的私房菜:__type_traits 111 断准则是:如果 class 内含指标成员,并且对它进行内存动态配置,那么这个 class 就需要实作出自己的 non-trivial-xxx5。 即使你无法全面针对你自己定义的型别,设计__type_traits 特化版本,无论如 何, 至少,有了这个 __type_traits 之后,当我们设计新的泛型算法时,面 对 C++ 纯量型别,便有足够的信息决定采用最有效的拷贝动作或赋值动作 — 因 为每㆒个纯量型别都有对应的 __type_traits 特化版本,其㆗每㆒个 typedef 的 值都是 __true_type。 5 请参考 [Meyers98] 条款 11: Declare a copy constructor and an assignment operator for classes with dynamically allocated memory, 以及条款 45:Know what functions C++ silently writes and calls. The Annotated STL Sources 112 第 3 章 迭代器(iterators)概念与 traits 编程技法 The Annotated STL Sources 4.1 容器的概观与分类 113 4 序列式容器 sequence containers 4.1 容器的概观与分类 容器,置物之所也。 研究数据的特定排列方式,以利搜寻或排序或其它特殊目的,这㆒专门学科我们 称为数据结构(Data Structures)。大学信息相关教育里头,与编程最有直接关系 的科目,首推数据结构与算法(Algorithms)。几乎可以说,任何特定的数据结 构都是为了实现某种特定的算法。STL 容器即是将运用最广的㆒些数据结构实 作出来(图 4-1)。未来,在每五年召开㆒次的 C++ 标准委员会㆗,STL 容器的 数量还有可能增加。 众所周知,常用的数据结构不外乎 array(数组)、list(串行)、tree(树)、stack (堆栈)、queue(队列)、hash table(杂凑表)、set(集合)、map(映像表)… 等等。根据「资料在容器㆗的排列」特性,这些数据结构分为序列式(sequence) 和关系型(associative)两种。本章探讨序列式容器,㆘㆒章探讨关系型容器。 容器是大多数㆟对 STL 的第㆒印象,这说明了容器的好用与受欢迎。容器也是许 多㆟对 STL 的唯㆒印象,这说明了还有多少㆟利器(STL)在手而未能善用。 The Annotated STL Sources 114 序列式容器 Sequence Containers 第 4 章 序列式容器(sequence containers) 关系型容器 Associative Containers array(build-in) vector heap C++ 内建 以算法型式 呈现(xxx_heap) RB-tree set map 非公开 priority-queue list multiset multimap slist deque stack 非标准 配接器 hashtable hash_set hash_map 非标准 非标准 非标准 queue 配接器 hash_multiset 非标准 hash_multimap 非标准 图 4-1 SGI STL 的各种容器。本图以内缩方式来表达基层与衍生层的关系。 这里所谓的衍生,并非继承(inheritance)关系,而是内含(containment)关系。 例如 heap 内含㆒个 vector,priority-queue 内含㆒个 heap,stack 和 queue 都含㆒个 deque,set/map/multiset/multimap 都内含㆒个 RB-tree,hast_x 都内含㆒个 hashtable。 4.1.1 序列式容器 ( sequential containers) 所谓序列式容器,其㆗的元素都可序(ordered),但未排序(sorted)。C++ 语 言本身提供了㆒个序列式容 器 array,STL 另外再提供 vector, list, deque, stack, queue, priority-queue 等等序列式容器。其㆗ stack 和 queue 由于 只是将 deque 改头换面而成,技术㆖被归类为㆒种配接器(adapter),但我仍 把它们放在本章讨论。 本章将带你仔细看过各种序列式容器的关键实作细节。 The Annotated STL Sources 4.2 4.2 vector vector 115 4.2.1 vector 概述 vector 的数据安排以及操作方式,与 array 非常像似。两者的唯㆒差别在于空 间的运用弹性。array 是静态空间,㆒旦配置了就不能改变;要换个大(或小) ㆒点的房子,可以,㆒切细琐得由客端自己来:首先配置㆒块新空间,然后将元 素从旧址㆒㆒搬往新址,然后再把原来的空间释还给系统。vector 是动态空间, 随着元素的加入,它的内部机制会自行扩充空间以容纳新元素。因此,vector 的 运用对于内存的樽节与运用弹性有很大的帮助,我们再也不必因为害怕空间不 足而㆒开始就要求㆒个大块头 array 了,我们可以安心使用 vector,吃多少用 多少。 vector 的实作技术,关键在于其对大小的控制以及重新配置时的数据搬移效率。 ㆒旦 vector 旧有空间满载,如果客端每新增㆒个元素,vector 内部只是扩充㆒ 个元素的空间,实为不智,因为所谓扩充空间(不论多大),㆒如稍早所说,是 「配置新空间 / 数据搬移 / 释还旧空间」的大工程,时间成本很高,应该加入某 种未雨绸缪的考虑。稍后我们便可看到 SGI vector 的空间配置策略。 4.2.2 vector 定义式摘要 以㆘是 vector 定义式的源码摘录。虽然 STL 规定,欲使用 vector 者必须先 含入 ,但 SGI STL 将 vector 实作于更底层的 。 // alloc 是 SGI STL 的空间配置器,见第㆓章。 template class vector { public: // vector 的巢状型别定义 typedef T value_type; typedef value_type* pointer; typedef value_type* iterator; typedef value_type& reference; typedef size_t typedef ptrdiff_t size_type; difference_type; protected: The Annotated STL Sources 116 第 4 章 序列式容器(sequence containers) // 以㆘,simple_alloc 是 SGI STL 的空间配置器,见 2.2.4 节。 typedef simple_alloc data_allocator; iterator start; iterator finish; // 表示目前使用空间的头 // 表示目前使用空间的尾 iterator end_of_storage; // 表示目前可用空间的尾 void insert_aux(iterator position, const T& x); void deallocate() { if (start) data_allocator::deallocate(start, end_of_storage - start); } void fill_initialize(size_type n, const T& value) { start = allocate_and_fill(n, value); finish = start + n; end_of_storage = finish; } public: iterator begin() { return start; } iterator end() { return finish; } size_type size() const { return size_type(end() - begin()); } size_type capacity() const { return size_type(end_of_storage - begin()); } bool empty() const { return begin() == end(); } reference operator[](size_type n) { return *(begin() + n); } vector() : start(0), finish(0), end_of_storage(0) {} vector(size_type n, const T& value) { fill_initialize(n, value); } vector(int n, const T& value) { fill_initialize(n, value); } vector(long n, const T& value) { fill_initialize(n, value); } explicit vector(size_type n) { fill_initialize(n, T()); } ~vector() } destroy(start, finish); deallocate(); // 全域函式,见 2.2.3 节。 // 这是 vector 的㆒个 member function reference front() { return *begin(); } reference back() { return *(end() - 1); } // 第㆒个元素 // 最后㆒个元素 void push_back(const T& x) { if (finish != end_of_storage) { // 将元素安插至最尾端 construct(finish, x); ++finish; } else insert_aux(end(), x); } void pop_back() { // 全域函式,见 2.2.3 节。 // 这是 vector 的㆒个 member function // 将最尾端元素取出 The Annotated STL Sources 4.2 vector --finish; destroy(finish); } // 全域函式,见 2.2.3 节。 117 iterator erase(iterator position) { if (position + 1 != end()) // 清除某位置㆖的元素 copy(position + 1, finish, position); --finish; // 后续元素往前搬移 destroy(finish); // 全域函式,见 2.2.3 节。 return position; } void resize(size_type new_size, const T& x) { if (new_size < size()) erase(begin() + new_size, end()); else insert(end(), new_size - size(), x); } void resize(size_type new_size) { resize(new_size, T()); } void clear() { erase(begin(), end()); } protected: // 配置空间并填满内容 iterator allocate_and_fill(size_type n, const T& x) { iterator result = data_allocator::allocate(n); uninitialized_fill_n(result, n, x); // 全域函式,见 2.3 节 return result; } 4.2.3 vector 的迭代器 vector 维护的是㆒个连续线性空间,所以不论其元素型别为何,原生指标都可以 做为 vector 的迭代器而满足所有必要条件,因为 vector 迭代器所需要的操作行 为如operator*,operator->,operator++,operator--,operator+, operator-, operator+=,operator-=,原生指标㆝生就具备。vector 支援随机存取,而原 生指标正有着这样的能力。所以,vector 提供的是 Random Access Iterators。 template class vector { public: typedef T value_type; typedef value_type* iterator; // vector 的迭代器是原生指标 ... }; 根据㆖述定义,如果客端写出这样的码: The Annotated STL Sources 118 第 4 章 序列式容器(sequence containers) vector::iterator ivite; vector::iterator svite; ivite 的型别其实就是 int*,svite 的型别其实就是 Shape*。 4.2.4 vector 的数据结构 vector 所采用的数据结构非常简单:线性连续空间。它以两个迭代器 start 和 finish 分 别 指 向 配 置 得 来 的 连 续 空 间 ㆗ 目 前 已 被 使 用 的 范 围 , 并 以 迭 代 器 end_of_storage 指向整块连续空间(含备用空间)的尾端: template class vector { ... protected: iterator start; iterator finish; // 表示目前使用空间的头 // 表示目前使用空间的尾 iterator end_of_storage; // 表示目前可用空间的尾 ... }; 为了降低空间配置时的速度成本,vector 实际配置的大小可能比客端需求量更大 ㆒些,以备将来可能的扩充。这便是容量(capacity)的观念。换句话说㆒个 vector 的容量永远大于或等于其大小。㆒旦容量等于大小,便是满载,㆘次再有新增元 素,整个 vector 就得另觅居所。见图 4-2。 运用 start, finish, end_of_storage ㆔个迭代器,便可轻易提供首尾标示、 大小、容量、空容器判断、注标([ ])运算子、最前端元素值、最后端元素值… 等机能: template class vector { ... public: iterator begin() { return start; } iterator end() { return finish; } size_type size() const { return size_type(end() - begin()); } size_type capacity() const { return size_type(end_of_storage - begin()); } bool empty() const { return begin() == end(); } reference operator[](size_type n) { return *(begin() + n); } reference front() { return *begin(); } The Annotated STL Sources 4.2 vector 119 reference back() { return *(end() - 1); } ... }; 9 9 start 经过以㆘动作: vector iv(2, 9); size( ) capacity( ) 1 2 3 4 备 用 operator[3] finish end_of_storage iv.push_back(1); iv.push_back(2); iv.push_back(3); iv.push_back(4); vector 内存及各成员呈现 左图状态 增加新元素(s)时,如果超过当时的容量,则容量 会扩充至两倍。如果两倍容量仍不足,就扩张至足 够大的容量。 注意,本图系直接在原空间之后画㆖新增空间,其 实没那么单纯。容量的扩张必须经历「重新配置、 元素搬移、释放原空间」等过程,工程浩大。 图 4-2 vector 示意图 4.2.5 vector 的建构与内存管理:constructor, push_back 千头万绪该如何说起?以客端程序代码为引导,观察其所得结果并实证源码,是个 良好的学习路径。㆘面是个小小的测试程序,我的观察重点在建构的方式、元素 的添加,以及大小、容量的变化: // filename : 4vector-test.cpp #include #include #include The Annotated STL Sources 120 using namespace std; int main() { int i; vector iv(2,9); 第 4 章 序列式容器(sequence containers) cout << "size=" << iv.size() << endl; cout << "capacity=" << iv.capacity() << endl; iv.push_back(1); cout << "size=" << iv.size() << endl; cout << "capacity=" << iv.capacity() << endl; iv.push_back(2); cout << "size=" << iv.size() << endl; cout << "capacity=" << iv.capacity() << endl; iv.push_back(3); cout << "size=" << iv.size() << endl; cout << "capacity=" << iv.capacity() << endl; iv.push_back(4); cout << "size=" << iv.size() << endl; cout << "capacity=" << iv.capacity() << endl; for(i=0; i::start last vector::finish vector::end_of_storage erase(first, last); 之后 first vector vector::start last vector::finish vector::end_of_storage 图 4-3a 局部区间的清除动作:erase(first,last) ㆘面是 vector::insert() 实作内容: // 从 position 开始,安插 n 个元素,元素初值为 x template The Annotated STL Sources 4.2 vector 125 void vector::insert(iterator position, size_type n, const T& x) { if (n != 0) { // 当 n != 0 才进行以㆘所有动作 if (size_type(end_of_storage - finish) >= n) // 备用空间大于等于「新增元素个数」 T x_copy = x; // 以㆘计算安插点之后的现有元素个数 const size_type elems_after = finish - position; iterator old_finish = finish; if (elems_after > n) // 「安插点之后的现有元素个数」大于「新增元素个数」 uninitialized_copy(finish - n, finish, finish); finish += n; // 将 vector 尾端标记后移 copy_backward(position, old_finish - n, old_finish); fill(position, position + n, x_copy); // 从安插点开始填入新值 } else { // 「安插点之后的现有元素个数」小于等于「新增元素个数」 uninitialized_fill_n(finish, n - elems_after, x_copy); finish += n - elems_after; uninitialized_copy(position, old_finish, finish); finish += elems_after; fill(position, old_finish, x_copy); } } else { // 备用空间小于「新增元素个数」(那就必须配置额外的内存) // 首先决定新长度:旧长度的两倍,或旧长度+新增元素个数。 const size_type old_size = size(); const size_type len = old_size + max(old_size, n); // 以㆘配置新的 vector 空间 iterator new_start = data_allocator::allocate(len); iterator new_finish = new_start; __STL_TRY { // 以㆘首先将旧 vector 的安插点之前的元素复制到新空间。 new_finish = uninitialized_copy(start, position, new_start); // 以㆘再将新增元素(初值皆为 n)填入新空间。 new_finish = uninitialized_fill_n(new_finish, n, x); // 以㆘再将旧 vector 的安插点之后的元素复制到新空间。 new_finish = uninitialized_copy(position, finish, new_finish); } # ifdef __STL_USE_EXCEPTIONS catch(...) { // 如有异常发生,实现 "commit or rollback" semantics. destroy(new_start, new_finish); data_allocator::deallocate(new_start, len); throw; } # endif /* __STL_USE_EXCEPTIONS */ The Annotated STL Sources 126 第 4 章 序列式容器(sequence containers) // 以㆘清除并释放旧的 vector destroy(start, finish); deallocate(); // 以㆘调整水位标记 start = new_start; finish = new_finish; end_of_storage = new_start + len; } } } 注意,安插完成后,新节点将位于标兵迭代器(㆖例之 position,标示出安插点) 所指之节点的前方 — 这是 STL 对于「安插动作」的标准规范。图 4-3b 展 示 insert(position,n,x) 的动作。 insert(position,n,x); (1) 备用空间 2≥ 新增元素个数 2 例:㆘图,n==2 (1-1)安插点之后的现有元素个数 3 > 新增元素个数 2 vector 安插点 position 1 uninitialized_copy() 备用空间 start 安插点 finish end_of_storage vector position 3 copy_backward() start vector 安插点 position x x finish 2 end_of_storage start 4 fill() finish end_of_storage 图 4-3b-1 insert(position,n,x) 状况 1 The Annotated STL Sources 4.2 vector (1-2)安插点之后的现有元素个数 2≤ 新增元素个数 3 安插点 position vector 备用空间 127 start finish end_of_storage vector 安插点 position 1 uninitialized_fill_n() x start 安插点 finish 2 end_of_storage vector position 3 uninitialized_copy() x start 安插点 position finish 4 end_of_storage vector x x x start 5 fill() finish end_of_storage 图 4-3b-2 insert(position,n,x) 状况 2 The Annotated STL Sources 128 insert(position,n,x); (2) 备用空间 新增元素个数 例:㆘图,n==3 安插点 position vector 第 4 章 序列式容器(sequence containers) 备用空间 start 2 finish end_of_storage uninitialized_copy() 安插点 position 4 uninitialized_copy() 1 配置 新空间 vector x x x start 3 uninitialized_fill_n() finish end_of_storage 图 4-3b-3 insert(position,n,x) 状况 3 4.3 4.3.1 list list 概述 相较于 vector 的连续线性空间,list 就显得复杂许多,它的好处是每次安插 或删除㆒个元素,就配置或释放㆒个元素空间。因此,list 对于空间的运用有绝 对的精准,㆒点也不浪费。而且,对于任何位置的元素安插或元素移除,list 永 远是常数时间。 list 和 vector 是两个最常被使用的容器。什么时机㆘最适合使用哪㆒种容器, 必须视元素的多寡、元素的构造复杂度(有无 non-trivial copy constructor, non-trivial copy assignmen operator)、元素存取行为的特性而定。[Lippman 98] 6.3 节对这两 种容器提出了㆒份测试报告。 The Annotated STL Sources 4.3 list 129 4.3.2 list 的节点( node) 每㆒个设计过 list 的㆟都知道,list 本身和 list 的节点是不同的结构,需要分开 设计。以㆘是 STL list 的节点(node)结构: template struct __list_node { typedef void* void_pointer; void_pointer prev; // 型别为 void*。其实可设为 __list_node* void_pointer next; T data; }; 显然这是㆒个双向串行1。 __list_node object prev next data 4.3.3 list 的迭代器 list 不再能够像 vector ㆒样以原生指标做为迭代器,因为其节点不保证在储 存空间㆗连续存在。list 迭代器必须有能力指向 list 的节点,并有能力做正 确的递增、递减、取值、成员存取…等动作。所谓「list 迭代器正确的递增、递 减、取值、成员取用」动作是指,递增时指向㆘㆒个节点,递减时指向㆖㆒个节 点,取值时取的是节点的资料值,成员取用时取用的是节点的成员,如图 4-4。 由于 STL list 是㆒个双向串行(double linked-list),迭代器必须具备前移、后 移的能力。所以 list 提供的是 Bidirectional Iterators。 list 有㆒个重要性质:安插动作(insert)和接合动作(splice)都不会造成原有 的 list 迭代器失效。这在 vector 是不成立的,因为 vector 的安插动作可能 造成记忆体重新配置,导致原有的迭代器全部失效。甚至 list 的元素删除动作 1 SGI STL 另有㆒个单向串行 slist,我将在 4.9 节介绍它。 The Annotated STL Sources 130 第 4 章 序列式容器(sequence containers) (erase),也只有「指向被删除元素」的那个迭代器失效,其它迭代器不受任何 影响。 node -- ++ prev next data operator* prev next data prev next data 图 4-4 list 的节点与 list 的迭代器 以㆘是 list 迭代器的设计: template struct __list_iterator { typedef __list_iterator typedef __list_iterator iterator; self; typedef typedef typedef typedef typedef typedef typedef bidirectional_iterator_tag iterator_category; T value_type; Ptr pointer; Ref reference; __list_node* link_type; size_t size_type; ptrdiff_t difference_type; link_type node; // 迭代器内部当然要有㆒个原生指标,指向 list 的节点 // constructor __list_iterator(link_type x) : node(x) {} __list_iterator() {} __list_iterator(const iterator& x) : node(x.node) {} bool operator==(const self& x) const { return node == x.node; } bool operator!=(const self& x) const { return node != x.node; } // 以㆘对迭代器取值(dereference),取的是节点的资料值。 reference operator*() const { return (*node).data; } // 以㆘是迭代器的成员存取(member access)运算子的标准作法。 The Annotated STL Sources 4.3 list 131 pointer operator->() const { return &(operator*()); } // 对迭代器累加 1,就是前进㆒个节点 self& operator++() node = (link_type)((*node).next); return *this; } self operator++(int) self tmp = *this; ++*this; return tmp; } // 对迭代器递减 1,就是后退㆒个节点 self& operator--() node = (link_type)((*node).prev); return *this; } self operator--(int) self tmp = *this; --*this; return tmp; } }; 4.3.4 list 的数据结构 SGI list 不仅是㆒个双向串行,而且还是㆒个环状双向串行。所以它只需要㆒个 指标,便可以完整表现整个串行: template // 预设使用 alloc 为配置器 class list { protected: typedef __list_node list_node; public: typedef list_node* link_type; protected: link_type node; // 只要㆒个指标,便可表示整个环状双向串行 ... }; 如果让指标 node 指向刻意置于尾端的㆒个空白节点,node 便能符合 STL 对于 「前闭后开」区间的要求,成为 last 迭代器,如图 4-5。这么㆒来,以㆘几个函 式便都可以轻易完成: The Annotated STL Sources 132 第 4 章 序列式容器(sequence containers) iterator begin() { return (link_type)((*node).next); } iterator end() { return node; } bool empty() const { return node->next == node; } size_type size() const { size_type result = 0; distance(begin(), end(), result); // 全域函式,第 3 章。 return result; } // 取头节点的内容(元素值)。 reference front() { return *begin(); } // 取尾节点的内容(元素值)。 reference back() { return *(--end()); } list ilist; 环状双向串行 ilist.end() ilist.node ilist.begin() 4 3 ite= find(ilist.begin(), ilist.end(), 3); 0 正向 1 逆向 2 图 4-5 list 示意图。是环状串行只需㆒个标记,即可完全表示整个串行。只要 刻意在环状串行的尾端加㆖㆒个空白节点,便符合 STL 规范之「前闭后开」区间。 4.3.5 list 的建构与内存管理: constructor, push_back, insert 千头万绪该如何说起?以客端程序代码为引导,观察其所得结果并实证源码,是个 The Annotated STL Sources 4.3 list 133 良好的学习路径。㆘面是㆒个测试程序,我的观察重点在建构的方式以及大小的 变化: // filename : 4list-test.cpp #include #include #include using namespace std; int main() { int i; list ilist; cout << "size=" << ilist.size() << endl; ilist.push_back(0); ilist.push_back(1); ilist.push_back(2); ilist.push_back(3); ilist.push_back(4); cout << "size=" << ilist.size() << endl; // size=0 // size=5 list::iterator ite; for(ite = ilist.begin(); ite != ilist.end(); ++ite) cout << *ite << ' '; cout << endl; ite = find(ilist.begin(), ilist.end(), 3); if (ite!=0) ilist.insert(ite, 99); cout << "size=" << ilist.size() << endl; cout << *ite << endl; // 0 1 2 3 4 // size=6 // 3 for(ite = ilist.begin(); ite != ilist.end(); ++ite) cout << *ite << ' '; cout << endl; ite = find(ilist.begin(), ilist.end(), 1); if (ite!=0) cout << *(ilist.erase(ite)) << endl; // 0 1 2 99 3 4 // 2 for(ite = ilist.begin(); ite != ilist.end(); ++ite) cout << *ite << ' '; // 0 2 99 3 4 cout << endl; } The Annotated STL Sources 134 第 4 章 序列式容器(sequence containers) list 预 设 使 用 alloc ( 2.2.4 节 ) 做 为 空 间 配 置 器 , 并 据 此 另 外 定 义 了 ㆒ 个 list_node_allocator,为的是更方便㆞以节点大小为配置单位: template // 预设使用 alloc 为配置器 class list { protected: typedef __list_node list_node; // 专属之空间配置器,每次配置㆒个节点大小: typedef simple_alloc list_node_allocator; ... }; 于是,list_node_allocator(n) 表示配置 n 个节点空间。以㆘㆕个函式,分别 用来配置、释放、建构、摧毁㆒个节点: protected: // 配置㆒个节点并传回 link_type get_node() { return list_node_allocator::allocate(); } // 释放㆒个节点 void put_node(link_type p) { list_node_allocator::deallocate(p); } // 产生(配置并建构)㆒个节点,带有元素值 link_type create_node(const T& x) { link_type p = get_node(); construct(&p->data, x);// 全域函式,建构/解构基本工具。 return p; } // 摧毁(解构并释放)㆒个节点 void destroy_node(link_type p) { destroy(&p->data); // 全域函式,建构/解构基本工具。 put_node(p); } list 提供有许多 constructors,其㆗㆒个是 default constructor,允许我们不指 定任何参数做出㆒个空的 list 出来: public: list() { empty_initialize(); } // 产生㆒个空串行。 protected: void empty_initialize() node = get_node(); // 配置㆒个节点空间,令 node 指向它。 node->next = node; // 令 node 头尾都指向自己,不设元素值。 node->prev = node; } The Annotated STL Sources 4.3 list 135 list::node prev next empty list(空串行) 当我们以 push_back() 将新元素安插于 list 尾端,此函式内部呼叫 insert(): void push_back(const T& x) { insert(end(), x); } insert() 是㆒个多载化函式,有多种型式,其㆗最简单的㆒种如㆘,符合以㆖所 需。首先配置并建构㆒个节点,然后在尾端做适当的指标动作,将新节点安插进 去: // 函式目的:在迭代器 position 所指位置安插㆒个节点,内容为 x。 iterator insert(iterator position, const T& x) { link_type tmp = create_node(x); // 产生㆒个节点(设妥内容为 x) // 调整双向指标,使 tmp 安插进去。 tmp->next = position.node; tmp->prev = position.node->prev; (link_type(position.node->prev))->next = tmp; position.node->prev = tmp; return tmp; } 于是,先前测试程序连续安插了五个节点(其值为 0 1 2 3 4)之后,list 的状态 如图 4-5。如果我们希望在 list 内的某处安插新节点,首先必须确定安插位置, 例如我希望在资料值为 3 的节点处安插㆒个数据值为 99 的节点,可以这么做: ilite = find(il.begin(), il.end(), 3); if (ilite!=0) il.insert(ilite, 99); find() 动作稍后再做说明。安插之后的 list 状态如图 4-6。注意,安插完成后, 新节点将位于标兵迭代器(标示出安插点)所指之节点的前方 — 这是 STL 对于 「安插动作」的标准规范。由于 list 不像 vector 那样有可能在空间不足时做重 新配置、数据搬移的动作,所以安插前的所有迭代器在安插动作之后都仍然有效。 The Annotated STL Sources 136 ilist.end() ilist.node ilist.begin() 4 第 4 章 序列式容器(sequence containers) ite= find(ilist.begin(), ilist.end(), 3); ilist.insert(ite,99); ite 3 99 nex t prev 0 1 2 图 4-6 安插新节点 99 于节点 3 的位置㆖(所谓安插是指「安插在…之前」) 4.3.6 list 的元素操作: push_front, push_back, erase, pop_front, pop_back, clear, remove, unique, splice, merge, reverse, sort list 所提供的元素操作动作很多,无法在有限的篇幅㆗㆒㆒讲解 — 其实也没有 这种必要。为搭配先前对空间配置的讨论,我挑选数个相关函式做为解说对象。 先前示例㆗出现有尾部安插动作(push_back),现在我们来看看其它的安插动 作和移除动作。 // 安插㆒个节点,做为头节点 void push_front(const T& x) { insert(begin(), x); } // 安插㆒个节点,做为尾节点(㆖㆒小节才介绍过) void push_back(const T& x) { insert(end(), x); } // 移除迭代器 position 所指节点 iterator erase(iterator position) { link_type next_node = link_type(position.node->next); link_type prev_node = link_type(position.node->prev); prev_node->next = next_node; next_node->prev = prev_node; destroy_node(position.node); return iterator(next_node); The Annotated STL Sources 4.3 list 137 } // 移除头节点 void pop_front() { erase(begin()); } // 移除尾节点 void pop_back() iterator tmp = end(); erase(--tmp); } // 清除所有节点(整个串行) template void list::clear() { link_type cur = (link_type) node->next; // begin() while (cur != node) { // 巡访每㆒个节点 link_type tmp = cur; cur = (link_type) cur->next; destroy_node(tmp); // 摧毁(解构并释放)㆒个节点 } // 恢复 node 原始状态 node->next = node; node->prev = node; } // 将数值为 value 之所有元素移除 template void list::remove(const T& value) { iterator first = begin(); iterator last = end(); while (first != last) { iterator next = first; ++next; // 巡访每㆒个节点 if (*first == value) erase(first); // 找到就移除 first = next; } } // 移除数值相同的连续元素。注意,只有「连续而相同的元素」,才会被移除剩㆒个。 template void list::unique() { iterator first = begin(); iterator last = end(); if (first == last) return; iterator next = first; while (++next != last) { if (*first == *next) erase(next); // 空串行,什么都不必做。 // 巡访每㆒个节点 // 如果在此区段㆗有相同的元素 // 移除之 else The Annotated STL Sources 138 first = next; next = first; 第 4 章 序列式容器(sequence containers) // 调整指标 // 修正区段范围 } } 由于 list 是㆒个双向环状串行,只要我们把边际条件处理好,那么,在头部或 尾部安插元素(push_front 和 push_back),动作几乎是㆒样的,在头部或尾 部移除元素(pop_front 和 pop_back),动作也几乎是㆒样的。移除(erase) 某个迭代器所指元素,只是做㆒些指标搬移动作而已,并不复杂。如果图 4-6 再 经以㆘搜寻并移除的动作,状况将如图 4-7。 ite = find(ilist.begin(), ilist.end(), 1); if (ite!=0) cout << *(ilist.erase(ite)) << endl; next ilist.end() ilist.node ilist.begin() 4 3 prev 99 0 ite= find(ilist.begin(), ilist.end(), 1); ite 1 destroy_node() 2 ilist.erase(ite); 图 4-7 移除「元素值为 1」的节点 list 内部提供㆒个所谓的迁移动作(transfer):将某连续范围的元素迁移到 某个特定位置之前。技术㆖很简单,节点间的指标移动而已。这个动作为其它的 复杂动作如 splice, sort, merge 等奠定良好的基础。㆘面是 transfer 的源 码: The Annotated STL Sources 4.3 list 139 protected: // 将 [first,last) 内的所有元素搬移到 position 之前。 void transfer(iterator position, iterator first, iterator last) if (position != last) { { (*(link_type((*last.node).prev))).next = position.node; (*(link_type((*first.node).prev))).next = last.node; (*(link_type((*position.node).prev))).next = first.node; link_type tmp = link_type((*position.node).prev); (*position.node).prev = (*last.node).prev; (*last.node).prev = (*first.node).prev; (*first.node).prev = tmp; } } 以㆖七个动作,㆒步㆒步㆞显示于图 4-8a。 position 迁移(transfer) next prev 初始状态 next // // // // // // // (1) (2) (3) (4) (5) (6) (7) prev first last 4 next prev next tmp 3 position 1 prev next prev next tmp 7 first position 2 5 last prev first 6 last 图 4-8a list::transfer 的动作示意 The Annotated STL Sources 140 第 4 章 序列式容器(sequence containers) transfer 所接受的 [first,last) 区间,是否可以在同㆒个 list 之㆗呢?答 案是可以。你只要想象图 4-8a 所画的两条 lists 其实是同㆒个 list 的两个区 段,就不难得到答案了。 ㆖述的 transfer 并非公开界面。list 公开提供的是所谓的接合动作(splice): 将某连续范围的元素从㆒个 list 搬移到另㆒个(或同㆒个)list 的某个定点。 如果接续先前 4list-test.cpp 程序的最后执行点,继续执行以㆘ splice 动作: int iv[5] = { 5,6,7,8,9 }; list ilist2(iv, iv+5); // 目前,ilist 的内容为 0 2 99 3 4 ite = find(ilist.begin(), ilist.end(), ilist.splice(ite,ilist2); ilist.reverse(); ilist.sort(); 99); // 0 2 5 6 7 8 9 99 3 4 // 4 3 99 9 8 7 6 5 2 0 // 0 2 3 4 5 6 7 8 9 99 很容易便可看出效果。图 4-8b 显示接合动作。技术㆖很简单,只是节点间的指标 移动而已,这些动作已完全由 transfer() 做掉了。 接合(splice)之前 接合(splice)之后 图 4-8b list 的接合(splice)动作 The Annotated STL Sources 4.3 list 141 为了提供各种接口弹性,list::splice 有许多版本: public: // 将 x 接合于 position 所指位置之前。x 必须不同于 *this。 void splice(iterator position, list& x) { if (!x.empty()) transfer(position, x.begin(), x.end()); } // 将 i 所指元素接合于 position 所指位置之前。position 和 i 可指向同㆒个 list。 void splice(iterator position, list&, iterator i) { iterator j = i; ++j; if (position == i || position == j) return; transfer(position, i, j); } // 将 [first,last) 内的所有元素接合于 position 所指位置之前。 // position 和[first,last)可指向同㆒个 list, // 但 position 不能位于[first,last)之内。 void splice(iterator position, list&, iterator first, iterator last) if (first != last) transfer(position, first, last); } 以㆘是 merge(), reverse(), sort() 的源码。有了 transfer() 在手,这些 动作都不难完成。 // merge() 将 x 合并到 *this 身㆖。两个 lists 的内容都必须先经过递增排序。 template void list::merge(list& x) { iterator first1 = begin(); iterator last1 = end(); iterator first2 = x.begin(); iterator last2 = x.end(); // 注意:前提是,两个 lists 都已经过递增排序, while (first1 != last1 && first2 != last2) if (*first2 < *first1) { iterator next = first2; transfer(first1, first2, ++next); first2 = next; } else ++first1; if (first2 != last2) transfer(last1, first2, last2); } The Annotated STL Sources { 142 第 4 章 序列式容器(sequence containers) // reverse() 将 *this 的内容逆向重置 template void list::reverse() { // 以㆘判断,如果是空白串行,或仅有㆒个元素,就不做任何动作。 // 使用 size() == 0 || size() == 1 来判断,虽然也可以,但是比较慢。 if (node->next == node || link_type(node->next)->next == node) return; iterator first = begin(); ++first; while (first != end()) { iterator old = first; ++first; transfer(begin(), old, first); } } // list 不能使用 STL 算法 sort(),必须使用自己的 sort() member function, // 因为 STL 算法 sort() 只接受 RamdonAccessIterator. // 本函式采用 quick sort. template void list::sort() { // 以㆘判断,如果是空白串行,或仅有㆒个元素,就不做任何动作。 // 使用 size() == 0 || size() == 1 来判断,虽然也可以,但是比较慢。 if (node->next == node || link_type(node->next)->next == node) return; // ㆒些新的 lists,做为㆗介数据存放区 list carry; list counter[64]; int fill = 0; while (!empty()) { carry.splice(carry.begin(), *this, begin()); int i = 0; while(i < fill && !counter[i].empty()) { counter[i].merge(carry); carry.swap(counter[i++]); } carry.swap(counter[i]); if (i == fill) ++fill; } for (int i = 1; i < fill; ++i) counter[i].merge(counter[i-1]); swap(counter[fill-1]); } The Annotated STL Sources 4.4 deque 143 4.4 4.4.1 deque deque 概述 vector 是单向开口的连续线性空间,deque 则是㆒种双向开口的连续线性空间。 所谓双向开口,意思是可以在头尾两端分别做元素的安插和删除动作,如图 4-9。 vector 当然也可以在头尾两端做动作(从技术观点),但是其头部动作效率奇差, 无法被接受。 push deque (双向进出) push ... 4 1 7 6 2 5 ... pop 图 4-9 deque 示意 pop deque 和 vector 的最大差异,㆒在于 deque 允许于常数时间内对起头端进行 元素的安插或移除动作,㆓在于 deque 没有所谓容量(capacity)观念,因为 它是动态㆞以分段连续空间组合而成,随时可以增加㆒段新的空间并链接起来。 换句话说,像 vector 那样「因旧空间不足而重新配置㆒块更大空间,然后复制 元素,再释放旧空间」这样的事情在 deque 是不会发生的。也因此,deque 没 有必要提供所谓的空间保留(reserve)功能。 虽然 deque 也提供 Ramdon Access Iterator,但它的迭代器并不是原生指标,其 复杂度和 vector 不可以道里计(稍后看到源码,你便知道),这当然在在影响 了各个运算层面。因此,除非必要,我们应尽可能选择使用 vector 而非 deque。 对 deque 进行的排序动作,为了最高效率,可将 deque 先完整复制到㆒个 vector 身㆖,将 vector 排序后(利用 STL sort 算法),再复制回 deque。 The Annotated STL Sources 144 4.4.2 deque 的中控器 第 4 章 序列式容器(sequence containers) deque 是连续空间(至少逻辑看来如此),连续线性空间总令我们联想到 array 或 vector。array 无法成长,vector 虽可成长,却只能向尾端成长,而且其所 谓成长原是个假象,事实㆖是 (1) 另觅更大空间、(2) 将原数据复制过去、(3) 释 放原空间 ㆔部曲。如果不是 vector 每次配置新空间时都有留㆘㆒些余裕,其「成 长」假象所带来的代价将是相当高昂。 deque 系由㆒段㆒段的定量连续空间构成。㆒旦有必要在 deque 的前端或尾端增 加新空间,便配置㆒段定量连续空间,串接在整个 deque 的头端或尾端。deque 的 最大任务,便是在这些分段的定量连续空间㆖,维护其整体连续的假象,并提供 随机存取的界面。避开了「重新配置、复制、释放」的轮回,代价则是复杂的迭 代器架构。 受 到 分 段 连 续 线 性 空 间 的 字 面 影 响 , 我 们 可 能 以 为 deque 的 实 作 复 杂 度 和 vector 相比虽不㆗亦不远矣,其实不然。主要因为,既曰分段连续线性空间,就 必须有㆗央控制,而为了维护整体连续的假象,数据结构的设计及迭代器前进后 退等动作都颇为繁琐。deque 的实作码份量远比 vector 或 list 都多得多。 deque 采用㆒块所谓的 map(注意,不是 STL 的 map 容器)做为主控。这里所 谓 map 是㆒小块连续空间,其㆗每个元素(此处称为㆒个节点,node)都是指标, 指向另㆒段(较大的)连续线性空间,称为缓冲区。缓冲区才是 deque 的储存空 间主体。SGI STL 允许我们指定缓冲区大小,默认值 0 表示将使用 512 bytes 缓 冲区。 template class deque { public: // Basic types typedef T value_type; typedef value_type* pointer; ... protected: // Internal typedefs // 元素的指针的指针(pointer of pointer of T) typedef pointer* map_pointer; protected: // Data members The Annotated STL Sources 4.4 deque map_pointer map; // 指向 map,map 是块连续空间,其内的每个元素 145 // 都是㆒个指标(称为节点),指向㆒块缓冲区。 size_type map_size; // map 内可容纳多少指标。 ... }; 把令㆟头皮发麻的各种型别定义(为了型别安全,那其实是有必要的)整理㆒㆘, 我们便可发现,map 其实是㆒个 T**,也就是说它是㆒个指标,所指之物又是㆒ 个指标,指向型别为 T 的㆒块空间,如图 4-10。 稍后在 deque 的建构过程㆗,我会详细解释 map 的配置及维护。 ... 缓冲区 map 当map使用率已经满载,便需要再找㆒块更大的 空间来做为map。配置策略见 reallocate_map() 新的 map 于是可以容纳更多的节点(亦即代表更多的缓冲区) 图 4-10 deque 的结构设计㆗,map 和 node-buffer(节点-缓冲区)的关系。 The Annotated STL Sources 146 4.4.3 deque 的迭代器 第 4 章 序列式容器(sequence containers) deque 是 分 段 连 续 空 间 。 维 护 其 「 整 体 连 续 」 假 象 的 任 务 , 着 落 在 迭 代 器 的 operator++ 和 operator-- 两个运算子身㆖。 让我们思考㆒㆘,deque 迭代器应该具备什么结构。首先,它必须能够指出分段 连续空间(亦即缓冲区)在哪里,其次它必须能够判断自己是否已经处于其所在 缓冲区的边缘,如果是,㆒旦前进或后退时就必须跳跃至㆘㆒个或㆖㆒个缓冲区。 为了能够正确跳跃,deque 必须随时掌握管控㆗心(map)。㆘面这种实作方式 符合需求: template struct __deque_iterator { // 未继承 std::iterator typedef __deque_iterator iterator; typedef __deque_iterator const_iterator; static size_t buffer_size() {return __deque_buf_size(BufSiz, sizeof(T)); } // 未继承 std::iterator,所以必须自行撰写五个必要的迭代器相应型别(第 3 章) typedef random_access_iterator_tag iterator_category; // (1) typedef T value_type; typedef Ptr pointer; typedef Ref reference; typedef size_t size_type; typedef ptrdiff_t difference_type; typedef T** map_pointer; typedef __deque_iterator self; // 保持与容器的联结 // (2) // (3) // (4) // (5) T* cur; T* first; T* last; // 此迭代器所指之缓冲区㆗的现行(current)元素 // 此迭代器所指之缓冲区的头 // 此迭代器所指之缓冲区的尾(含备用空间) map_pointer node; // 指向管控㆗心 ... }; 其㆗用来决定缓冲区大小的函式 buffer_size(),呼叫 __deque_buf_size(), 后者是个全域函式,定义如㆘: // 如果 n // 如果 n 不为 0,传回 n,表示 buffer size 由使用者自定。 为 0,表示 buffer size 使用默认值,那么 // // 如果 如果 sz(元素大小,sizeof(value_type))小于 512,传回 512/sz, sz 不小于 512,传回 1。 The Annotated STL Sources 4.4 deque 147 inline size_t __deque_buf_size(size_t n, size_t sz) { return n != 0 ? n : (sz < 512 ? size_t(512 / sz) : size_t(1)); } 图 4-11 是 deque 的㆗控器、缓冲区、迭代器的相互关系。 缓冲区 buffer ... map 节点 node ㆗控器 cur first last node 迭代器 iterator 图 4-11 deque 的㆗控器、缓冲区、迭代器的相互关系 假设现在我们产生㆒个 deque,并令其缓冲区大小为 32,于是每个缓冲区 可容纳 32/sizeof(int)=4 个元素。经过某些操作之后,deque 拥有 20 个元素, 那么其 begin() 和 end() 所传回的两个迭代器应该如图 4-12。这两个迭代器事 实㆖㆒直保持在 deque 内,名为 start 和 finish,稍后在 deque 数据结构㆗ 便可看到)。 20 个元素需要 20/8 = 3 个缓冲区,所以 map 之内运用了㆔个节点。迭代器 start 内的 cur 指标当然指向缓冲区的第㆒个元素,迭代器 finish 内的 cur 指标当然 指向缓冲区的最后元素(的㆘㆒位置)。注意,最后㆒个缓冲区尚有备用空间。 稍后如果有新元素要安插于尾端,可直接拿此备用空间来使用。 The Annotated STL Sources 148 map 第 4 章 序列式容器(sequence containers) cur first last node cur first last node start(iterator) finish(iterator) 图 4-12 deque::begin() 传 回 迭 代 器 start ,deque::end() 传 回 迭 代 器 finish。这两个迭代器都是 deque 的 data members。图㆗所示的这个 deque 拥 有 20 个 int 元素,以 3 个缓冲区储存之。每个缓冲区 32 bytes,可储存 8 个 int 元素。map 大小为 8(起始值),目前用了 3 个节点。 ㆘面是 deque 迭代器的几个关键行为。由于迭代器内对各种指标运算都做了多载 化动作,所以各种指标运算如加、减、前进、后退…都不能直观视之。其㆗最重 点的关键就是:㆒旦行进时遇到缓冲区边缘,要特别当心,视前进或后退而定, 可能需要呼叫 set_node() 跳㆒个缓冲区: void set_node(map_pointer new_node) { node = new_node; first = *new_node; last = first + difference_type(buffer_size()); } // 以㆘各个多载化运算子是 __deque_iterator<> 成功运作的关键。 reference operator*() const { return *cur; } pointer operator->() const { return &(operator*()); } The Annotated STL Sources 4.4 deque 149 difference_type operator-(const self& x) const { return difference_type(buffer_size()) * (node - x.node - 1) + (cur - first) + (x.last - x.cur); } // 参考 More Effective C++, item6: Distinguish between prefix and // postfix forms of increment and decrement operators. self& operator++() { ++cur; if (cur == last) { set_node(node + 1); // 切换至㆘㆒个元素。 // 如果已达所在缓冲区的尾端, // 就切换至㆘㆒节点(亦即缓冲区) } cur = first; } return *this; // 的第㆒个元素。 self operator++(int) { self tmp = *this; ++*this; return tmp; } self& operator--() { if (cur == first) { set_node(node - 1); // 后置式,标准写法 // 如果已达所在缓冲区的头端, // 就切换至前㆒节点(亦即缓冲区) } cur = last; // 的最后㆒个元素。 --cur; return *this; } self operator--(int) { // 切换至前㆒个元素。 // 后置式,标准写法 self tmp = *this; --*this; return tmp; } // 以㆘实现随机存取。迭代器可以直接跳跃 n 个距离。 self& operator+=(difference_type n) { difference_type offset = n + (cur - first); if (offset >= 0 && offset < difference_type(buffer_size())) // 标的位置在同㆒缓冲区内 cur += n; else { // 标的位置不在同㆒缓冲区内 difference_type node_offset = offset > 0 ? offset / difference_type(buffer_size()) : -difference_type((-offset - 1) / buffer_size()) - 1; // 切换至正确的节点(亦即缓冲区) set_node(node + node_offset); // 切换至正确的元素 The Annotated STL Sources 150 第 4 章 序列式容器(sequence containers) cur = first + (offset - node_offset * difference_type(buffer_size())); } return *this; } // 参考 More Effective C++, item22: Consider using op= instead of // stand-alone op. self operator+(difference_type n) const { self tmp = *this; return tmp += n; // 唤起 operator+= } self& operator-=(difference_type n) { return *this += -n; } // 以㆖利用 operator+= 来完成 operator-= // 参考 More Effective C++, item22: Consider using op= instead of // stand-alone op. self operator-(difference_type n) const { self tmp = *this; return tmp -= n; // 唤起 operator-= } // 以㆘实现随机存取。迭代器可以直接跳跃 n 个距离。 reference operator[](difference_type n) const { return *(*this + n); } // 以㆖唤起 operator*, operator+ bool operator==(const self& x) const { return cur == x.cur; } bool operator!=(const self& x) const { return !(*this == x); } bool operator<(const self& x) const { return (node == x.node) ? (cur < x.cur) : (node < x.node); } 4.4.4 deque 的数据结构 deque 除了维护㆒个先前说过的指向 map 的指标外,也维护 start, finish 两 个迭代器,分别指向第㆒缓冲区的第㆒个元素和最后缓冲区的最后㆒个元素(的 ㆘㆒位置)。此外它当然也必须记住目前的 map 大小。因为㆒旦 map 所提供的 节点不足,就必须重新配置更大的㆒块 map。 // 见 __deque_buf_size()。BufSize 默认值为 0 的唯㆒理由是为了闪避某些 // 编译器在处理常数算式(constant expressions)时的臭虫。 // 预设使用 alloc 为配置器。 template class deque { public: // Basic types typedef T value_type; The Annotated STL Sources 4.4 deque typedef value_type* pointer; typedef size_t size_type; 151 public: // Iterators typedef __deque_iterator iterator; protected: // Internal typedefs // 元素的指针的指针(pointer of pointer of T) typedef pointer* map_pointer; protected: iterator start; iterator finish; map_pointer map; // Data members // 表现第㆒个节点。 // 表现最后㆒个节点。 // 指向 map,map 是块连续空间, // 其每个元素都是个指针,指向㆒个节点(缓冲区)。 size_type map_size; // map 内有多少指标。 ... }; 有了㆖述结构,以㆘数个机能便可轻易完成: public: // Basic accessors iterator begin() { return start; } iterator end() { return finish; } reference operator[](size_type n) { return start[difference_type(n)]; // 唤起 __deque_iterator<>::operator[] } reference front() { return *start; } // 唤起 __deque_iterator<>::operator* reference back() { iterator tmp = finish; --tmp; // 唤起 __deque_iterator<>::operator-- return *tmp; // 唤起 __deque_iterator<>::operator* // 以㆖㆔行何不改为:return *(finish-1); // 因为 __deque_iterator<> 没有为 (finish-1) 定义运算子?! } // ㆘行最后有两个 ‘;’,虽奇怪但合乎语法。 size_type size() const { return finish - start;; } // 以㆖唤起 iterator::operator- size_type max_size() const { return size_type(-1); } bool empty() const { return finish == start; } The Annotated STL Sources 152 第 4 章 序列式容器(sequence containers) 4.4.5 deque 的建构与内存管理 ctor, push_back, push_front 千头万绪该如何说起?以客端程序代码为引导,观察其所得结果并实证源码,是个 良好的学习路径。㆘面是㆒个测试程序,我的观察重点在建构的方式以及大小的 变化,以及容器最前端的安插功能: // filename : 4deque-test.cpp #include #include #include using namespace std; int main() { deque ideq(20,9); cout << "size=" << ideq.size() << endl; // 注意,alloc 只适用于 G++ // size=20 // 现在,应该已经建构了㆒个 deque,有 20 个 int 元素,初值皆为 9。 // 缓冲区大小为 32bytes。 // 为每㆒个元素设定新值。 for(int i=0; i void deque::push_back_aux(const value_type& t) { value_type t_copy = t; reserve_map_at_back(); // 若符合某种条件则必须重换㆒个 map *(finish.node + 1) = allocate_node(); __STL_TRY { construct(finish.cur, t_copy); finish.set_node(finish.node + 1); finish.cur = finish.first; // 配置㆒个新节点(缓冲区) // 针对标的元素设值 // 改变 finish,令其指向新节点 // 设定 finish 的状态 } __STL_UNWIND(deallocate_node(*(finish.node + 1))); } 现在,deque 的状态如图 4-14。 The Annotated STL Sources 4.4 deque 157 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 id::push_back(3); 16 17 18 19 0 1 2 3 map cur first last node cur first last node start(iterator) finish(iterator) 图 4-14 延续图 4-13 的状态,在尾端再加㆒个元素,于是引发新缓冲区的配置, 同时也造成迭代器 finish 的状态改变。map 大小为 8(初始值),目前用了 4 个节点。 接㆘来范例程序在 deque 的前端安插㆒个新元素: ideq.push_front(99); push_front() 函式动作如㆘: public: // push_* and pop_* void push_front(const value_type& t) { if (start.cur != start.first) { // 第㆒缓冲区尚有备用空间 construct(start.cur - 1, t); // 直接在备用空间㆖建构元素 --start.cur; // 调整第㆒缓冲区的使用状态 } else // 第㆒缓冲区已无备用空间 push_front_aux(t); } The Annotated STL Sources 158 第 4 章 序列式容器(sequence containers) 由于目前状态㆘,第㆒缓冲区并无备用空间,所以呼叫 push_front_aux(): // 只有当 start.cur == start.first 时才会被呼叫。 // 也就是说只有当第㆒个缓冲区没有任何备用元素时才会被呼叫。 template void deque::push_front_aux(const value_type& t) { value_type t_copy = t; reserve_map_at_front(); // 若符合某种条件则必须重换㆒个 map *(start.node - 1) = allocate_node(); __STL_TRY { start.set_node(start.node - 1); start.cur = start.last - 1; construct(start.cur, t_copy); // 配置㆒个新节点(缓冲区) // 改变 start,令其指向新节点 // 设定 start 的状态 // 针对标的元素设值 } catch(...) { // "commit or rollback" 语意:若非全部成功,就㆒个不留。 start.set_node(start.node + 1); start.cur = start.first; deallocate_node(*(start.node - 1)); throw; } } 此 函 式 ㆒ 开 始 即 呼 叫 reserve_map_at_front() , 后 者 用 来 判 断 是 否 需 要 扩 充 map,如有需要就付诸行动。稍后我会呈现 reserve_map_at_front() 的函式内 容。目前的状态不需要重新整治 map,所以后继流程便配置了㆒块新缓冲区并直 接将节点安置于现有的 map ㆖,然后设定新元素,然后改变迭代器 start 的状 态,如图 4-15。 The Annotated STL Sources 4.4 deque ideq::push_front(99); 99 159 0 8 16 1 9 17 2 10 18 3 11 19 4 12 0 5 13 1 6 14 2 7 15 3 map cur first last node cur first last node start(iterator) finish(iterator) 图 4-15 延续图 4-14 的状态,在最前端加㆖㆒个元素。引发新缓冲区的配置,同 时也造成迭代器 start 状态改变。map 大小为 8(初始值),目前用掉 5 个节点。 接㆘来范例程序又在 deque 的最前端安插两个新元素: ideq.push_front(98); ideq.push_front(97); 这㆒次,由于第㆒缓冲区有备用空间,push_front() 可以直接在备用空间㆖建 构新元素,如图 4-16。 The Annotated STL Sources 160 第 4 章 序列式容器(sequence containers) ideq::push_front(98); ideq::push_front(97); 97 98 99 0 8 16 1 9 17 2 10 18 3 11 19 4 12 0 5 13 1 6 14 2 7 15 3 map cur first last node cur first last node start(iterator) finish(iterator) 图 4-16 延续图 4-15 的状态,在最前端再加两个元素。由于第㆒缓冲区尚有备 用空间,因此直接取用备用空间来建构新元素即可。 图 4-12 至图 4-16 的连环图解,已经充份展示了 deque 容器的空间运用策略。让 我们回头看看㆒个悬而未解的问题:什么时候 map 需要重新整治?这个问题的判 断由 reserve_map_at_back() 和 reserve_map_at_front() 进 行, 实际动 作 则由 reallocate_map() 执行: void reserve_map_at_back (size_type nodes_to_add = 1) { if (nodes_to_add + 1 > map_size - (finish.node - map)) // 如果 map 尾端的节点备用空间不足 // 符合以㆖条件则必须重换㆒个 map(配置更大的,拷贝原来的,释放原来的) reallocate_map(nodes_to_add, false); } void reserve_map_at_front (size_type nodes_to_add = 1) { if (nodes_to_add > start.node - map) // 如果 map 前端的节点备用空间不足 The Annotated STL Sources 4.4 deque 161 // 符合以㆖条件则必须重换㆒个 map(配置更大的,拷贝原来的,释放原来的) reallocate_map(nodes_to_add, true); } template void deque::reallocate_map(size_type nodes_to_add, bool add_at_front) { size_type old_num_nodes = finish.node - start.node + 1; size_type new_num_nodes = old_num_nodes + nodes_to_add; map_pointer new_nstart; if (map_size > 2 * new_num_nodes) { new_nstart = map + (map_size - new_num_nodes) / 2 + (add_at_front ? nodes_to_add : 0); if (new_nstart < start.node) copy(start.node, finish.node + 1, new_nstart); else copy_backward(start.node, finish.node + 1, new_nstart + old_num_nodes); } else { size_type new_map_size = map_size + max(map_size, nodes_to_add) + 2; // 配置㆒块空间,准备给新 map 使用。 map_pointer new_map = map_allocator::allocate(new_map_size); new_nstart = new_map + (new_map_size - new_num_nodes) / 2 + (add_at_front ? nodes_to_add : 0); // 把原 map 内容拷贝过来。 copy(start.node, finish.node + 1, new_nstart); // 释放原 map map_allocator::deallocate(map, map_size); // 设定新 map 的起始地址与大小 map = new_map; map_size = new_map_size; } // 重新设定迭代器 start 和 finish start.set_node(new_nstart); finish.set_node(new_nstart + old_num_nodes - 1); } 4.4.6 deque 的元素操作 pop_back, pop_front, clear, erase, insert deque 所提供的元素操作动作很多,无法在有限的篇幅㆗㆒㆒讲解 — 其实也没 有这种必要。以㆘我只挑选几个 member functions 做为示范说明。 前述测试程序曾经以泛型算法 find() 寻找 deque 的某个元素: The Annotated STL Sources 162 第 4 章 序列式容器(sequence containers) deque::iterator itr; itr = find(ideq.begin(), ideq.end(), 99); 当 find() 动作完成,迭代器 itr 状态如图 4-17 所示。㆘面这两个动作输出相 同的结果,印证我们对 deque 迭代器的认识。 cout << *itr << endl; cout << *(itr.cur) << endl; // 99 // 99 cur first 97 last 98 node 99 deque::iterator itr; itr = find(id.begin(), id.end(), 99); 0 8 16 1 9 17 2 10 18 3 11 19 4 12 0 5 13 1 6 14 2 7 15 3 map cur first last node cur first last node start(iterator) finish(iterator) 图 4-17 延续图 4-16 的状态,以 find() 寻找数值为 99 的元素。此函式将传回 ㆒个迭代器,指向第㆒个符合条件的元素。注意,该迭代器的㆕个字段都必须有 正确的设定。 The Annotated STL Sources 4.4 deque 163 前㆒节已经展示过 push_back() 和 push_front() 的实作内容,现在我举对应 的 pop_back() 和 pop_front() 为例。所谓 pop,是将元素拿掉。无论从 deque 的最前端或最尾端取元素,都需考虑在某种条件㆘,将缓冲区释放掉: void pop_back() { if (finish.cur != finish.first) { // 最后缓冲区有㆒个(或更多)元素 --finish.cur; // 调整指针,相当于排除了最后元素 destroy(finish.cur); // 将最后元素解构 } else // 最后缓冲区没有任何元素 pop_back_aux(); // 这里将进行缓冲区的释放工作 } // 只有当 finish.cur == finish.first 时才会被呼叫。 template void deque::pop_back_aux() { deallocate_node(finish.first); // 释放最后㆒个缓冲区 finish.set_node(finish.node - 1); // 调整 finish 的状态,使指向 finish.cur = finish.last - 1; destroy(finish.cur); // ㆖㆒个缓冲区的最后㆒个元素 // 将该元素解构。 } void pop_front() { if (start.cur != start.last - 1) { // 第㆒缓冲区有㆒个(或更多)元素 destroy(start.cur); ++start.cur; } else // 第㆒缓冲区仅有㆒个元素 pop_front_aux(); // 将第㆒元素解构 // 调整指针,相当于排除了第㆒元素 // 这里将进行缓冲区的释放工作 } // 只有当 start.cur == start.last - 1 时才会被呼叫。 template void deque::pop_front_aux() { destroy(start.cur); deallocate_node(start.first); start.set_node(start.node + 1); start.cur = start.first; // 将第㆒缓冲区的第㆒个元素解构。 // 释放第㆒缓冲区。 // 调整 start 的状态,使指向 // ㆘㆒个缓冲区的第㆒个元素。 } The Annotated STL Sources 164 第 4 章 序列式容器(sequence containers) ㆘面这个例子是 clear(),用来清除整个 deque。请注意,deque 的最初状态(无 任何元素时)保有㆒个缓冲区,因此 clear() 完成之后回复初始状态,也㆒样要 保留㆒个缓冲区: // 注意,最终需要保留㆒个缓冲区。这是 deque 的策略,也是 deque 的初始状态。 template void deque::clear() { // 以㆘针对头尾以外的每㆒个缓冲区(它们㆒定都是饱满的) for (map_pointer node = start.node + 1; node < finish.node; ++node) { // 将缓冲区内的所有元素解构。注意,呼叫的是 destroy() 第㆓版本,见 2.2.3 节 destroy(*node, *node + buffer_size()); // 释放缓冲区内存 data_allocator::deallocate(*node, buffer_size()); } if (start.node != finish.node) { destroy(start.cur, start.last); // 至少有头尾两个缓冲区 // 将头缓冲区的目前所有元素解构 destroy(finish.first, finish.cur); // 将尾缓冲区的目前所有元素解构 // 以㆘释放尾缓冲区。注意,头缓冲区保留。 data_allocator::deallocate(finish.first, buffer_size()); } else // 只有㆒个缓冲区 destroy(start.cur, finish.cur); // 将此唯㆒缓冲区内的所有元素解构 // 注意,并不释放缓冲区空间。这唯㆒的缓冲区将保留。 finish = start; // 调整状态 } ㆘面这个例子是 erase(),用来清除某个元素: // 清除 pos 所指的元素。pos 为清除点。 iterator erase(iterator pos) { iterator next = pos; ++next; difference_type index = pos - start; // 清除点之前的元素个数 if (index < (size() >> 1)) { copy_backward(start, pos, next); // 如果清除点之前的元素比较少, // 就搬移清除点之前的元素 pop_front(); } else { copy(next, finish, pos); pop_back(); // 搬移完毕,最前㆒个元素赘余,去除之 // 清除点之后的元素比较少, // 就搬移清除点之后的元素 // 搬移完毕,最后㆒个元素赘余,去除之 } return start + index; } The Annotated STL Sources 4.4 deque 165 ㆘面这个例子是 erase(),用来清除 [first,last) 区间内的所有元素: template deque::iterator deque::erase(iterator first, iterator last) { if (first == start && last == finish) { // 如果清除区间就是整个 deque clear(); return finish; } else { // 直接呼叫 clear() 即可 difference_type n = last - first; difference_type elems_before = first - start; if (elems_before < (size() - n) / 2) { // 清除区间的长度 // 清除区间前方的元素个数 // 如果前方的元素比较少, copy_backward(start, first, last); iterator new_start = start + n; destroy(start, new_start); // 向后搬移前方元素(覆盖清除区间) // 标记 deque 的新起点 // 搬移完毕,将赘余的元素解构 // 以㆘将赘余的缓冲区释放 for (map_pointer cur = start.node; cur < new_start.node; ++cur) data_allocator::deallocate(*cur, buffer_size()); } start = new_start; // 设定 deque 的新起点 else { // 如果清除区间后方的元素比较少 copy(last, finish, first); iterator new_finish = finish - n; destroy(new_finish, finish); // 向前搬移后方元素(覆盖清除区间) // 标记 deque 的新尾点 // 搬移完毕,将赘余的元素解构 // 以㆘将赘余的缓冲区释放 for (map_pointer cur = new_finish.node + 1; cur <= finish.node; ++cur) data_allocator::deallocate(*cur, buffer_size()); finish = new_finish; // 设定 deque 的新尾点 } return start + elems_before; } } 本节要说明的最后㆒个例子是 insert。deque 为这个功能提供了许多版本,最 基础最重要的是以㆘版本,允许在某个点(之前)安插㆒个元素,并设定其值。 // 在 position 处安插㆒个元素,其值为 x iterator insert(iterator position, const value_type& x) { if (position.cur == start.cur) { // 如果安插点是 deque 最前端 push_front(x); // 交给 push_front 去做 return start; } else if (position.cur == finish.cur) { // 如果安插点是 deque 最尾端 push_back(x); // 交给 push_back 去做 iterator tmp = finish; --tmp; The Annotated STL Sources 166 return tmp; } else { 第 4 章 序列式容器(sequence containers) return insert_aux(position, x); // 交给 insert_aux 去做 } } template typename deque::iterator deque::insert_aux(iterator pos, const value_type& x) { difference_type index = pos - start; value_type x_copy = x; // 安插点之前的元素个数 if (index < size() / 2) { push_front(front()); iterator front1 = start; ++front1; iterator front2 = front1; ++front2; pos = start + index; iterator pos1 = pos; ++pos1; copy(front2, pos1, front1); } else { push_back(back()); iterator back1 = finish; --back1; iterator back2 = back1; --back2; pos = start + index; // 如果安插点之前的元素个数比较少 // 在最前端加入与第㆒元素同值的元素。 // 以㆘标示记号,然后进行元素搬移... // 元素搬移 // 安插点之后的元素个数比较少 // 在最尾端加入与最后元素同值的元素。 // 以㆘标示记号,然后进行元素搬移... copy_backward(pos, back2, back1); // 元素搬移 } *pos = x_copy; // 在安插点㆖设定新值 return pos; } The Annotated STL Sources 4.5 stack 167 4.5 4.5.1 stack stack 概述 stack 是㆒种先进后出(First In Last Out,FILO)的数据结构。它只有㆒个出口, 型式如图 4-18。stack 允许新增元素、移除元素、取得最顶端元素。但除了最顶 端外,没有任何其它方法可以存取 stack 的其它元素。换言之 stack 不允许有 走访行为。 将元素推入 stack 的动作称为 push,将元素推出 stack 的动作称为 pop。 stack(先进后出) push 4 1 7 6 2 5 ... pop 图 4-18 stack 的结构 4.5.2 stack 定义式完整列表 以某种既有容器做为底部结构,将其接口改变,使符合「先进后出」的特性,形 成㆒个 stack,是很容易做到的。deque 是双向开口的数据结构,若以 deque 为 底部结构并封闭其头端开口,便轻而易举㆞形成了㆒个 stack。因此,SGI STL 便 以 deque 做为预设情况㆘的 stack 底部结构,stack 的实作因而非常简单,源 码十分简短,本处完整列出。 由于 stack 系以底部容器完成其所有工作,而具有这种「修改某物接口,形成另 ㆒种风貌」之性质者,称为 adapter(配接器),因此 STL stack 往往不被归类 为 container(容器),而被归类为 container adapter。 template > class stack { // 以㆘的 __STL_NULL_TMPL_ARGS 会开展为 <>,见 1.9.1 节 friend bool operator== __STL_NULL_TMPL_ARGS (const stack&, const stack&); friend bool operator< __STL_NULL_TMPL_ARGS (const stack&, const stack&); public: The Annotated STL Sources 168 第 4 章 序列式容器(sequence containers) typedef typename Sequence::value_type value_type; typedef typename Sequence::size_type size_type; typedef typename Sequence::reference reference; typedef typename Sequence::const_reference const_reference; protected: Sequence c; // 底层容器 public: // 以㆘完全利用 Sequence c 的操作,完成 stack 的操作。 bool empty() const { return c.empty(); } size_type size() const { return c.size(); } reference top() { return c.back(); } const_reference top() const { return c.back(); } // deque 是两头可进出,stack 是末端进,末端出(所以后进者先出)。 void push(const value_type& x) { c.push_back(x); } void pop() { c.pop_back(); } }; template bool operator==(const stack& x, const stack& y) { return x.c == y.c; } template bool operator<(const stack& x, const stack& y) { return x.c < y.c; } 4.5.3 stack 没有迭代器 stack 所有元素的进出都必须符合「先进后出」的条件,只有 stack 顶端的元素, 才有机会被外界取用。stack 不提供走访功能,也不提供迭代器。 4.5.4 以 list 做为 stack 的底层容器 除了 deque 之外,list 也是双向开口的数据结构。㆖述 stack 源码㆗使用的底 层容器的函式有 empty, size, back, push_back, pop_back,凡此种种 list 都具备。因此若以 list 为底部结构并封闭其头端开口,㆒样能够轻易形成㆒个 stack。㆘面是作法示范。 // file : 4stack-test.cpp #include #include #include The Annotated STL Sources 4.6 queue 169 #include using namespace std; int main() { stack > istack; istack.push(1); istack.push(3); istack.push(5); istack.push(7); cout << istack.size() << endl; cout << istack.top() << endl; // 4 // 7 istack.pop(); cout << istack.pop(); cout << istack.pop(); cout << istack.top() << endl; istack.top() << endl; istack.top() << endl; // 5 // 3 // 1 cout << istack.size() } 4.6 queue 4.6.1 queue 概述 << endl; // 1 queue 是㆒种先进先出(First In First Out,FIFO)的数据结构。它有两个出口, 型式如图 4-19。queue 允许新增元素、移除元素、从最底端加入元素、取得最顶 端元素。但除了最底端可以加入、最顶端可以取出,没有任何其它方法可以存取 queue 的其它元素。换言之 queue 不允许有走访行为。 将元素推入 queue 的动作称为 push,将元素推出 queue 的动作称为 pop。 queue (先进先出) push 4 1 7 6 2 5 ... pop 图 4-19 queue 的结构 The Annotated STL Sources 170 4.6.2 第 4 章 序列式容器(sequence containers) queue 定义式完整列表 以某种既有容器为底部结构,将其接口改变,使符合「先进先出」的特性,形成 ㆒个 queue,是很容易做到的。deque 是双向开口的数据结构,若以 deque 为 底部结构并封闭其底端的出口和前端的入口,便轻而易举㆞形成了㆒个 queue。 因此,SGI STL 便以 deque 做为预设情况㆘的 queue 底部结构,queue 的实作 因而非常简单,源码十分简短,本处完整列出。 由于 queue 系以底部容器完成其所有工作,而具有这种「修改某物接口,形成另 ㆒种风貌」之性质者,称为 adapter(配接器),因此 STL queue 往往不被归类 为 container(容器),而被归类为 container adapter。 template > class queue { // 以㆘的 __STL_NULL_TMPL_ARGS 会开展为 <>,见 1.9.1 节 friend bool operator== __STL_NULL_TMPL_ARGS (const queue& x, const queue& y); friend bool operator< __STL_NULL_TMPL_ARGS (const queue& x, const queue& y); public: typedef typename Sequence::value_type value_type; typedef typename Sequence::size_type size_type; typedef typename Sequence::reference reference; typedef typename Sequence::const_reference const_reference; protected: Sequence c; // 底层容器 public: // 以㆘完全利用 Sequence c 的操作,完成 queue 的操作。 bool empty() const { return c.empty(); } size_type size() const { return c.size(); } reference front() { return c.front(); } const_reference front() const { return c.front(); } reference back() { return c.back(); } const_reference back() const { return c.back(); } // deque 是两头可进出,queue 是末端进,前端出(所以先进者先出)。 void push(const value_type& x) { c.push_back(x); } void pop() { c.pop_front(); } }; template bool operator==(const queue& x, const queue& y) { return x.c == y.c; } The Annotated STL Sources 4.6 queue 171 template bool operator<(const queue& x, const queue& y) { return x.c < y.c; } 4.6.3 queue 没有迭代器 queue 所有元素的进出都必须符合「先进先出」的条件,只有 queue 顶端的元素, 才有机会被外界取用。queue 不提供走访功能,也不提供迭代器。 4.6.4 以 list 做为 queue 的底层容器 除了 deque 之外,list 也是双向开口的数据结构。㆖述 queue 源码㆗使用的底 层容器的函式有 empty, size, back, push_back, pop_back,凡此种种 list 都具备。因此若以 list 为底部结构并封闭其头端开口,㆒样能够轻易形成㆒个 queue。㆘面是作法示范。 // file : 4queue-test.cpp #include #include #include #include using namespace std; int main() { queue > iqueue; iqueue.push(1); iqueue.push(3); iqueue.push(5); iqueue.push(7); cout << iqueue.size() << endl; cout << iqueue.front() << endl; // 4 // 1 iqueue.pop(); cout << iqueue.pop(); cout << iqueue.pop(); cout << iqueue.front() iqueue.front() iqueue.front() << endl; << endl; << endl; // 3 // 5 // 7 cout << iqueue.size() << endl; // 1 } The Annotated STL Sources 172 4.7 4.7.1 第 4 章 序列式容器(sequence containers) heap(隐 性表述 , implicit representation) heap 概述 heap 并不归属于 STL 容器组件,它是个幕后英雄,扮演 priority queue(4.8 节)的推手。顾名思义,priority queue 允许使用者以任何次序将任何元素推入 容器内,但取出时㆒定是从优先权最高(也就是数值最高)之元素开始取。binary max heap 正是具有这样的特性,适合做为 priority queue 的底层机制。 让我们做点分析。如果使用 4.3 节的 list 做为 priority queue 的底层机制,元 素安插动作可享常数时间。但是要找到 list ㆗的极值,却需要对整个 list 进行 线性扫描。我们也可以改个作法,让元素安插前先经过排序这㆒关,使得 list 的 元素值总是由小到大(或由大到小),但这么㆒来,收之东隅却失之桑榆:虽然 取得极值以及元素删除动作达到最高效率,元素的安插却只有线性表现。 比较麻辣的作法是以 binary search tree(如 5.1 节的 RB-tree)做为 priority queue 的底层机制。这么㆒来元素的安插和极值的取得就有 O(logN) 的表现。但 杀鸡用牛刀,未免小题大作,㆒来 binary search tree 的输入需要足够的随机性, ㆓来 binary search tree 并不容易实作。priority queue 的复杂度,最好介于 queue 和 binary search tree 之间,才算适得其所。binary heap 便是这种条件㆘ 的适当候选㆟。 所谓 binary heap 就是㆒种 complete binary tree(完全㆓元树)2,也就是说,整 棵 binary tree 除了最底层的叶节点(s) 之外,是填满的,而最底层的叶节点(s) 由 左至右又不得有空隙。图 4-20 是㆒个 complete binary tree。 2 关于 tree 的种种,5.1 节会有更多介绍。 The Annotated STL Sources 4.7 heap(隐性表述,implicit representation) A B C 173 H D I J E F G -∞ A B C D E F G H I J 某种实作技巧 图 4-20 ㆒个完全㆓元树(complete binary tree),及其 array 表述式 complete binary tree 整棵树内没有任何节点漏洞,这带来㆒个极大好处:我们可 以利用 array 来储存所有节点。假设动用㆒个小技巧3,将 array 的 #0 元素保 留(或设为无限大值或无限小值),那么当 complete binary tree ㆗的某个节点位 于 array 的i 处,其左子节点必位于 array 的2i 处,其右子节点必位于 array 的 2i+1 处,其父节点必位于「i/2」处(此处的「」权且代表高斯符号,取其整数)。 通过这么简单的位置规则,array 可以轻易实作出 complete binary tree。这种以 array 表述 tree 的方式,我们称为隐式表述法(implicit representation)。 这么㆒来,我们需要的工具就很简单了:㆒个 array 和㆒组 heap 算法(用来 安插元素、删除元素、取极值、将某㆒整组数据排列成㆒个 heap)。array 的缺 点是无法动态改变大小,而 heap 却需要这项功能,因此以 vector(4.2 节)代 替 array 是更好的选择。 根据元素排列方式,heap 可分为 max-heap 和 min-heap 两种,前者每个节点的 键值(key)都大于或等于其子节点键值,后者的每个节点键值(key)都小于或等 3 SGI STL 提供的 heap 并未使用此㆒小技巧。计算左右子节点以及父节点的方式,因 而略有不同。详见稍后的源码及解说。 The Annotated STL Sources 174 第 4 章 序列式容器(sequence containers) 于其子节点键值。因此,max-heap 的最大值在根节点,并总是位于底层 array 或 vector 的 起 头 处 ;min-heap 的 最 小 值 在 根 节 点 , 亦 总 是 位 于 底 层 array 或 vector 的起头处。STL 供应的是 max-heap,因此以㆘我说 heap 时,指的是 max-heap。 4.7.2 heap 算法 push_heap 演 算法 图 4-21 是 push_heap 算法的实际操演情况。为了满足 complete binary tree 的 条件,新加入的元素㆒定要放在最㆘㆒层做为叶节点,并填补在由左至右的第㆒ 个空格,也就是把新元素安插在底层 vector 的 end() 处。 ... ... 68 31 65 21 24 32 26 19 16 13 50 ... 68 ... ... 68 31 65 21 50 32 26 19 16 13 24 ... 68 31 Percolate up 65 31 65 21 24 32 26 21 50 32 26 19 16 13 push 50 19 16 13 24 ... ... 68 50 65 21 31 32 26 19 16 13 24 ... 68 ... ... 68 50 65 21 31 32 26 19 16 13 24 ... 68 50 65 50 65 21 31 32 26 21 31 32 26 19 16 13 24 19 16 13 24 图 4-21 push_heap 算法 The Annotated STL Sources 4.7 heap(隐性表述,implicit representation) 175 新元素是否适合于其现有位置呢?为满足 max-heap 的条件(每个节点的键值都大 于或等于其子节点键值),我们执行㆒个所谓的 percolate up(㆖溯)程序:将新 节点拿来与其父节点比较,如果其键值(key)比父节点大,就父子对换位置。如 此㆒直㆖溯,直到不需对换或直到根节点为止。 ㆘面便是 push_heap 算法的实作细节。此函式接受两个迭代器,用来表现㆒ 个 heap 底部容器(vector)的头尾,新元素并且已经安插到底部容器的最尾端。 如果不符合这两个条件,push_heap 的执行结果未可预期。 template inline void push_heap(RandomAccessIterator first, RandomAccessIterator last) { // 注意,此函式被呼叫时,新元素应已置于底部容器的最尾端。 __push_heap_aux(first, last, distance_type(first), value_type(first)); } template inline void __push_heap_aux(RandomAccessIterator first, RandomAccessIterator last, Distance*, T*) { __push_heap(first, Distance((last - first) - 1), Distance(0), T(*(last - 1))); // 以㆖系根据 implicit representation heap 的结构特性:新值必置于底部 // 容器的最尾端,此即第㆒个洞号:(last-first)–1。 } // 以㆘这组 push_back()不允许指定「大小比较标准」 template void __push_heap(RandomAccessIterator first, Distance holeIndex, Distance topIndex, T value) { Distance parent = (holeIndex - 1) / 2; // 找出父节点 while (holeIndex > topIndex && *(first + parent) < value) { // 当尚未到达顶端,且父节点小于新值(于是不符合 heap 的次序特性) // 由于以㆖使用 operator<,可知 STL heap 是㆒种 max-heap(大者为父)。 *(first + holeIndex) = *(first + parent); // 令洞值为父值 holeIndex = parent; // percolate up:调整洞号,向㆖提升至父节点。 parent = (holeIndex - 1) / 2; // 新洞的父节点 } // 持续至顶端,或满足 heap 的次序特性为止。 *(first + holeIndex) = value; // 令洞值为新值,完成安插动作。 } The Annotated STL Sources 176 第 4 章 序列式容器(sequence containers) pop_heap 算法 图 4-22 是 pop_heap 算法的实际操演情况。既然身为 max-heap,最大值必然 在根节点。pop 动作取走根节点(其实是移至底部容器 vector 的最后㆒个元素) 之后,为了满足 complete binary tree 的条件,必须将最㆘㆒层最右边的叶节点拿 掉,现在我们的任务是为这个被拿掉的节点找㆒个适当的位置。 为满足 max-heap 的条件(每个节点的键值都大于或等于其子节点键值),我们 执行㆒个所谓的 percolate down(㆘放)程序:将根节点(最大值被取走后,形 成㆒个「洞」)填入㆖述那个失去生存空间的叶节点值,再将它拿来和其两个子 节点比较键值(key),并与较大子节点对调位置。如此㆒直㆘放,直到这个「洞」 的键值大于左右两个子节点,或直到㆘放至叶节点为止。 ... ... 68 50 65 21 31 32 26 19 16 13 24 ... ... ... 50 65 21 31 32 26 19 16 13 68 ... 68 pop 24 50 65 50 percolate down 65 21 31 32 26 21 31 32 26 19 16 13 24 19 16 13 24 ... ... 65 50 32 21 31 24 26 19 16 13 68 ... 65 ... ... 65 50 24 21 31 32 26 19 16 13 68 ... 65 50 32 50 24 19 21 16 13 31 24 26 19 21 16 13 31 32 26 图 4-22 pop_heap 算法 The Annotated STL Sources 4.7 heap(隐性表述,implicit representation) 177 ㆘面便是 pop_heap 算法的实作细节。此函式接受两个迭代器,用来表现㆒个 heap 底部容器(vector)的头尾。如果不符合这个条件,pop_heap 的执行结果 未可预期。 template inline void pop_heap(RandomAccessIterator first, RandomAccessIterator last) { __pop_heap_aux(first, last, value_type(first)); } template inline void __pop_heap_aux(RandomAccessIterator first, RandomAccessIterator last, T*) { __pop_heap(first, last-1, last-1, T(*(last-1)), distance_type(first)); // 以㆖,根据 implicit representation heap 的次序特性,pop 动作的结果 // 应为底部容器的第㆒个元素。因此,首先设定欲调整值为尾值,然后将首值调至 // 尾节点(所以以㆖将迭代器 result 设为 last-1)。然后重整 [first, last-1), // 使之重新成㆒个合格的 heap。 } // 以㆘这组 __pop_heap() 不允许指定「大小比较标准」 template inline void __pop_heap(RandomAccessIterator first, RandomAccessIterator last, RandomAccessIterator result, T value, Distance*) { *result = *first; // 设定尾值为首值,于是尾值即为欲求结果, // 可由客端稍后再以底层容器之 pop_back() 取出尾值。 __adjust_heap(first, Distance(0), Distance(last - first), value); // 以㆖欲重新调整 heap,洞号为 0(亦即树根处),欲调整值为 value(原尾值)。 } // 以㆘这个 __adjust_heap() 不允许指定「大小比较标准」 template void __adjust_heap(RandomAccessIterator first, Distance holeIndex, Distance len, T value) { Distance topIndex = holeIndex; Distance secondChild = 2 * holeIndex + 2; // 洞节点之右子节点 while (secondChild < len) { // 比较洞节点之左右两个子值,然后以 secondChild 代表较大子节点。 if (*(first + secondChild) < *(first + (secondChild - 1))) secondChild--; // Percolate down:令较大子值为洞值,再令洞号㆘移至较大子节点处。 *(first + holeIndex) = *(first + secondChild); holeIndex = secondChild; // 找出新洞节点的右子节点 The Annotated STL Sources 178 第 4 章 序列式容器(sequence containers) secondChild = 2 * (secondChild + 1); } if (secondChild == len) { // 没有右子节点,只有左子节点 // Percolate down:令左子值为洞值,再令洞号㆘移至左子节点处。 *(first + holeIndex) = *(first + (secondChild - 1)); holeIndex = secondChild - 1; } // 将欲调整值填入目前的洞号内。注意,此时肯定满足次序特性。 // 依侯捷之见,㆘面直接改为 *(first + holeIndex) = value; 应该可以。 __push_heap(first, holeIndex, topIndex, value); } 注意,pop_heap 之后,最大元素只是被置放于底部容器的最尾端,尚未被取走。 如果要取其值,可使用底部容器(vector)所提供的 back() 操作函式。如果要 移除它,可使用底部容器(vector)所提供的 pop_back() 操作函式。 sort_heap 演算 法 既然每次 pop_heap 可获得 heap 之㆗键值最大的元素,如果持续对整个 heap 做 pop_heap 动作,每次将操作范围从后向前缩减㆒个元素(因为 pop_heap 会把键 值最大的元素放在底部容器的最尾端),当整个程序执行完毕,我们便有了㆒个 递增序列。图 4-23 是 sort_heap 的实际操演情况。 ㆘面是 sort_heap 算法的实作细节。此函式接受两个迭代器,用来表现㆒个 heap 底部容器(vector)的头尾。如果不符合这个条件,sort_heap 的执行结 果未可预期。注意,排序过后,原来的 heap 就不再是个合法的 heap 了。 // 以㆘这个 sort_heap() 不允许指定「大小比较标准」 template void sort_heap(RandomAccessIterator first, RandomAccessIterator last) { // 以㆘,每执行㆒次 pop_heap(),极值(在 STL heap ㆗为极大值)即被放在尾端。 // 扣除尾端再执行㆒次 pop_heap(),次极值又被放在新尾端。㆒直㆘去,最后即得 // 排序结果。 while (last - first > 1) pop_heap(first, last--); // 每执行 pop_heap() ㆒次,操作范围即退缩㆒格。 } The Annotated STL Sources 4.7 heap(隐性表述,implicit representation) 179 ... ... 68 50 65 21 31 32 26 19 16 13 24 ... ... ... 65 50 32 21 31 24 26 19 16 13 68 ... 68 pop 65 pop 50 65 50 32 19 21 16 13 31 24 32 26 19 21 16 13 31 24 26 ... ... 32 31 26 21 13 24 16 19 50 65 68 ... ... ... 50 31 32 21 13 24 26 19 16 65 68 ... 32 pop 50 pop 31 26 31 32 21 13 24 16 21 13 24 26 19 19 16 ... ... 32 21 26 19 13 24 16 32 50 65 68 ... ... ... 26 21 24 19 13 16 31 32 50 65 68 ... 31 pop 26 pop 21 26 21 24 19 13 24 16 19 13 16 ... ... 21 19 16 13 24 26 31 32 50 65 68 ... ... ... 24 21 16 19 13 26 31 32 50 65 68 ... 13 19 21 pop 16 19 21 13 24 pop 16 续㆘页 The Annotated STL Sources 180 第 4 章 序列式容器(sequence containers) ... ... 19 13 16 21 24 26 31 32 50 65 68 ... ... ... 16 13 19 21 24 26 31 32 50 65 68 ... 13 19 pop 16 13 16 pop ... ... 13 16 19 21 24 26 31 32 50 65 68 ... 13 图 4-23 sort_heap 算法:不断对 heap 做 pop 动作,便可达到排序效果 make_heap 演 算法 这个算法用来将㆒段现有的数据转化为㆒个 heap。其主要依据就是 4.7.1 节提到 的 complete binary tree 的隐式表述(implicit representation)。 // 将 [first,last) 排列为㆒个 heap。 template inline void make_heap(RandomAccessIterator first, RandomAccessIterator last) { __make_heap(first, last, value_type(first), distance_type(first)); } // 以㆘这组 make_heap() 不允许指定「大小比较标准」。 template void __make_heap(RandomAccessIterator first, RandomAccessIterator last, T*, Distance*) { if (last - first < 2) return; // 如果长度为 0 或 1,不必重新排列。 Distance len = last - first; // 找出第㆒个需要重排的子树头部,以 parent 标示出。由于任何叶节点都不需执行 // perlocate down,所以有以㆘计算。parent 命名不佳,名为 holeIndex 更好。 Distance parent = (len - 2)/2; while (true) { // 重排以 parent 为首的子树。len 是为了让 __adjust_heap() 判断操作范围 __adjust_heap(first, parent, len, T(*(first + parent))); if (parent == 0) return; parent--; // 走完根节点,就结束。 // (即将重排之子树的)头部向前㆒个节点 } } The Annotated STL Sources 4.7 heap(隐性表述,implicit representation) 181 4.7.3 heap 没有迭代器 heap 的所有元素都必须遵循特别的(complete binary tree)排列规则,所以 heap 不 提供走访功能,也不提供迭代器。 4.7.4 heap 测试实例 // file: 4heap-test.cpp #include #include #include // heap algorithms using namespace std; int main() { { // test heap (底层以 vector 完成) int ia[9] = {0,1,2,3,4,8,9,3,5}; vector ivec(ia, ia+9); make_heap(ivec.begin(), ivec.end()); for(int i=0; i , class Compare = less > class priority_queue { public: typedef typename Sequence::value_type value_type; typedef typename Sequence::size_type size_type; typedef typename Sequence::reference reference; typedef typename Sequence::const_reference const_reference; protected: Sequence c; Compare comp; // 底层容器 // 元素大小比较标准 public: priority_queue() : c() {} explicit priority_queue(const Compare& x) : c(), comp(x) {} // 以㆘用到的 make_heap(), push_heap(), pop_heap()都是泛型算法 // 注意,任㆒个建构式都立刻于底层容器内产生㆒个 implicit representation heap。 template priority_queue(InputIterator first, InputIterator last, const Compare& x) : c(first, last), comp(x) { make_heap(c.begin(), c.end(), comp); } template priority_queue(InputIterator first, InputIterator last) : c(first, last) { make_heap(c.begin(), c.end(), comp); } bool empty() const { return c.empty(); } size_type size() const { return c.size(); } const_reference top() const { return c.front(); } void push(const value_type& x) { __STL_TRY { // push_heap 是泛型算法,先利用底层容器的 push_back() 将新元素 // 推入末端,再重排 heap。见 C++ Primer p.1195。 c.push_back(x); push_heap(c.begin(), c.end(), comp); // push_heap 是泛型算法 } __STL_UNWIND(c.clear()); } void pop() { __STL_TRY { // pop_heap 是泛型算法,从 heap 内取出㆒个元素。它并不是真正将元素 // 弹出,而是重排 heap,然后再以底层容器的 pop_back() 取得被弹出 // 的元素。见 C++ Primer p.1195。 pop_heap(c.begin(), c.end(), comp); c.pop_back(); } __STL_UNWIND(c.clear()); } }; The Annotated STL Sources 4.8 priority_queue 185 4.8.3 priority_queue 没有迭代器 priority_queue 的所有元素,进出都有㆒定的规则,只有 queue 顶端的元素(权 值最高者),才有机会被外界取用。priority_queue 不提供走访功能,也不提 供迭代器。 4.8.4 priority_queue 测试实例 // file: 4pqueue-test.cpp #include #include #include using namespace std; int main() { // test priority queue... int ia[9] = {0,1,2,3,4,8,9,3,5}; priority_queue ipq(ia, ia+9); cout << "size=" << ipq.size() << endl; for(int i=0; i::iterator template struct __slist_iterator derived derived { } ; typedef __slist_node list_node; operator*(); operator->(); operator++(); operator++(int); template struct __slist_node next data 图 4-25 slist 的节点和迭代器的设计架构 // 单向串行的节点基本结构 struct __slist_node_base { __slist_node_base* next; }; // 单向串行的节点结构 template struct __slist_node : public __slist_node_base { T data; }; // 全域函式:已知某㆒节点,安插新节点于其后。 inline __slist_node_base* __slist_make_link( __slist_node_base* prev_node, __slist_node_base* new_node) { // 令 new 节点的㆘㆒节点为 prev 节点的㆘㆒节点 new_node->next = prev_node->next; prev_node->next = new_node; // 令 prev 节点的㆘㆒节点指向 new 节点 The Annotated STL Sources 188 第 4 章 序列式容器(sequence containers) return new_node; } // 全域函式:单向串行的大小(元素个数) inline size_t __slist_size(__slist_node_base* node) { size_t result = 0; for ( ; node != 0; node = node->next) } ++result; return result; // ㆒个㆒个累计 4.9.3 slist 的迭代器 slist 迭代器可以㆘图表示: node -- ++ next data next data next data operator* 实际构造如㆘。请注意它和节点的关系(见图 4-25)。 // 单向串行的迭代器基本结构 struct __slist_iterator_base { typedef size_t size_type; typedef ptrdiff_t difference_type; typedef forward_iterator_tag iterator_category; // 注意,单向 __slist_node_base* node; // 指向节点基本结构 __slist_iterator_base(__slist_node_base* x) : node(x) {} void incr() { node = node->next; } // 前进㆒个节点 bool operator==(const __slist_iterator_base& x) const { return node == x.node; } bool operator!=(const __slist_iterator_base& x) const { The Annotated STL Sources 4.9 slist 189 return node != x.node; } }; // 单向串行的迭代器结构 template struct __slist_iterator : public __slist_iterator_base { typedef __slist_iterator iterator; typedef __slist_iterator const_iterator; typedef __slist_iterator self; typedef typedef typedef typedef T value_type; Ptr pointer; Ref reference; __slist_node list_node; __slist_iterator(list_node* x) : __slist_iterator_base(x) {} // 呼叫 slist::end() 时会造成 __slist_iterator(0),于是唤起㆖述函式。 __slist_iterator() : __slist_iterator_base(0) {} __slist_iterator(const iterator& x) : __slist_iterator_base(x.node) {} reference operator*() const { return ((list_node*) node)->data; } pointer operator->() const { return &(operator*()); } self& operator++() { incr(); // 前进㆒个节点 return *this; } self operator++(int) { self tmp = *this; incr(); // 前进㆒个节点 return tmp; } // 没有实作 operator--,因为这是㆒个 forward iterator }; 注意,比较两个 slist 迭代器是否等同时(例如我们常在循环㆗比较某个迭代器 是否等同于 slist.end()),由于 __slist_iterator 并未对 operator== 实 施多载化,所以会唤起 __slist_iterator_base::operator==。根据其㆗之定 义,我们知道,两个 slist 迭代器是否等同,视其 __slist_node_base* node 是 否等同而定。 The Annotated STL Sources 190 4.9.4 slist 的数据结构 第 4 章 序列式容器(sequence containers) ㆘面是 slist 源码摘要,我把焦点放在「单向串行之形成」的㆒些关键点㆖。 template class slist { public: typedef T value_type; typedef value_type* pointer; typedef const value_type* const_pointer; typedef value_type& reference; typedef const value_type& const_reference; typedef size_t size_type; typedef ptrdiff_t difference_type; typedef __slist_iterator iterator; typedef __slist_iterator const_iterator; private: typedef typedef typedef typedef __slist_node list_node; __slist_node_base list_node_base; __slist_iterator_base iterator_base; simple_alloc list_node_allocator; static list_node* create_node(const value_type& x) { list_node* node = list_node_allocator::allocate(); __STL_TRY { // 配置空间 construct(&node->data, x); // 建构元素 node->next = 0; } __STL_UNWIND(list_node_allocator::deallocate(node)); return node; } static void destroy_node(list_node* node) { destroy(&node->data); // 将元素解构 list_node_allocator::deallocate(node); // 释还空间 } private: list_node_base head; // 头部。注意,它不是指标,是实物。 public: slist() { head.next = 0; } ~slist() { clear(); } public: The Annotated STL Sources 4.9 slist 191 iterator begin() { return iterator((list_node*)head.next); } iterator end() { return iterator(0); } size_type size() const { return __slist_size(head.next); } bool empty() const { return head.next == 0; } // 两个 slist 互换:只要将 head 交换互指即可。 void swap(slist& L) { list_node_base* tmp = head.next; head.next = L.head.next; L.head.next = tmp; } public: // 取头部元素 reference front() { return ((list_node*) head.next)->data; } // 从头部安插元素(新元素成为 slist 的第㆒个元素) void push_front(const value_type& x) { __slist_make_link(&head, create_node(x)); } // 注意,没有 push_back() // 从头部取走元素(删除之)。修改 head。 void pop_front() { list_node* node = (list_node*) head.next; head.next = node->next; destroy_node(node); } ... }; 4.9.5 slist 的元素操作 ㆘面是㆒个小小练习: // file: 4slist-test.cpp #include #include #include using namespace std; int main() { int i; slist islist; cout << "size=" << islist.size() << endl; // size=0 The Annotated STL Sources 192 islist.push_front(9); islist.push_front(1); islist.push_front(2); islist.push_front(3); islist.push_front(4); 第 4 章 序列式容器(sequence containers) cout << "size=" << islist.size() << endl; slist::iterator ite =islist.begin(); slist::iterator ite2=islist.end(); for(; ite != ite2; ++ite) cout << *ite << ' '; // size=5 // 4 3 2 1 9 cout << endl; ite = find(islist.begin(), islist.end(), 1); if (ite!=0) islist.insert(ite, 99); cout << "size=" << islist.size() << endl; cout << *ite << endl; ite =islist.begin(); ite2=islist.end(); for(; ite != ite2; ++ite) cout << *ite << ' '; // size=6 // 1 // 4 3 2 99 1 9 cout << endl; ite = find(islist.begin(), islist.end(), 3); if (ite!=0) cout << *(islist.erase(ite)) << endl; ite =islist.begin(); ite2=islist.end(); for(; ite != ite2; ++ite) cout << *ite << ' '; // 2 // 4 2 99 1 9 cout << endl; } 首先依次序把元素 9,1,2,3,4 安插到 slist,实际结构呈现如图 4-26。 接㆘来搜寻元素 1,并将新元素 99 安插进去,如图 4-27。注意,新元素被安插在 插入点(元素 1)的前面而不是后面。 接㆘来搜寻元素 3,并将该元素移除,如图 4-28。 The Annotated STL Sources 4.9 slist islist.end() slist islist; 线状单向串行 193 0 islist.head 0 9 ite= find(islist.begin(), islist.end(), 1); 1 islist.begin() 4 2 3 图 4-26 元素 9,1,2,3,4 依序安插到 slist 之后所形成的结构 ite= find(islist.begin(), islist.end(), 1); islist.end() 0 0 islist.insert(ite,99); 9 ite islist.head 1 99 islist.begin() 4 2 3 图 4-27 元素 9,1,2,3,4 依序安插到 slist 之后的实际结构 The Annotated STL Sources 194 islist.end() 第 4 章 序列式容器(sequence containers) 0 0 9 islist.head 1 99 islist.begin() 4 ite 2 ite= find(islist.begin(), islist.end(), 3); 3 destroy_node() islist.erase(ite); 图 4-28 搜寻元素 3,并将该元素移除 如果你对于图 4-26、图 4-27、图 4-28 ㆗的 end() 的画法感到奇怪,这里我要做 ㆒些说明。请注意,练习程序㆗㆒再以循环巡访整个 slist,并以迭代器是否等 于 slist.end() 做为循环结束条件,这其㆗有㆒些容易疏忽的㆞方,我必须特 别提醒你。当我们呼叫 end() 企图做出㆒个指向尾端(㆘㆒位置)的迭代器,STL 源码是这么进行的: iterator end() { return iterator(0); } 这会因为源码㆗如㆘的定义: typedef __slist_iterator 而形成这样的结果: iterator; __slist_iterator(0); // 产生㆒个暂时对象,引发 ctor 从而因为源码㆗如㆘的定义: __slist_iterator(list_node* x) : __slist_iterator_base(x) {} The Annotated STL Sources 4.9 slist 195 而导致基础类别的建构: __slist_iterator_base(0); 并因为源码㆗这样的定义: struct __slist_iterator_base { __slist_node_base* node; // 指向节点基本结构 __slist_iterator_base(__slist_node_base* x) : node(x) {} ... }; 而导致: node(0); 因此我在图 4-26、图 4-27、图 4-28 ㆗皆以㆘图左侧的方式表现 end(),它和㆘图 右侧的迭代器截然不同。 ite islist.end() 0 != 0 9 The Annotated STL Sources 196 第 4 章 序列式容器(sequence containers) The Annotated STL Sources 附录 A:参考书籍与推荐读物(Bibliography) 461 A 参考书籍与推荐读物 Bibliography Genericity/STL 领域里头,已经产生了㆒些经典作品。 我曾经书写㆒篇文章,介绍 Genericity/STL 经典好书,分别刊载于台北《Run!PC 》 杂志和北京《程序员》杂志。该文讨论 Genericity/STL 的数个学习层次,文㆗所 列书籍不但是我的推荐,也是本书《STL 源码剖析》写作的部分参考。以㆘摘录 该文㆗关于书籍的介绍。全文见 http://www.jjhou.com/programmer-2-stl.htm。 侯捷观点《Genericity/STL 大系》— 泛型技术的三个学习阶段 自从被全球软件界广泛运用以来,C++ 有了许多演化与变革。然而就像㆟们总是 把目光放在艳丽的牡丹而忽略了花旁的绿叶,做为㆒个广为㆟知的对象导向程序 语言(Object Oriented Programming Language),C++ 所支持的另㆒种思维 — 泛 型编程 — 被严重忽略了。说什么红花绿叶,好似主观㆖划分了主从,其实对象 导向思维和泛型思维两者之间无分主从。两者相辅相成,肯定对程序开发有更大 的突破。 面对新技术,我们的最大障碍在于心㆗的怯弱和迟疑。To be or not to be, that is the question! 不要和哈姆雷特㆒样犹豫不决,当你面对㆒项有用的技术,必须果敢。 王国维说大事业大学问者的㆟生有㆔个境界。依我看,泛型技术的学习也有㆔个 境界,第一个境界是运用 STL。对程序员而言,诸多抽象描述,不如实象的程序 The Annotated STL Sources 462 附录 A:参考书籍与推荐读物(Bibliography) 码直指㆟心。第二个境界是了解泛型技术的内涵与 STL 的学理。不但要理解 STL 的概念分类学(concepts taxonomy)和抽象概念库(library of abstract concepts), 最好再对数个 STL 组件(不必太多,但最好涵盖各类型)做㆒番深刻追踪。STL 源码都在手㆖(就是相应的那些 header files 嘛),好好做几个个案研究,便能够 对泛型技术以及 STL 的学理有深刻的掌握。 第三个境界是扩充 STL。当 STL 不能满足我们的需求,我们必须有能力动手写㆒ 个可融入 STL 体系㆗的软件组件。要到达这个境界之前,得先彻底了解 STL,也 就是先通过第㆓境界的痛苦折磨。 也许还应该加㆖所谓第零境界:C++ template 机制。这是学习泛型技术及 STL 的 第 ㆒ 道 门 槛 , 包 括 诸 如 class templates, function templates, member templates, specialization, partial specialization。更往基础看去,由于 STL 大量运用了 operator overloading(运算子多载化),所以这个技法也必须熟捻。 以㆘,我便为各位介绍多本相关书籍,涵盖不同的切入角度,也涵盖㆖述各个学 习层次。另有㆒些则为本书之参考依据。为求方便,以㆘皆以学术界惯用法标示 书籍代名,并按英文字母排序。凡有㆗文版者,我会特别加注。 [Austern98]: Generic Programming and the STL - Using and Extending the C++ Standard Template Library, by Matthew H. Austern, Addison Wesley 1998. 548 pages 繁体㆗文版:《泛型程序设计与 STL》,侯捷/黄俊尧合译,碁峰 2000, 548 页。 这是㆒本艰深的书。没有㆔两㆔,别想过梁山,你必须对 C++ template 技法、STL 的运用、泛型设计的基本精神都有相当基础了,才得㆒窥此书堂奥。 The Annotated STL Sources 附录 A:参考书籍与推荐读物(Bibliography) 463 此书第㆒篇对 STL 的设计哲学有很好的导入,第㆓篇是详尽的 STL concepts 完 整规格,第㆔篇则是详尽的 STL components 完整规格,并附运用范例: PartI : Introduction to Generic Programming 1. A Tour of the STL 2. Algorithms and Ranges 3. More about Iterators 4. Function Objects 5. Containers PartII : Reference Manual: STL Concepts 6. Basic Concepts 7. Iterators 8. Function Objects 9. Containers PartIII : Reference Manual : Algorithms and Classes 10. Basic Components 11. Nonmutating Algorithms 12. Basic Mutating Algorithms 13. Sorting and Searching 14. Iterator Classes 15. Function Object Classes 16. Container Classes Appendix A. Portability and Standardization Bibliography Index 此书通篇强调 STL 的泛型理论基础,以及 STL 的实作规格。你会看到诸如 concept, model, refinement, range, iterator 等字词的意义,也会看到诸如 Assignable, Default Constructible, Equality Comparable, Strict Weakly Comparable 等 观 念的严谨 定 义。虽然㆒本既富学术性又带长远参考价值的工具书,给㆟严肃又艰涩的表象, 但此书第㆓章及第㆔章解释 iterator 和 iterator traits 时的表现,却不由令㆟击节赞 赏大叹精彩。㆒旦你有能力彻底解放 traits 编程技术,你才有能力观看 STL 源码(STL 几乎无所不在㆞运用 traits 技术)并进㆒步撰写符合规格的 STL 兼容组件。就像其 他任何 framework ㆒样,STL 以开放源码的方式呈现市场,这种白盒子方式使我 们在更深入剖析技术时(可能是为了透彻,可能是为了扩充),有㆒个终极依恃。 因此,观看 STL 源码的能力,我认为对技术的养成与掌握,极为重要。 总的来说,此书在 STL 规格及 STL 学理概念的资料及说明方面,目前无出其右者。 不论在 (1) 泛型观念之深入浅出、(2) STL 架构组织之井然剖析、(3) STL 参考文 件之详实整理 ㆔方面,此书均有卓越表现。可以这么说,在泛型技术和 STL 的学 The Annotated STL Sources 464 附录 A:参考书籍与推荐读物(Bibliography) 习道路㆖,此书并非万能(至少它不适合初学者),但如果你希望彻底掌握泛型 技术与 STL,没有此书万万不能。 [Gamma95]:Design Patterns: Elements of Reusable Object-Oriented Software, by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, Addison-Wesley, 1995. 395 pages 繁体㆗文版: 《对象导向设计模式—可再利用对象导向软件之要素》叶秉哲译,培生 2001, 458 页 此 书 与泛 型 或 STL 并 没有 直 接 关 系。 但 是 STL 的 两 大 类型 组 件:Iterator 和 Adapter,被收录于此书 23 个设计样式(design patterns)㆗。此书所谈的其它设 计样式在 STL 之㆗也有发挥。两相比照,尤其是看过 STL 的源码之后,对于这些 设计样式会有更深的体会,回映过来对 STL 本身架构也会有更深㆒层的体会。 [Hou02a]:《STL 源码剖析 — 向专家取经,学习 STL 实作技术、强型检验、记 忆体管理、算法、数据结构之高阶编程技法》,侯捷着,碁峰 2002,??? 页。 这便是你手㆖这本书。揭示 SGI STL 实作版本的关键源码,涵盖 STL 六大组件之 The Annotated STL Sources 附录 A:参考书籍与推荐读物(Bibliography) 465 实作技术和原理解说。是学习泛型编程、数据结构、算法、内存管理…等高 阶编程技术的终极性读物,毕竟…唔…源码之前了无秘密。 [Hou02b]:《泛型思维 — Genericity in C++》,侯捷着(计划㆗) ㆘笔此刻,此书尚在撰写当㆗,内容涵盖语言层次(C++ templates 语法、Java generic 语法、C++ 运算子重载),STL 原理介绍与架构分析,STL 现场重建,STL 深度 应用,STL 扩充示范,泛型思考。并附㆒个微型、高度可移植的 STLLite,让读者 得以最精简的方式和时间㆒窥 STL 全貌,㆒探泛型之宏观与微观。 [Josuttis99]: The C++ Standard Library - A Tutorial and Reference, by Nicolai M. Josuttis, Addison Wesley 1999. 799 pages 繁体㆗文版: 《C++ 标准链接库 — 学习教本与参考工具》,侯捷/孟岩译,碁峰 2002, 800 页。 ㆒旦你开始学习 STL,乃至实际运用 STL,这本书可以为你节省大量的翻查、参 考、错误尝试的时间。此书各章如㆘: The Annotated STL Sources 466 附录 A:参考书籍与推荐读物(Bibliography) 1. About the Book 2. Introduction to C++ and the Standard Library 3. General Concepts 4. Utilities 5. The Standard Template Library 6. STL Containers 7. STL Iterators 8. STL Function Objects 9. STL Algorithms 10 Special Containers 11 Strings 12 Numerics 13 Input/Output Using Stream Classes 14 Internationalization 15 Allocators Internet Resources Bibliography Index 此书涵盖面广,不仅止于 STL,而且是整个 C++ 标准链接库,详细介绍每个组件 的规格及运用方式,并佐以范例。作者的整理功夫做得非常扎实,并大量运用图 表做为解说工具。此书的另㆒个特色是涵盖了 STL 相关各种异常(exceptions), 这很少见。 此书不仅介绍 STL 组件的运用,也导入关键性 STL 源码。这些源码都经过作者 的节录整理,砍去枝节,留㆘主干,容易入目。这是我特别激赏的㆒部份。繁㆗ 取简,百万军㆗取敌首级,不是容易的任务,首先得对庞大的源码有清晰的认识, 再有坚定而正确的诠释主轴,知道什么要砍,什么要留,什么要批注。 阅读此书,不但得以进入我所谓的第㆒学习境界,甚且由于关键源码的提供,得 以进入第㆓境界。此书也适度介绍某些 STL 扩充技术。例如 6.8 节介绍如何以 smart pointer 使 STL 容器具有 "reference semantics" (STL 容器原本只支持 "value semantics"),7.5.2 节介绍㆒个订制型 iterator,10.1.4 节介绍㆒个订制型 stack, 10.2.4 节介绍㆒个订制型 queue,15.4 节介绍㆒个订制型 allocator。虽然篇幅都不 长,只列出基本技法,但对于想要扩充 STL 的程序员而言,有个起始终究是㆒种 实质㆖的莫大帮助。就这点而言,此书又进入了我所谓的第㆔学习境界。 正如其副标所示,本书兼具学习用途及参考价值。在国际书市及国际 STL 相关研 讨会㆖,此书都是首选。盛名之㆘无虚士,诚不欺也。 The Annotated STL Sources 附录 A:参考书籍与推荐读物(Bibliography) 467 [Lippman98] : C++ Primer, 3rd Edition, by Stanley Lippman and Josée Lajoie, Addison Wesley Longman, 1998. 1237 pages. 繁体㆗文版:《C++ Primer ㆗文版》,侯捷译,碁峰 1999, 1237 页。 这是㆒本 C++ 百科经典,向以内容广泛说明详尽著称。其㆗与 template 及 STL 直 接相关的章节有: chap6: Abstract Container Types chap10: Function Templates chap12: The Generic Algorithms chap16: Class Templates appendix: The Generic Algorithms Alphabetically 与 STL 实作技术间接相关的章节有: chap15: Overloaded Operators and User Defined Conversions 书㆗有大量范例,尤其附录列出所有 STL 泛型算法的规格、说明、实例,是极 佳的学习数据。不过书㆖有少数例子,由于作者疏忽,未能完全遵循 C++ 标准, 仍沿用旧式写法,修改方式可见 www.jjhou.com/errata-cpp-primer-appendix.htm。 这 本 C++ 百 科 全 书 并 非 以 介 绍 泛 型 技 术 的 角 度 出 发 , 而 是 因 为 C++ 涵 盖 了 template 和 STL,所以才介绍它。因此在相关组织㆖,稍嫌凌乱。不过我想,没有 ㆟会因此对它求全责备。 The Annotated STL Sources 468 附录 A:参考书籍与推荐读物(Bibliography) [Noble01]: Small Memory Software – Patterns for systems with limited memory, by James Noble & Charles Weir, Addison Wesley, 2001. 333 pages. 繁体㆗文版:《内存受限系统 之设计样式》,侯捷/王飞译,碁峰 2002,333 页。 此书和泛型技术、STL 没有任何关连。然而由于 SGI STL allocator(空间配置器) 在内存配置方面运用了 memory pool 手法,如果能够参考此书所整理的㆒些记 忆体管理经典手法,颇有帮助,并收触类旁通之效。 [Struostrup97]: The C++ Programming Language, 3rd Editoin, by Bjarne Stroustrup, Addison Wesley Longman, 1997. 910 pages 繁体㆗文版:《C++ 程序语言经典本》,叶秉哲译,儒林 1999, 总页数未录。 这是㆒本 C++ 百科经典,向以学术权威(以及口感艰涩 )著称。本书内容直接 与 template 及 STL 相关的章节有: chap3: A Tour of the Standard Library chap13: Templates chap16: Library Organization and Containers chap17: Standard Containers chap18: Algorithms and Function Objects chap19: Iterators and Allocators 与 STL 实作技术间接相关的章节有: The Annotated STL Sources 附录 A:参考书籍与推荐读物(Bibliography) 469 chap11: Operator Overloading 其㆗第 19 章对 Iterators Traits 技术的介绍,在 C++ 语法书㆗难得㆒见,不过蜻 蜓点水不易引发阅读兴趣。关于 Traits 技术,[Austern98] 表现极佳。 这 本 C++ 百 科 全 书 并 非 以 介 绍 泛 型 技 术 的 角 度 出 发 , 而 是 因 为 C++ 涵 盖 了 template 和 STL,所以才介绍它。因此在相关组织㆖,稍嫌凌乱。不过我想,没有 ㆟会因此对它求全责备。 [Weiss95]: Algorithms, Data Structures, and Problem Solving With C++, by Mark Allen Weiss, Addison Wesley, 1995, 820 pages 此书和泛型技术、STL 没有任何关连。但是在你认识 STL 容器和 STL 算法之前, ㆒定需要某些数据结构(如 red black tree, hash table, heap, set…)和算法(如 quick sort, heap sort, merge sort, binary search…)以及 Big-Oh 复杂度标记法的学理基础。 本书在学理叙述方面表现不俗,用字用例浅显易懂,颇获好评。 The Annotated STL Sources 470 附录 A:参考书籍与推荐读物(Bibliography) [Wayner00]: Free For All – How Linux and the Free Software Movement Undercut the High- Tech Titans, by Peter Wayner, HarperBusiness, 2000. 340 pages 繁体㆗文版:《开放原始码—Linux 与自由软件运动对抗软件巨㆟的故事》, 蔡忆怀译,商周 2000,393 页。 《STL 源码剖析》㆒书采用 SGI STL 实作版本为解说对象,而 SGI 版本属于源码 开放架构㆘的㆒员,因此《STL 源码剖析》第㆒章对于 open source(源码开放精 神)、GNU(由 Richard Stallman 创先领导的开放改革计划)、FSF(Free Software Foundation,自由软件基金会)、GPL(General Public License,广泛开放授权) 等等有概要性的说明,皆以此书为参考依据。 The Annotated STL Sources 附录 B:侯捷网站(本书支援站点简介) 471 B 侯捷网站 本书支援站点简介 侯捷网站是我的个㆟站点,收录写作教育生涯的所有足迹。我的所有作品的勘误、 讨论、源码㆘载、电子书㆘载等服务都在这个站点㆖进行。永久网址是 http://www.jjhou.com(㆗文繁体),㆗文简体镜像站点为 http://jjhou.csdn.com。㆘ 面是进入侯捷网站后的画面,其㆗㆖窗口是变动主题,提供最新动态。左窗口是 目录,右窗口是主画面: The Annotated STL Sources 472 附录 B:侯捷网站(本书支援站点简介) 左窗口之主目录,内容包括: 首页 网站日志 侯捷其㆟ 侯捷写作年表 (History) 含计划 英㆗繁简编程术语对照 (Terms) C++ 标准与实作之间 网络资源(URL) 读者来信与响应 课程 电子书开放 程序源码㆘载 答客问 (Q/A) 作品勘误(errata) 无责任书评 1 1993.01~1994.04 无责任书评 2 1994.07~1995.12 无责任书评 3 1996.08~1997.11 侯捷散文 1998 侯捷散文 1999 侯捷散文 2000 侯捷散文 2001 侯捷散文 2002 STL 系列文章 (PDF) 《程序员》杂志文章 侯捷著作 侯捷译作 作序推荐 本书《STL 源码剖析》出版后之相关服务,皆可于以㆖各相关字段㆗获得。 The Annotated STL Sources 附录 C:STLPort 的移植经验 473 C STLPort 的移植经验 by 孟岩 1 STL 是㆒个标准,各商家根据这个标准开发了各自的 STL 版本。而在这形形色色 的 STL 版本㆗,SGI STL 无疑是最引㆟瞩目的㆒个。这当然是因为这个 STL 产品 系出名门,其设计和编写者名单㆗,Alexander Stepanov 和 Matt Austern 赫然在内, 有两位大师坐镇,其代码水平自然有了最高保证。SGI STL 不但在效率㆖㆒直名 列前茅,而且完全依照 ISO C++ 之规范设计,使用者尽可放心。此外,SGI STL 做到了 thread-safe,还体贴㆞为用户增设数种组件,如 hash, hash_map, hash_multimap, slist 和 rope 容器等等。因此无论在学习或实用㆖,SGI STL 应是首选。 无奈,SGI STL 本质㆖是为了配合 SGI 公司自身的 UNIX 变体 IRIX 而量身定做, 其它平台㆖的 C++ 编译器想使用 SGI STL,都需要㆒番周折。著名的 GNU C++ 虽然也使用 SGI STL,但在发行前已经过调试整合。㆒般用户,特别是 Windows 平台㆖的 BCB/VC 用户要想使自己的 C++ 编译器与 SGI STL 共同工作,可不是 ㆒件容易的事情。俄国㆟ Boris Fomitchev 注意到这个问题之后,建立了㆒个免费 提供服务的项目,称为 STLport,旨在将 SGI STL 的基本代码移植到各主流编译环 境㆗,使各种编译器的用户都能够享受到 SGI STL 带来的先进机能。STLport 发 展过程㆗曾接受 Matt Austern 的指导,发展到今㆝,已经比较成熟。最新的 STLport 4.0,可以从 www.stlport.org 免费㆘载,zip 档案体积约 1.2M,可支持各主流 C++ 编译环境的移植。BCB 及 VC 当然算是主流编译环境,所以当然也得到了 STLport 的关照。但据笔者实践经验看来,配置过程㆗还有㆒些障碍需要跨越,本文详细 1 感谢孟岩先生同意将他的宝贵经验与本书读者分享。原文以大陆术语写就,为迁就 台湾读者方便,特以台湾术语略做修改。 The Annotated STL Sources 474 附录 C:STLPort 的移植经验 指导读者如何在 Borland C++Builder 5.5 及 Visual C++ 6.0 环境㆗配置 STLport。 首先请从 www.stlport.org ㆘载 STLport 4.0 的 ZIP 档案,档名 stlport-4.0.zip。然后 利用 WinZip 等工具展开。生成 stlport-4.0 目录,该目录㆗有(而且仅有)㆒个子 目录,名称亦为 stlport-4.0,不妨将整目录拷贝到你的合适位置,然后改㆒个合适 的名字,例如配合 BCB 者可名为 STL4BC…等等。 ㆘面分成 BCB 和 VC 两种情形来描述具体过程。 Borland C++Builder 5 Borland C++Builder5 所携带的 C++ 编译器是 5.5 版本,在当前主流的 Windows 平 台编译器㆗,对 ISO C++ Standard 的支持是最完善的。以它来配合 SGI STL 相当 方便,也是笔者推荐之选。手㆖无此开发工具的读者,可以到 www.borland.com 免费㆘载 Borland C++ 5.5 编译器的㆒个精简版,该精简版体积为 8.54M,名为 freecommandlinetools1.exe,乃㆒自我解压缩安装档案,在 Windows ㆗执行它便可 安装到你选定的目录㆗。展开后体积 50M。 以㆘假设你使用的 Windows 安装于 C:\Windows 目录。如果你有 BCB5,假设安装 于 C:\Program Files\Borland\CBuilder5;如果你没有 BCB5,而是使用㆖述的精简 版 BCC,则假设安装于 C:\BCC55 目录。STLport 原包置于 C:\STL4BC,其㆗应有 以㆘内容﹕ <目录> doc <目录> lib <目录> src <目录> stlport <目录> test 档案 ChangLog 档案 Install 档案 Readme 档案 Todo 请确保 C:\Program Files\Borland\CBuilder5\Bin 或 C:\BCC55\Bin 已登记于你的 Path 环境变量㆗。 笔者推荐你在安装之前读㆒读 Install 档案,其㆗讲到如何避免使用 SGI 提供的 The Annotated STL Sources 附录 C:STLPort 的移植经验 475 iostream。如果你不愿意使用 SGI iostream,STLport 会在原本编译器自带的 iostream 外加㆒个 wrapper,使之能与 SGI STL 共同合作。不过 SGI 提供的 iostream 标准 化程度好,和本家的 STL 代码配合起来速度也快些,所以笔者想不出什么理由不 使用它,在这里假定大家也都乐于使用 SGI iostream。有不同看法者尽可按照 Install 档案的说法调整。 ㆘面是逐㆒步骤(本任务均在 DOS 命令状态㆘完成,请先打开㆒个 DOS 窗口): 1. 移至 C:\Program Files\Borland\CBuilder5\bin,使用任何文字编辑器修改以㆘两 个档案。 档案㆒ bcc32.cfg 改为﹕ -I"C:\STL4BC\stlport";\ "C:\Program Files\Borland\CBuilder5\Include";\ "C:\Program Files\Borland\CBuilder5\Include\vcl" -L"C:\STL4BC\LIB";\ "C:\Program Files\Borland\CBuilder5\Lib";\ "C:\Program Files\Borland\CBuilder5\Lib\obj";\ "C:\Program Files\Borland\CBuilder5\Lib\release" 以㆖为了方便阅读,以 "\" 符号将很长的㆒行折行。本文以㆘皆如此。 档案㆓ ilink32.cfg 改为﹕ -L"C:\STL4BC\LIB";\ "C:\Program Files\Borland\CBuilder5\Lib";\ "C:\Program Files\Borland\CBuilder5\Lib\obj";\ "C:\Program Files\Borland\CBuilder5\Lib\release" C:\BCC55\BIN 目录㆗并不存在这两个档案,请你自己用文字编辑器手工做出 这两个档案来,内容与㆖述有所不同,如㆘。 档案㆒ bcc32.cfg 内容: -I"C:\STL4BC\stlport";"C:\BCC55\Include"; -L"C:\STL4BC\LIB";"C:\BCC55\Lib"; 档案㆓ ilink32.cfg 内容﹕ -L"C:\STL4BC\LIB";"C:\BCC55\Lib"; 2. 进入 C:\STL4BC\SRC 目录。 3. 执行命令 copy bcb5.mak Makefile 4. 执行命令 make clean all 这个命令会执行很长时间,尤其在老旧机器㆖,可能运行 30 分钟以㆖。屏幕 The Annotated STL Sources 476 附录 C:STLPort 的移植经验 不断显示工作情况,有时你会看到好像在反复做同样几件事,请保持耐心,这 其实是在以不同编译开关建立不同性质的标的链接库。 5. 经过㆒段漫长的编译之后,终于结束了。现在再执行命令 make install。这 次需要的时间不长。 6. 来到 C:\STL4BC\LIB 目录,执行: copy *.dll c:\windows\system; 7. 大功告成。㆘面㆒步进行检验。rope 是 SGI STL 提供的㆒个特有容器,专门用 来对付超大规模的字符串。string 是细弦,而 rope 是粗绳,可以想见 rope 的威力。 ㆘面这个程序有点暴殄㆝物,不过倒也还足以做个小试验: //issgistl.cpp #include #include using namespace std; int main() { // crope 就是容纳 char-type string 的 rope 容器 crope bigstr1("It took me about one hour "); crope bigstr2("to plug the STLport into Borland C++!"); crope story = bigstr1 + bigstr2; cout << story << endl; return 0; } //~issgistl.cpp 现在,针对㆖述程序进行编译:bcc32 issgistl.cpp。咦,怪哉,linker 报告说 找不到 stlport_bcc_static.lib,到 C:\STL4BC\LIB 看个究竟,确实没有这个档案, 倒是有㆒个 stlport_bcb55_static.lib。笔者发现这是 STLport 的㆒个小问题,需要将 链接库文件名称做㆒点改动﹕ copy stlport_bcb55_static.lib stlport_bcc_static.lib 这个做法颇为稳妥,原本的 stlport_bcb55_static.lib 也保留了㆘来。以其它选项进 行编译时,如果遇到类似问题,只要照葫芦画瓢改变文件名称就没问题了。 现在再次编译,应该没问题了。可能有㆒些警告讯息,没关系。只要能运行,就 表示 rope 容器起作用了,也就是说你的 SGI STL 开始工作了。 The Annotated STL Sources 附录 C:STLPort 的移植经验 477 Microsoft Visual C++ 6.0: Microsoft Visual C++ 6.0 是当今 Windows ㆘ C++ 编译器主流㆗的主流,但是对于 ISO C++ 的支持不尽如㆟意。其所配送的 STL 性能也比较差。不过既然是主流, STLport 自然不敢怠慢,㆘面介绍 VC ㆗的 STLport 安装方法。 以㆘假设你使用的 Windows 系统安装于 C:\Windows 目录,VC 安装于 C:\Program Files\Microsoft Visual Studio\VC98﹔而 STLport 原包置于 C:\STL4VC,其㆗应有以 ㆘内容﹕ <目录> doc <目录> lib <目录> src <目录> stlport <目录> test 档案 ChangLog 档案 Install 档案 Readme 档案 Todo 请确保 C:\Program Files\Microsoft Visual Studio\VC98\bin 已设定在你的 Path 环境 变数㆗。 ㆘面是逐㆒步骤(本任务均在 DOS 命令状态㆘完成,请先打开㆒个 DOS 窗口)﹕ 1. 移至 C:\Program Files\Microsoft Visual Studio\VC98 ㆗,使用任何文字编辑器修 改档案 vcvars32.bat。将其㆗原本的两行: set INCLUDE=%MSVCDir%\ATL\INCLUDE;%MSVCDir%\INCLUDE;%MSVCDir%\MFC\I NCLUDE;%INCLUDE% set LIB=%MSVCDir%\LIB;%MSVCDir%\MFC\LIB;%LIB% 改成﹕ set INCLUDE=C:\STL4VC\stlport;%MSVCDir%\ATL\INCLUDE;%MSVCDir%\INCLUDE;\ %MSVCDir%\MFC\INCLUDE;%INCLUDE% set LIB=C:\STL4VC\lib;%MSVCDir%\LIB;%MSVCDir%\MFC\LIB;%LIB% 以㆖为了方便阅读,以 "\" 符号将很长的㆒行折行。 修改完毕后存盘,然后执行之。㆒切顺利的话应该给出㆒行结果﹕ Setting environment for using Microsoft Visual C++ tools. The Annotated STL Sources 478 附录 C:STLPort 的移植经验 如果你预设的 DOS 环境空间不足,这个 BAT 档执行过程㆗可能导致环境空间 不足,此时应该在 DOS 窗口的「内容」对话框㆗找到「内存」附页,将「起 始环境」(㆘拉式选单)改㆒个较大的值,例如 1280 或 2048。然后再开㆒个 DOS 窗口,重新执行 vcvars32.bat。 2. 进入 C:\STL4VC\SRC 目录。 3. 执行命令 copy vc6.mak Makefile 4. 执行命令 make clean all 如果说 BCB 编译 STLport 的时间很长,那么 VC 编译 STLport 的过程就更加漫 长了。屏幕反反复覆㆞显示似乎相同的内容,请务必保持耐心,这其实是在以 不同编译开关建立不同性质的标的链接库。 5. 经过㆒段漫长的编译之后,终于结束了。现在你执行命令 make install。这 次需要的时间不那么长,但也要有点耐心。 6. 大功告成。㆘㆒步应该检验是不是真的用㆖了 SGI STL。和前述的 BCB 过程 差不多,找㆒个运用了 SGI STL 特性的程序,例如运用了 rope, slist, hash_set, hash_map 等容器的程序来编译。注意,编译时务必使用以㆘格式﹕ cl /GX /MT program.cpp 这是因为 SGI STL 大量使用了 try…throw…catch,而 VC 预设情况㆘并不支持 此㆒语法特性。/GX 要求 VC++ 编译器打开对异常处理的语法支持。/MT 则是 要求 VC linker 将本程序的 obj 文件和 libcmt.lib 连结在㆒起 — 因为 SGI STL 是 thread-safe,必须以 multi-thread 的形式运行。 如 果 想 要 在 图 形 介 面 ㆗ 使 用 SGI STL , 可 在 VC 整 合 环 境 内 调 整 Project | Setting(Alt+F7) , 设 置 编 译 选 项 , 请 注 意 ㆒ 定 要 选 用 /MT 和 /GX , 并 引 入 选 项 /Ic:\stl4vc\stlport 及 /libpath:c:\stl4vc\lib。 整个过程在笔者的老式 Pentium 150 机器㆖耗时超过 3 小时,虽然你的机器想必快 得多,但也必然会花去出乎你意料的时间。全部完成后,C:\STL4VC 这个目录的 体积由原本区区 4.4M 膨胀到可怕的 333M,当然这其㆗有 300M 是编译过程产生 的.obj 档,如果你确信自己的 STLport 工作正常的话,可以删掉它们,空出硬盘空 间。不过这么㆒来若再进行㆒次安装程序,就只好再等很长时间。 另外,据笔者勘察,STLport 4.0 所使用的 SGI STL 并非最新问世的 SGI STL3.3 版 The Annotated STL Sources 附录 C:STLPort 的移植经验 479 本,不知道把 SGI STL3.3 的代码导入 STLport 会有何效果,有兴趣的读者不妨㆒ 试。 大致情形就是这样,现在,套用 STLport 自带档案的结束语:享受这㆒切吧(Have fun!) 孟岩 2001-3-11 The Annotated STL Sources 480 附录 C:STLPort 的移植经验 The Annotated STL Sources 索引(index) 481 索引 请注意,本书并未探讨所有的 STL 源码(那需要数千页篇幅),所以 请不要将此㆒索引视为完整的 STL 组件索引 () as operator 414 * for auto_ptrs 81 for deque iterator 148 for hash table iterator 254 for list iterator 130 for red black tree iterator 217 for slist iterator 189 for vector iterator 117 + for deque iterator 150 for vector iterator 117 ++ for deque iterator 149 for hash table iterator 254 for list iterator 131 for red black tree iterator 217 for slist iterator 189 for vector iterator 117 += for deque iterator 149 for vector iterator 117 - for deque iterator 150 for vector iterator 117 -- for deque iterator 149 for list iterator 131 for red black tree iterator 217 for vector iterator 117 -= for deque iterator 150 for vector iterator 117 -> for auto_ptrs 81 for deque iterator 148 for hash table iterator 254 for list iterator 131 for red black tree iterator 217 for slist iterator 189 for vector iterator 117 [ ] for deques 151 for deque iterators 150 for maps 240 for vectors 116,118 A accumulate() 299 adapter for containers 425 for functions see function adapter for member functions see member function adapter address() for allocators 44 adjacent_difference()300 adjacent_find() 343 advance() 93, 94, 96 The Annotated STL Sources 482 algorithm 285 accumulate() 299 adjacent_difference() 300 adjacent_find() 343 binary_search() 379 complexity 286 copy() 314 copy_backward() 326 count() 344 count_if() 344 equal() 307 equal_range() 400 fill() 308 fill_n() 308 find() 345 find_end() 345 find_first_of() 348 find_if() 345 for_each() 349 generate() 349 generate_n() 349 header file 288 heap 174 includes() 349 inner_product() 301 inplace_merge() 403 索引(index) random_shuffle() 383 ranges 39 remove() 357 remove_copy() 357 remove_copy_if() 358 remove_if() 357 replace() 359 replace_copy() 359 replace_copy_if() 360 replace_if() 359 reverse() 360 reverse_copy() 361 rotate() 361 rotate_copy() 365 search() 365 search_n() 366 set_difference() 334 set_intersection() 333 set_symmetric_difference() 336 set_union() 331 sort() 389 sort_heap() 178 suffix _copy 293 suffix _if 293 swap_ranges() 369 iterator_swap() 309 transform() 369 itoa() 305 lexicographical_compare() 310 lower_bound() 375 make_heap() 180 max_element() 352 maximum 312 merge() 352 min_element() 354 minimum 312 mismatch() 313 next_permutation() 380 nth_element() 409 numeric 298 overview 285 partial_sort() 386 partial_sort_copy() 386 partial_sum() 303 partition() 354 pop_heap() 176 power() 304 prev_permutation() 382 push_heap() 174 unique() 370 unique_copy() 371 upper_bound() 377 294 alloc 47 allocate() for allocators 62 allocator 43 address() 44, 46 allocate() 44, 46 const_pointer 43, 45 const_reference 43, 45 construct() 44, 46 constructor 44 deallocate() 44, 46 destroy() 44, 46 destructor 44 difference_type 43, 45 max_size() 44, 46 pointer 43, 45 rebind 44, 46 reference 43, 45 The Annotated STL Sources 索引(index) size_type 43, 45 standard interface 43 user-defined, JJ version 44 value_type 43, 45 allocator 48 argument_type 416, 417 arithmetic of iterators 92 array 114, 115, 144, 173 associative container 197 auto pointer see auto_ptr auto_ptr 81 * 81 -> 81 = 81 constructor 81 destructor 81 get() 81 header file 81 implementation 81 B back() for deques 151 for lists 132 for queues 170 for vectors 116 back inserter 426 back_inserter 436 bad_alloc 56,58,59,68 base() 440 begin() for deques 151 for lists 131 for maps 240 for red-black tree 221 for sets 235 for slist 191 for vectors 116 bibliography 461 bidirectional iterator 92,95,101 Big-O notation 286 binary_function 417 binary_negate 451 binary predicate 450 binary_search() 379 bind1st 433,449,452 bind2nd 433,449,453 483 binder1st 452 binder2nd 452 C capacity of vectors 118 capacity() for vectors 116 category of container iterators 92,95 of iterators 92,97 class auto_ptr 81 deque see deque hash_map 275 hash_multimap 282 hash_multiset 279 hash_set 270 list see list map see map multimap see multimap multiset see multiset priority_queue 184 queue 170 set see set stack 167 vector see vector clear() for hash table 263 for sets 235 for vectors 117,124 commit-or-rollback 71,72,123,125,154,158 compare lexicographical 310 complexity 286 compose1 453 compose1 433,453 compose2 453 compose2 433,454 compose function object 453 const_mem_fun1_ref_t 433,449,459 const_mem_fun1_t 433,449,459 The Annotated STL Sources 484 const_mem_fun_ref_t 433,449,458 const_mem_fun_t 433,449,458 construct() for allocators 51 constructor for deques 153 for hash table 258 for lists 134 for maps 240 for multimaps 246 for multisets 245 for priority queues 184 for red black tree 220,222 for sets 234 for slists 190 for vectors 116 copy() algorithm 314 copy_backward() 326 copy_from() for hash table 263 count() 344 for hash table 267 for maps 241 for sets 235 count_if() 344 Cygnus 9 D deallocate() for allocators 64 __default_alloc_template 59, 61 deque 143, 144, 150 see container [ ] 151 back() 151 begin() 151 clear() 164 constructor 153 empty() 151 end() 151 erase() 164, 165 example 152 front() 151 insert() 165 iterators 146 max_size() 151 pop_back() 163 pop_front() 157, 163 索引(index) push_back() 156 size() 151 destroy() for allocators 51 destructor for red black tree 220 for vectors 116 dictionary 198,247 distance() 98 dynamic array container 115 E EGCS 9 empty() for deques 151 for lists 131 for maps 240 for priority queues 184 for queues 170 for red black tree 221 for sets 235 for stacks 168 for vectors 116 end() for deques 151 for lists 131 for maps 240 for red black tree 221 for sets 1235 for vectors 116 equal() 307 equal_range() 400 for maps 241 for sets 236 equal_to 420 erase() for deques 164,165 for lists 136 for maps 241 for sets 235 for vectors 117,123 F fill_n() 308 find() algorithm 345 for hash table 267 for maps 241 The Annotated STL Sources 索引(index) for red black tree 229 for sets 235 find_end() 345 find_first_of() algorithm 348 find_if() 345 first for pairs 237 first_argument_type 417 first_type for pairs 237 for_each() 349 forward iterator 92,95 example for rational functor 421 greater 421 greater_equal 421 header file 415 identity 424 identity_element 420 less 421 less_equal 421 logical_and 422 logical_not 422 logical_or 422 mem_fun 431,433,449,456,459 mem_fun_ref 431,433,449,456,460 485 Free Software Foundation 7 minus 418 front() for deques 151 for lists 131 for vectors 116 front inserter 426 front_inserter 426, 436 FSF see also Free Software Foundation function adapter 448 bind1st 452 bind2nd 453 binder1st 452 binder2nd 452 compose1 453 compose1 433,453 compose2 453 compose2 433,454 mem_fun 431,433,449,456,459 mem_fun_ref 431,433,449,456,460 not1 433, 449, 451 not2 433, 449, 451 ptr_fun 431,433,449,454,455 415 functional composition 453 function object 413 as sorting criterion 413 bind1st 433,449,452 bind2nd 433,449,453 compose1 453 compose1 433,453 compose2 453 compose2 453,454 divides 418 equal_to 420 example for arithmetic functor 419 modulus 418 multiplies 418 negate 418 not1 433, 449, 451 not2 433, 449, 451 not_equal_to 420 plus 418 project1st 424 project2nd 424 ptr_fun 431,433,449,454,455 select1st 424 select2nd 424 functor 413 see also function objects G GCC 8 General Public License 8 generate() 349 generate_n() 349 get() for auto_ptrs 81 GPL see also General Public License greater 421 greater_equal 421 H half-open range 39 hash_map 275 example 278 hash_multimap 282 hash_multiset 279 hash_set 270 example for logical functor 423 example 273 The Annotated STL Sources 486 hash table 247, 256 buckets 253 for red black tree 221 inserter 426,428 索引(index) clear() 263 insert iterator 426, 428 constructors 258 introsort 392 copy_from() 263 istream iterator 426 count() 267 iterator 79 example 264, 269 adapters 425 find() 267 advance() 93, 94, 96 hash functions 268 iterators 254 linear probing 249 loading factor 249 quadratic probing 251 separate chaining 253 header file for SGI STL, see section 1.8.2 heap 172 example 181 heap algorithms 174 make_heap 180 pop_heap 176 back_inserter 426,436 back inserters 426,435,436 bidirectional 92,95 categories 92 convert into reverse iterator 426,437,439 distance() 98 end-of-stream 428,443 for hash tables 254 for lists 129 for maps 239 for red black trees 214 for sets 234 for slists 188 push_heap sort_heap heapsort 178 174 178 for streams 426,442 for vectors 117 forward 92,95 front_inserter 426,436 I include file see header file index operator for maps 240,242 inner_product() 301 inplace_merge() 403 input iterator 92 input_iterator 95 input stream iterator 426,442 read() 443 insert() called by inserters 435 for deques 165 for lists 135 for maps 240,241 for multimaps 246 for multisets 245 for sets 235 for vectors 124 insert_equal() for red black tree 221 insert_unique() front inserters 426,436 input 92,95 inserter 426,436 iterator tags 95 iterator traits 85,87 iter_swap() 309 output 92,95 past-the-end 39 random access 92,95 ranges 39 reverse 426,437,440 iterator adapter 425,435 for streams 426,442 inserter 426,436 reverse 426,437,440 iterator tag 95 iterator traits 85,87 for pointers 87 iter_swap() 309 K key_comp() 221,235,240 key_compare 219,234,239 key_type 218,234,239 The Annotated STL Sources 索引(index) L less 421 less_equal 421 lexicographical_compare() 310 linear complexity 286,287 list 131 see container back() 131 begin() 131 clear() 137 constructor 134 empty() 131 end() 131 erase() 136 example 133 front() 131 insert() 135 iterators 129, 130 merge() 141 pop_back() 137 pop_front() 137 push_back() 135, 136 push_front() 136, remove() 137 reverse() 142 size() 131 sort() 142 splice() 140, 141 splice functions 141 unique() 137 logarithmic complexity 287 logical_and 422 logical_not 422 logical_or 422 lower_bound() 375 for maps 241 for sets 235 M make_heap() 180 malloc_alloc for allocators 54 __malloc_alloc_template 56, 57 map 237, 239 see container < 241 == 241 487 [ ] 240, 242 begin() 240 clear() 241 constructors 240 count() 241 empty() 240 end() 240 equal_range() 241 erase() 241 example 242 find() 241 insert() 240, 241 iterators 239 lower_bound() 241 max_size() 240 rbegin() 240 rend() 240 size() 240 sorting criterion 238 swap() 240 upper_bound() 241 max() 312 max_element() 352 max_size() for deques 151 for maps 240 for red black tree 221 for sets 235 member function adapter 456 mem_fun 431,433,449,456,459 mem_fun_ref 431,433,449,456,460 mem_fun 431,433,449,456,459 mem_fun1_ref_t 433,449,459 mem_fun1_t 433,449,459 mem_fun_ref 431,433,449,456,460 mem_fun_ref_t 433,449,458 mem_fun_t 433,449,458 memmove() 75 50,70 memory pool 54,60,66,69 merge() 352 for lists 141 merge sort 412 min() 312 min_element() 354 minus 418 mismatch() 313 modulus 418 multimap 246 The Annotated STL Sources 488 constructors 246 insert() 246 multiplies 418 multiset 245 constructor 245 insert() 245 mutating algorithms 291 N negate 418 new 44,45,48,49,53,58 next_permutation() 380 n-log-n complexity 392,396,412 not1 433, 449, 451 not2 433, 449, 451 not_equal_to 420 nth_element() 409 numeric algorithms 298 298 O O(n) 286 Open Closed Principle xvii Open Source 8 ostream iterator 426 output iterator 92,95 output_iterator 95 P pair 237 constructor 237 first 237 first_type 237 second 237 second_type 237 partial_sort() 386 partial_sort_copy() 386 partial_sum() 303 partition() 354 past-the-end iterator 39 plus 418 POD 73 pop() for priority queues 184 for queues 170 for stacks 168 pop_back() 索引(index) for deques 163 for lists 137 for vectors 116,123 pop_front() for deques 157,163 for lists 137 pop_heap() 176 predicate 450 prev_permutation() 382 priority queue 183,184 constructor 184 empty() 184 example 185 pop() 184 push() 184 size() 184 size_type 184 top() 184 value_type 184 priority_queue 184 ptrdiff_t type 90 ptr_fun 431,433,449,454,455 push() for priority queues 184 for queues 170 for stacks 168 push_back() called by inserters 435, for deques 156 for lists 135,136 for vectors 116 push_front() called by inserters 435 for lists 136 push_heap() 174 Q quadratic complexity 286 queue 169, 170 < 170, 171 == 170 back() 170 empty() 170 example 171 front() 170 pop() 170 push() 170 size() 170 size_type 170 The Annotated STL Sources 索引(index) value_type 170 quicksort 392 R rand() 384 random access iterator 92,95 random_access_iterator 95 random_shuffle() 383 rbegin() for maps 240 for sets 235 read() for input streams 443 reallocation for vectors 115,122,123 rebind for allocators 44,46 red black tree 208, 218 for auto_ptrs 81 resize() for vectors 117 result_type 416,417 reverse() 360 for lists 142 reverse_copy() 361 reverse iterator 425,426,437 base() 440 reverse_iterator 440 Richard Stallman 7 rotate() 361 rotate_copy() 365 S search() 365 search_n() 366 second 489 begin() 221 for pairs 237 constructor 220,222 destructor 220 second_argument_type 417 second_type empty() 221 for pairs 237 end() 221 sequence container 113 example 227 set 233 find() 229 see container insert_equal() insert_unique() 221 221 < 236 == 236 iterators 214 begin() 235 max_size() 221 clear() 235 member access 223 rebalance 225 rotate left 226 rotate right 227 constructors 234 count() 235 empty() 235 end() 235 size() 221 equal_range() 236 release() for auto_ptrs 81 remove() 357 for lists 137 remove_copy() 357 remove_copy_if() 358 remove_if() 357 rend() for maps 240 for sets 235 replace_copy() 359 replace_copy_if() 360 replace_if() 359 reserve() 360 reset() erase() 235 example 236 find() 235 insert() 235 iterators 234 lower_bound() 235 max_size() 235 rbegin() 235 rend() 235 size() 235 sorting criterion 233 swap() 235 upper_bound() 236 set_difference() 334 set_intersection() 333 The Annotated STL Sources 490 set_symmetric_difference() 336 set_union() 331 simple_alloc for allocators 54 索引(index) top() 168 value_type 168 standard template library 73 see STL size() 20 for deques 151 for lists 131 for maps 240 for priority queues 184 for queues 170 for red black tree 221 for sets 235 for stacks 168 for vectors 116 size_type for priority queues 184 for queues 170 for stacks 168 slist 186, 190 see container begin() 191 constructor 190 destructor 190 difference with list 186 empty() 191 end() 191, 194 example 191 front() 191 iterators 188 pop_front() 191 push_front() 191 size() 191 swap() 191 smart pointer auto_ptr 81 sort() 389 for lists 142 sort_heap() 178 splice() for lists 140,141 stack 167 < 167, 168 == 167, 168 empty() 168 example 168 pop() 168 push() 168 size() 168 size_type 168 STL implementation HP implementation 9 PJ implementation 10 RW implementation 11 SGI implementation 13 STLport implementation 12 stream iterator 426,442 end-of-stream 428,443 subscript operator for deques 151 for maps 240,242 for vectors 116 suffix _copy 293 _if 293 swap() 67 for maps 240 for sets 235 T tags for iterators 95 top() for priority queues 184 for stacks 168 traits for iterators 87 for types 103 transform() 369 tree AVL trees 203 balanced binary (search) trees 203 binary (search) trees 200 Red Black trees 208 U unary_function 416 unary_negate 451 unary predicate 450 uninitialized_copy() 70, 73 uninitialized_fill() 71, 75 uninitialized_fill_n() 71, 72 unique() 370 The Annotated STL Sources 索引(index) for lists 137 unique_copy() 371 upper_bound() 377 for maps 241 for sets 236 V W X Y 491 value_comp() 235,240 value_compare 234,239 value_type for allocators 45 for priority queues 184 for queues 170 for stacks 168 for vectors 115 vector 115 see container [ ] 116 allocate_and_fill() 117 as dynamic array 115 back() 116 begin() 116 capacity 118 capacity() 116 clear() 117,124 constructor 116 destructor 116 difference_type 115 empty() 116 end() 116 erase() 117, 123 example 119 front() 116 header file 115 insert() 124 iterator operator ++ 117 iterator operator -- 117 iterator 115 iterators 117 pointer 115 pop_back() 116, 123 push_back() 116 reallocation 115,123 reference 115 resize() 117 size() 116 size_type 115 value_type 115 115 The Annotated STL Sources 492 索引(index) The Annotated STL Sources
还剩258页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 10 金币 [ 分享pdf获得金币 ] 2 人已下载

下载pdf

pdf贡献者

studhappy

贡献于2013-03-07

下载需要 10 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!