《EffcativeSTL》

来源:互联网 发布:手机淘宝千牛怎么装修 编辑:程序博客网 时间:2024/06/05 16:39
定义、使用和扩展STL
没有“STL”的官方定义,在本书中,“STL”的意思是与迭代器合作的C++标准库的一部
分。那包括标准容器(包括string),iostream库的一部分,函数对象和算法。它不包括
标准容器适配器(stack,queue和priority_queue)以及bitset和valarray容器,因为它们
缺乏迭代器支持。它也不包括数据。真的,数组以指针的形式支持迭代器,但数组是C++
语言的一部分,并非库。


技术上,我的STL定义排除了标准C++库的扩展,特别是散列容器,单链表,rope和多种非
标准函数对象。虽然如此,一个有效的STL程序员需要知道这样的扩展。存在STL扩展的
原因之一是STL被设计为可扩展的库。


在本书中,我没有说多少关于写你自己的算法的东西,而且我根本没有在写新容器和迭代
器上提供指导。我相信在你着手增加它的能力之前,掌握STL已经提供的东西很重要。


引用计数
讨论STL而没有提及引用计数是几乎不可能的。基于指针的容器的设计几乎总要导致引用
计数。另外,很多string实现内部是引用计数的。


任何时候我提及string和char或char*之间的关系,对于wstring和wchar_t或wchar_t*之
间的关序也是正确的。




术语,术语
下面的术语十分重要:
1、vector,string,deque和list被称为标准序列容器。标准关联容器是set,multiset,
map和multimap。
2、迭代器被分成五个种类,基于它们支持的操作。简要地说,输入迭代器是每个迭代
位置只能被读一次的只读迭代器。输出迭代器是每个迭代器位置只能被写一次的只写迭代
器。输入和输出迭代器被塑造为读和写输入和输入流(例如,文件)。因此并不奇怪输
入和输出迭代器最通常的表现分别是istream_iterator和ostream_iterator。


前向迭代器有输入和输出迭代器的能力,但是它们可以反复读或写一个位置。它们不支持
operator--,所以它们可以高效地向前移动任意次数。所有的标准STL容器都支持比前向
迭代器更强大的迭代器。但是,散列容器的一种设计可以产生前向迭代器。单链表容器
也提供前向迭代器。


双向迭代器就像前向迭代器,除了它们的后退可以像前进一样容易。标准关联容器都
提供双向迭代器。list也有。


随机访问迭代器可以做双向迭代器做的一切事情,但它们也提供“迭代器算术”,即,有
一步向前或向后跳的能力。vector,string,deque都提供随机访问迭代器。


3、重载了函数调用操作符,即operator()的任何类叫做仿函数类。从这样的类建立的对
象称为函数对象或仿函数。STL中大部分可以使用函数对象的地方也都可以用真函数,所以
我经常使用术语“函数对象”来表示C++函数和真的函数对象。


4、函数bind1st和bind2st称为绑定器。




STL的一个革命性方面是它的复杂度保证。下面是复杂度术语的快速入门。每个都引用了
它作为n的函数做一件事情要多久,n是容器或区间的元素数目。


1、以常数时间执行的操作的性能不受n的改变而影响,例如,向list中插入一个元素是
一个常数时间操作。不管list有一个还是一百万个元素,插入都花费同样数量的时间。
常数时间只表明它不被n影响。


2、当n变大时,以对数时间运行的操作需要更多的时间运行,但它需要的时间以与n的对数
成正比的比率增长。例如,在一百万个项上的一次操作预计花费大约在一百个项上三倍的
时间,因为logn^3=3logn。在关联容器上的大多数搜寻操作,例如set::find是对数时间
操作。


3、以线性时间运行的操作需要的时间以与n成正比的比率增长。标准算法count以线性时间
运行,因为它必须查看给定区间中的每个元素。如果区间的大小扩大了三倍,它也必须
做三倍的工作,而且我们期望它大约花费三倍时间来完成。


通常,常数时间操作运行得比要求对数时间的快,而对数时间操作运行得比线性的快。当
n变得足够大时,它总是真的,但对于n相对小的值,有时候更差理论复杂度操作可能或
胜过更好理论复杂度的操作。


术语的最后一个要注意的东西是,记住map或multimap里的每个元素都有两个组件。我一般
叫第一个组件键,叫第二个组件值。
如map<string,double> m;
string是键,double是值




代码例子
在模板参数列表中,class和typename表示完全相同的东西,但我发现typename能更清楚
地表示我通常想要说的:T可以是任何类型;不必是一个类。
但在另一个场景里,这不再是风格问题。为了避免潜在的解析含糊,你被要求在依赖形式
类型参数的类型名字之前使用typename。这样的类型被称为依赖类型。


例如,假设你想为函数写一个模板,给定一个STL容器,返回容器中的最后一个元素是否
大于第一个元素。这是一种方法:
template <typename C>
bool lastGreaterThanFirst(const C& container)
{
if (container.empty()) return false;
typename C::const_iterator begin(container.begin());
typename C::const_iterator end(container.end());
return *--end > *begin;
}




涉及效率的条款
1、用empty来代替检查size()是否为0
2、尽量使用区间成员函数代替它们的单元素兄弟
3、使用reserve来避免不必要的重新分配
4、小心string实现的多样性
5、考虑用有序vector代替关联容器
6、当关乎效率时应该在map::operator[]和map-insert之间仔细选择
7、熟悉非标准散列容器
8、需要一个一个字符输入时考虑使用istreambuf_iterator
9、了解你的排序选择
10、尽量用成员函数代替同名的算法
11、考虑使用函数对象代替函数作算法的参数




容器
STL有迭代器、算法和函数对象,但对于大多数C++程序员,容器是最突出的。它们比数组
更强大更灵活,可以动态增长(也常是缩减),可以管理属于它们自己的内存,可以跟踪
它们拥有的对象数目,可以限制它们支持操作的算法复杂度等等。


本节让你知道在选择适当的容器时应该面对的约束;避免产生为一个容器类型写的代码
也可以用于其它容器类型的错觉;容器里对象拷贝操作的重要性;当指针或auto_ptr
存放在容器中时出现的难点;删除的输入和输出;你可以或不可以使用自定义分配器;
达到最高效率的技巧和考虑在多线程环境下容器的使用。




仔细选择你的容器
C++中你可以支配的容器:
1、标准STL序列容器:vector,string,deque和list.
2、标准STL关联容器:set,multiset,map,multimap。
3、非标准序列容器slist和rope。slist是一个单向链表,rope本质上是一个重型字符串。
4、非标准关联容器hash_set,hash_multiset,hash_map和hash_multimap。
5、vector<char>可以作为string的替代品。
6、vector作为标准关联容器的替代品,有时候vector可以在时间和空间上都表现得比标准
关联容器好。
7、几种标准非STL容器,包括数组,bitset,valarray,stack,queue和priority_queue。值
得注意的是,数组可以和STL算法配合,因为指针可以当作数组的迭代器使用。




我们应该重视选择适当容器的问题。就连标准都介入了这个行动,提供了以下的在vector
qeueu和list之间作选择的指导方案:
vector,list和deque提供给程序员不同的复杂度,因此应该这么用:vector是一种可以默认
使用的序列类型,当很频繁地对序列中部进行插入和删除时应该用list,当大部分插入和
删除发生在序列的头和尾时可以选择deque这种数据结构。




连续内存容器和基于节点的容器的区别:
连续内存容器(也叫做基于数组的容器)在一个或多个(动态分配)的内存块中保存它们
的元素。如果一个新元素被插入或者已存在元素被删除,其他的同一个内存块的元素就
必须向上或向下移动为新元素提供空间或者填充原来被删除的元素所占的空间。这种移动
影响了效率和异常安全。标准的连续内存容器是vector,string和deque。非标准的rope
也是连续内存容器。


基于节点的容器在每个内存块(动态分配)中只保存一个元素。容器元素的插入或删除
只影响指向节点的指针,而不是节点自己的内容。所以当有东西插入或删除时,元素值
不需要移动。表现在链表的容器---比如list和slist是基于节点的,所有的标准关联
容器也是(它们的典型实现是平衡树)。非标准的散列容器使用不同的基于节点的实现。




在容器中选择的原则:
1、你需要“可以在容器的任意位置插入一个新元素”的能力吗?如果是,你需要序列容器
关联容器做不到。
2、你关心元素在容器中的顺序吗?如果不,散列容器就是可行的选择,否则,你要避免
使用散列容器。
3、必须使用标准C++中的容器吗?如果是,就可以除去散列容器、slist和rope。
4、你需要哪一类迭代器?如果必须是随机访问迭代器,在技术上你只能限于vector,deque
和string,但你也可能会考虑rope。如果需要双向迭代器,你就用不了slist和散列容器
的一般实现。
5、当插入或者删除数据时,是否非常在意容器内现有元素的移动?如果是,你就必须放弃
连续内存容器。
6、容器中的数据的内在布局需要兼容C吗?如果是,你就只能用vector。
7、查找速度很重要吗?如果是,你就应该看看散列容器,排序的vector和标准的关联容器
这样一顺序。
8、你介意如果容器的底层使用了引用计数吗?如果是,你就得避开string,因为很多string
的实现是用引用计数。你也不能用rope,因为权威的rope实现是基于引用计数的,于是你
得重新审核你的string,你可以考虑使用vector<char>。
9、你需要插入和删除的事务性语义吗?也就是说,你需要有可靠的回退插入和删除的能力
吗?如果是,你就需要使用基于节点的容器。如果你需要多元素插入(比如,以范围的方式)
的事务性语义,你就应该选择list,因为list是唯一提供多元素插入事务性语义的标准容器
事务性语义对于有兴趣写异常安全代码的程序员来说非常重要。(事务性语义也可以在连续
内存容器上实现,但会有一个性能开销,而且代码不那么直观)
10、你要把迭代器、指针和引用的失效次数减少到最少吗?如果是,你就应该使用基于节
点的容器,因为在这些容器上进行插入和删除不会使迭代器、指针和引用失效(除非它们
指向你删除的元素)。一般来说,在连续内存容器上插入和删除会使所有指向容器的迭代
器、指针和引用失效。
11、你需要具有以下特性的序列容器吗:1)可以使用随机访问迭代器;2)只要没有删除
而且插入只发生在容器结尾,指针和引用的数据不会失效,如果你遇到这种情况,deque
就是你梦想的容器。(有趣的是,当插入只在容器结尾时,deque的迭代器也可能会失
效,deque是唯一一个“在迭代器失效时不会使它的指针和引用失效”的标准STL容器。


元素顺序,标准的一致性,迭代器能力,内存布局和C的兼容性,查找速度,因为引用计数
造成的行为不规则,事务性语义的轻松实现和迭代器失效的条件。




小心对“容器无关代码”的幻想
STL是建立在泛化之上的。数组泛化为容器,参数化了所包含的对象的类型。函数泛化为
算法,参数化了所用的迭代器的类型。指针泛化为迭代器,参数化了所指向的对象的类型。


独立的容器类型泛化为序列或关联容器,而且类似的容器拥有类似的功能。标准的内存相
邻容器都提供随机访问迭代器,标准的基于节点的容器都提供双向迭代器。序列容器支持
push_front或push_back,但关联容器不支持。关联容器提供对数时间复杂度的lower_bound,
upper_bound和equal_range成员函数,但序列容器却没有。


在不同的种类中,甚至连一些如insert和erase这样简单的操作在名称和语义上也是天差
地别的。举个例子,当你把一个对象插入一个序列容器中,它保留在你放置的位置。但如
果你把一对象插入到一个关联容器中,容器会按照排列顺序把这个对象移到它应该在的位置
另外,在一个序列容器上用一个迭代器作为参数调用erase,会返回一个新迭代器,但在
关联容器上什么都不会返回。




假设,你希望写一段可以用在所有常用的序列容器上---vector,deque和list的代码。很显然
你必须使用它们能力的交集来编写,这意味着你不能使用reserve或capacity,因为deque
和list不支持它们。由于list的存在意味着你得放弃operator[],而且你必须受限于双向
迭代器的性能。这意味着你不能使用需要随机迭代器的算法,包括sort,stable_sort,
partial_sort和nth_element。


另一方面,你渴望支持vector的规则,不使用push_front和pop_front,而且用vector和
deque都会使splice和成员函数方式sort。在上面约束的联合下,后者意味着你不能在你
的“泛化的序列容器”上调用任何一种sort。




这里的罪魁祸首是不同序列容器所对应的不同的迭代器、指针和引用的失效规则。要写能正
确地和vector,deque和list配合的代码,你必须假设任何使那些容器的迭代器、指针和引用
失效的操作符真的在你用的容器起了作用。因此,你必须假设每次调用insert都使所有
东西失效了,因为deque::insert会使所有迭代器失效,而且因为缺少capacity,
vector::insert也必须假设使所有指针和引用失效。deque是唯一一个在迭代器失效的情
况下指针和引用仍然有效的东西。


你不能把容器里的数据传递给C风格的界面,因为只有vector支持这么做。你不能用bool
作为保存的对象来实例化你的容器。你不能期望享受到list的常数时间复杂度的插入和
删除,因为vector和deque的插入和删除操作是线性时间复杂度的。




list和deque不支持reserve,capacity。
list不支持operator[]。
vector不支持push_front和pop_front。
随机访问迭代器的算法不能用在list容器上。
vector和deque调用insert和erase会有线性时间复杂度而且会使所有迭代器、指针和引用
失效;而且不能兼容C风格的界面,不能存储bool。


如果你放弃了序列容器,把代码改为只能和不同的关联容器配合,这情况并没有什么改善
要同时兼容set和map几乎是不可能的,因为set保存单个对象,而map保存对象对。甚到要
同时兼容set和multiset(或map和multimap)也是很难的。set/map的insert成员函数只
返回一个值,和他们的multi兄弟的返回类型不同,而且你必须避免对一个保存在容器中
的值的拷贝份数作出任何假设。


你现在知道当你改变容器类型的时候,不光要修正编译器诊断出来的问题,而且要检查
所有使用容器的代码,根据新容器的性能特征和迭代器,指针和引用的失效规则来看看
哪些需要修改。如果你从vector切换到其他东西,你也需要确认你不再依靠vector的C
兼容的内存布局;如果你是切换到一个vector,你需要保证你不用它来保存bool。




既然有了要一次次改变容器类型的必然性,你可以用这个常用的方法让改变得以简化:
使用封装,封装,再封装。其中一种最简单的方法是通过自由地对容器和迭代器
类型使用typedef。因此,不要这么写:
class Widget{...};
vector<Widget> vww;
Widget bestWidget;
...//给bestWidget一个值
vector<Widget>::iterator i = //寻找和bestWidget相等的Widget
find(vw.begin(), vw.end(), bestWidget);


要这么写:
class Widget{...};
typedef vector<Widget> WidgetContainer;
typedef WidgetContainer::iterator WCIterator;
WidgetContainer cw;
Widget bestWidget;
...
WCIterator i = find(cw.begin(), cw.end(), bestWidget);


这使改变容器类型变得容易得多,如果问题的改变是简单的加上用户的allocator时特别
方便。(一个不影响对迭代器/指针/引用的失效规则的改变)。
这样也能简化书写。


当你使用STL一段时间后,你会认识到typedef是你的好朋友。


typedef只是其他类型的同义字,所以它提供的封装是纯的词法。不像#define是在预编译
阶段替换的。如果你不想暴露出用户对你所决定使用的容器的类型,你可以使用class。


要限制如果用一个容器类型替换了另一个容器可能需要修改的代码,就需要在类中隐藏
那个容器,而且要通过类的接口限制容器特殊信息可见性的数量。比如,如果你需要建立
一个客户列表,请不要直接用list。取而代之的是,建立一个CustomerList类,把list
隐藏在它的private区域:
class CustomerList{
private:
typedef list<Customer> CustomerContainer;
typedef CustomerContainer::iterator CCIterator;
CustomerContainer customers;


public: //通过这个接口
...//限制list特殊信息的可见性
};


nth_element需要随机访问迭代器。


如果你做好了对CustomerList地实现细节做好封装的话,那对CustomerList的客户的影响
将会很小。你写不出容器无关性代码,但他们可能可以。




条款3:使容器里对象的拷贝操作轻量而正确
容器容纳了对象,但不是你给它们的那个对象。此外,当你从容器中获取一个对象,你所
得到的对象不是容器里的那个对象。取而代之的是,当你向容器中添加一个对象(比如
通过insert或push_back等),进入容器的是你指定的对象的拷贝。拷进去,拷出来。这
就是STL的方式。


如果你从vector,string或duque中插入或删除了什么,现有的容器元素会移动(拷贝)。


如果你使用了任何排序算法:next_permutation或者previous_permutation; remove,
unique或它们的同类;rotate或reverse等,对象会移动(拷贝)。是的,拷贝对象是
STL的方式。


一个对象通过使用它的拷贝成员函数来拷贝,特别是它的拷贝构造函数和它的拷贝赋值
操作符。对于用户自定义类,比如Widget,这些函数传统上是这么声明的:
class Widget{
public:
...
Widget(const Widget&); //拷贝构造函数
Widget &operator(const Widget&); //拷贝赋值操作符
...
};
如果你自己没有声明这些函数,你的编译器会始终会为你声明它们。


如果你用一个拷贝过程很昂贵对象填充一个容器,那么一个简单的操作--把对象放进
容器也会是一个性能瓶颈。


由于继承的存在,拷贝会导致分割。


分割问题暗示了把一个派生类对象插入基类对象的容器几乎总是错的。如果你希望结果
对象表现在派生类对象,比如,调用派生类的虚函数等,总是错的。


一个使拷贝更高效、正确而且对分割问题免疫的简单的方式是建立指针的容器而不是对象
的容器。也就是说,不是建立一个Widget的容器,建立一个Widget*的容器。不幸的是,指针
的容器有它们自己STL相关的头疼问题。如果你想避免这些头疼并且仍要避开效率、正确
性和分割问题,你可能会发现智能指针的容器是一个吸引人的选择。


使用STL来代替数组,你可以使用一个可以在需要的时候增长的vector:
vector<Widget> vw; //建立一个0个Widget对象的vector
//需要的时候可以扩展


我们也可以建立一个可以足够包含maxNumWidgets个Widget的空vector,但没有构造Widget:
vector<Widget> vw;
vw.reserve(maxNumWidgets); 
和数组对比,STL容器更文明。




条款4:使用empty来代替检查size()是否为0:
理由很简单:对于所有的标准容器,empty是一个常数时间的操作,但对于一些list实现
size花费线性时间。


list<int> list1;
list<int> list2;
...
list1.splice( //把list2中
list1.end(), list2,
find(list2.begin(), list2.end(), 5), //从第一次出现5到
find(list2.rbegin(), list2.rend(), 10).base() //的所有节点移动list1的结尾
};
除非list2在5的后面有一个10,否则这段代码无法工作。


不管你如何看待splice和size,有的东西--size或splice的区间形式--必须让步。一个或
者另一个可以是常数时间操作,但不能都是。


不管发生了什么,如果你用empty来代替检查是否size()==0,你都不会出错。所以在想知道
容器是否包含0个元素的时候都应该调用empty。




条款5:尽量使用区间成员函数代替它们的单元素兄弟
给定两个vector,v1和v2,使v1的内容和v2的后半部分一样的最简单方式是什么:
v1.assign(v2.begin()+v2.size()/2, v2.end());


无论何时你必须完全代替一个容器的内容,你就应该想到赋值。


尽量避免手写显式循环。而避免循环的一个方法是使用一个算法来代替:
v1.clear();
copy(v2.begin()+v2.size()/2, v2.end(), back_inserter(v1));
写这些仍然比写assign的调用要做更多的工作。


几乎所有目标区间是通过插入迭代器(比如,通过inserter,back_inserter或front_inserter)
指定的copy的使用都可以---应该----通过调用区间成员函数来代替。比如这里,这个copy
的调用可以用一个insert的区间版本代替:
v1.insert(v1.end(), v2.begin() + v2.size()/2, v2.end());


所有用于标准容器的迭代器都提供了前向迭代器的功能。非标准的散列容器的迭代器也是。
在数组中表现在迭代器的指针也提供了这样的功能。


事实上,唯一不提供前向迭代器能力的标准迭代器是输入和输出迭代器。


一个区间插入可以在开始插入东西前计算出需要多少新内存(假设给的是前向迭代器),
所以它不用多于一次地重新分配vector的内在内存。


我刚才进行分析是用于vector的,但同样的理由也作用于string。对于deque,理由也很
相似,但deque管理它们内存的方式和vector和string不同,所以重复内存分配的论点
不能应用。但是,关于很多次不必要的元素移动的论点通常通过对函数调用次数的观
察也应用到了。




总结:哪些成员函数支持区间元素


1、区间构造。所有标准容器都提供了这种形式的构造函数:
container::container(InputIterator begin, //区间的起点
InputIterator end); //区间的终点
如果传给这个构造函数的迭代器是istream_iterators或istreambuf_iteratos,你可能
会遇到C++的最惊异的解析,原因之一是你的编译器可能会因为把这个构造看作一个函数
声明而不是一个新容器对象的定义而中断。


2、区间插入。所有标准序列容器都提供这种形式的insert:
void container::insert(iterator position, //区间插入的位置
InputIterator begin, //插入区间的起点
InputIterator end); //插入区间的终点


关联容器使用它们的比较函数来决定元素要放在哪里,所以它们省略了position参数。
void container::insert(InputIterator begin, InputIterator end);


当寻找用区间版本代替单元素插入的方法时,不要忘记有些单元素变量用采用不同的函数
名伪装它们自己。比如,push_front和push_back都把单元素插入容器,即使它们不叫
insert。如果你看见一个循环调用push_front或push_back,或如果你看见一个算法
---比如copy---的参数是front_inserter或者back_inserter,你就发现了一个insert
的区间形式应该作为优先策略的地方。




3、区间删除。每个标准容器都提供了一个区间形式的erase,但是序列和关联容器的返回
类型不同。序列容器提供了这个:
iterator container::erase(iterator begin, iterator end);


而关联容器提供这个:
void container::erase(iterator begin, iterator end);


原因是如果erase的关联容器版本返回一个迭代器(被删除的那个元素的下一个)会招致
一个无法接受的性能下降。


关于vector和string的插入和删除的一个论点是必须做很多重复的分配。(当然对于删除,
会发生重复的回收。)那是因为用于vector和string的内存自动增长来适应于新元素,
但当元素的数目减少时它不自动收缩。
一个非常重要的区间erase的表现是erase-remove惯用法。




4、区间赋值。所有标准序列容器都提供了区间形式的assign:
void container::assign(InputIterator begin, InputIterator end);


所以现在我们明白了,尽量使用区间成员函数来代替单元素兄弟的三个可靠的论点:
1、区间成员函数更容易写
2、它们更清楚地表达你的意图
3、它们提供了更高的性能。




条款6:警惕C++最令人恼怒的解析
假设你有一个int的文件,你想要把那些int拷贝到一个list中。这看起来像是一个合理的
方式:
ifstream dataFile("ints.dat");
list<int> data(istream_iterator<int>(dataFile), //警告!这完成的并不是像你想
istream_iterator<int>()); //像的那样
这段代码可以编译,但在运行时,它什么都没做。它不会从文件中读出任何数据。它甚至
不会建立一个list。那是因为第二句并不声明list,而且它也不调用构造函数。


其实它做的是:
我们会从最基本的开始。这行声明了一个函数f带有一个double而且返回一个int:
int f(double d);


第二行作了同样的事情。名为d的参数左右的括号是多余的,被忽略:
int f(double (d)); //同上,d左右的括号被忽略


下面这行声明了同样的函数。它只是省略了参数名:
int f(double); //同上,参数名被省略




下面我们再看看三个函数声明。第一声明了一个函数g,它带有一个参数,那个参数是指向
一个没有参数、返回double的函数的指针:
int g(double (*pf)()); //g带有一个指向函数的指针作为参数


这是完成同一件事的另一种方式。唯一的不同是pf使用非指针语法来声明(一个在C和C++
中都有效的语法):
int g(double pf()); //同上;pf其实是一个指针


照常,参数名可以省略,所以这是g的第三种声明,去掉了pf这个名字:
int g(double ()); //同上,参数名省略


注意,参数名左右的括号和单独的括号之间的区别。参数名左右的括号被忽略,但单独
括号指出存在一个参数列表:它们声明了存在指向函数的指针的参数。




下面我们再回头看看前面的代码:
list<int> data(istream_iterator<int>(dataFile), istream_iterator<int>());


这声明了一个函数data,我的返回类型是list<int>。这个函数data带有两个参数:
第一个参数叫做dataFile。它的类型是istream_iterator<int>。dataFile左右的括号
是多余的而且被忽略。
第二个参数没有名字。它的类型是指向一个没有参数而且返回istream_iterator<int>
的函数的指针。




奇怪吗?但这符合C++里的一条通用规则----几乎任何东西都可能被分析成函数声明。如果
你用C++编程有一段时间了,你应该会遇到另一个这条规则的表象。有多少次你会看见这
个错误?
class Widget{...}; //假设Widget有默认构造函数


Widget w(); //...


这并没有声明一个叫做w的Widget,它声明了一个叫作w的没有参数且返回Widget的函数。
学会识别这个失言(faux pas)是成为C++程序员的一个真正的通过仪式。


所有的这些是不是很有趣?但它没有帮我们说出我们想要说的,也就是应该用一个文件
的内容来初始化一个list<int>对象。
现在我们知道了我们必须战胜的解析,那就很容易表示了。用括号包围一个实参的声明是
不合法的,但用括号包围一个函数调用的观点是合法的,所以通过增加一对括号,我们
强迫编程器以我们的方式看事情:
list<int> data((istream_iterator<int>(dataFile)), //注意在list构造函数
istream_iterator<int>()); //的第一个实参左右的新括号


在构造对象的时候,要注意实参不能是一种类型,特别是实参有一个单参的括号的情况。
如上面的第一个参数会忽略括号,这样第一个参数就是iterator_iterator<int>类型。


一个更好的解决办法是在数据声明中从时髦地使用匿名istream_iterator对象后退一
步,仅仅给那些迭代器名字。以下代码到哪里都能工作:
ifstream dataFile("ints.dat");
istream_iterator<int> dataBegin(dataFile);
istream_iterator<int> dataEnd;
list<int> data(dataBegin, dataEnd);


命名迭代器对象的使用和普通的STL编程风格相反,但是你得判断这种方法对编译器和必
须使用编译器的人都模棱两可的代码是一个值得付出的代价。




条款7:当使用new得指针的容器时,记得在销毁容器前delete那些指针。
STL中的容器非常优秀。它们提供了前向和逆向遍历的迭代器(通过begin,end,rbegin等)
它们能告诉你所容纳的对象类型(通过value_type和typedef);在插入和删除中,它们负责
任何需要的内存管理;它们报告容纳了多少对象和最多可能容纳的数量(分别通过size
和max_size);而且当然当容器自己被销毁时会自动销毁容纳的每个对象。


但当容器容纳的是指向通过new分配的对象的指针时,它们就错了。的确,当一个指针
的容器被销毁时,会销毁它(那个容器)包含的每个元素,但指针的“析构函数”是无
操作!它肯定不会调用delete。


结果,下面代码直接导致一个内存泄漏:
void doSomething()
{
vector<Widget*> vwp;
for(int i = 0; i < SOME_MAGIC_NUMBER; ++i)
{
vwp.push_back(new Widget);
...//使用vwp
//Widgets在这里泄漏
}
当vwp出了生存域后,vwp的每个元素都被销毁,但那并不改变从没有把delete作用于new得
到的对象这个事实。那样的删除是你的职责,而不是vector的。


void doSomething()
{
vector<Widget *> vwp;
...//同上
for(vector<Widget*>::iterator i = vwp.begin();
i != vwp.end(); ++i)
{
delete *i;
}
}
这可以工作,但带来的另一个问题是这段代码不是异常安全的。如果在用指针填充了vwp
和你要删除它们之间抛出了一个异常,你会再次资源泄漏。幸运的是,两个问题都可以
克服。


要把你的类似for_each的循环转化为真正使用for_each,你需要把delete转入一个函数对象
中:
template <typename T>
struct DeleteObject:
public unary_function<const T*, void>{ //这里有这个继承
void operator()(const T* ptr) const
{
delete ptr;
}
};




现在你可以这么做:
void doSomething()
{
...//同上
for_each(vwp.begin(), vwp.end(), DeleteObject<Widget>);
}
不幸的是,这让你指定了DeleteObject将会删除的对象的类型(在本例中是Widget)。那是
很讨厌的,vwp是一个vector<Widget*>,所以当然DeleteObject会删除Widget*指针,它
会导致很难跟踪到的bug。假设,比如,有的人恶意地故意从string继承:
class SpecialString: public string{...};
这是很危险的行为,因为string,就像所有的标准STL容器,缺少虚析构函数,而从没有
虚析构函数的类公有继承是一个大的C++禁忌。


void doSomething()
{
deque<SpecialString *> dssp;
...
for_each(dssp.begin(); dssp.end(); //行为未定义,通过没有虚析构函数的基类
DeleteObject<string>());//指针来删除派生对象
}


你可以通过编译器推断传给DeleteObject::operator()的指针的类型来消除这个错误。
我们需要做的所有事就是把模板化从DeleteObject移到它的operator():
struct DeleteObject{
template <typename T> //模板化加在这里
void operator()(const T*ptr) const
{
delete ptr;
}
};


编译器知道传给DeleteObject::operator()的指针类型,所以我们可以让它通过指针的
类型自动实例化一个operator()。这种类型演绎下降让我们放弃使DeleteObject可适配
能力。


使用新版本的DeleteObject,用于SpecialString的客户代码看起来像这样:


void doSomething()
{
deque<SpecialString *> dssp;
...
for_each(dssp.begin(), dssp.end(),
DeleteObject()); //啊!良好定义的行为!
}
直截了当而且类型安全,正如我们喜欢的一样。


但仍不是异常安全的。如果在SpecialString被new但在调用for_each之前抛出一个异常,
就会发生泄漏。那个问题可以以多种方式解决,但最简单的可能是智能指针的容器来
代替指针的容器,典型的是引用计数指针。


STL本身没有包含引用计数指针,而且写一个好的,总是可以正确工作的智能指针是非常
需要技巧的。


幸运的是,基本上不需要你自己写,因为一个这样的智能指针是Boost库中的share_ptr。
利用Boost的shared_ptr,本条款的原始例子可以重写为这样:
void doSomething()
{
typedef boost::shared_ptr<Widget> SPW; //SPW=“shared_ptr to Widget"
vector<SPW> vwp;
for(int i = 0; i < SOME_MAGIC_NUMBER; ++i)
{
vwp.push_back(SPW(new Widget)); //从一个Widget建立SPW,然后进行一次push_back
...//使用vwp,
//这里没有Widget泄漏,甚至在上面代码中抛出异常
}


你不能有的愚蠢思想是你可以通过建立auto_ptr的容器来形式成以自动删除的指针。那是
很可怕的想法,非常危险。


你要删除指针的容器时要避免资源泄漏,你必须用智能引用计数指针对象(比如boost库
的shared_ptr)来代替指针,或者你必须在容器销毁前手动删除容器中的每个指针。






条款8:永不建立auto_ptr的容器:
auto_ptr的容器(COAPs,container of auto_ptr)是禁止的。试图使用它们的代码都不能
编译。
我会马上解释COAPs的幽灵有多令人担心,以至于标准化委员会采取特殊措施来保证它们
不合法。
现在,我要专注于一个不需要auto_ptr甚至容器知识的缺点:COAPs不可移植。


当你拷贝一个auto_ptr时,auto_ptr所指向对象的所有权被转移到拷贝的auto_ptr,而被
拷贝的auto_ptr被设为NULL。你正确的说一遍:拷贝一个auto_ptr将改变它的值:
auto_ptr<Widget> pw1(new Widget); //pw1指向一个Widget
auto_ptr<Widget> pw2(pw1); //pw2指向pw1的Widget;
//pw1被设为NULL,(Widget的所有权从pw1转移到pw2)
pw1 = pw2;  pw1现在指向Widget,pw2被设为NULL


现在考虑下面这段看起来很正确的代码,它建立一个auto_ptr<Widget>的vector,然后使
用一个比较指向的Widget的值的函数对它进行排序。
bool widgetAPCompare(const auto_ptr<Widget>& lhs,
const auto_ptr<Widget>& rhs)
{
return *lhs < *rhs; //对于这个例子,假设Widget存在operator<
}


vector<auto_ptr<Widget> > widgets; //建立一个vector,然后用Widget的auto_ptr
填充它;记住,这将不能编译!
sort(widgets.begin(), widgets.end(), widgetAPCompare);  //排序这个vector


在排序过程中widgets中的一个或多个auto_ptr可能已经被设为NULL。


它会这样是因为实现sort的方法---一个常见的方法,正如它呈现的---是使用了快速排序
算法的某种变体,排序一个容器的基本思想是,选择容器的某个元素作为“主元”,然后
对大于和小于或等于主元的值进行递归排序。多多少少看起来如下:
template <class RandomAccessIterator, //这个sort的声明
class Compare> //直接来自于标准
void sort(RandomAccessIterator first,
RandomAccessIterator last,
Compare comp)
{
///这个typedef在下面解释
typedef typename iterator_traits<RandomAccessIterator>::value_type ElementType;
RandomAccessIterator i;
...//让i指向主元
ElementType pivotValue(*i) //把主元拷贝到一个局部临时变量中
//下面做剩下的排序工作
...
}
上面唯一难理解的是引用了iterator_traits<RandomAccessIterator>::value_type,
而只不过是传给sort的迭代器所指向的对象类型的怪异的STL方式。(当我们涉及
iterator_traits<RandomAccessIterator>::value_type时,我们必须在它前面写上
typename,因为它是一个依赖于模板参数类型的名字,在这里是RandomAccessIterator.


上面出问题的是
EmelentType pivotValue(*i);
因为它把一个元素从保存的区间拷贝到局部临时对象中。在这里这个元素是一个
auto_ptr<Widget>。所以这个拷贝操作默默把被拷贝的auto_ptr设为NULL。另外,当
pivotValue出了生存期,它会自动删除指向的Widget。这时sort调用返回了,vector的
内容已经改变了,而且至少一个Widget已经被删除了。也可能有几个vector元素已经被
设为NULL,而且几个widget已经被删除,因为快速排序是一种递归算法,递归的每一层
都会拷贝一个主元。


智能指针的容器是很好的,只不过auto_ptr不是那样的智能指针。完全不是。




条款9:在删除选项中仔细选择
假定你有一个标准STL容器c,容纳int,
Container<int> c;
而你想把c中所有值为1963的对象都去掉。令人吃惊的是,完成这项任务的方法因不同的
容器类型而不同:没有一种方法是通用的。


如果你有一个连续内存容器(vector, string,或deque),最好的方法是erase-remove惯用
法:
c.erase(remove(c.begin(), c.end(), 1963), //当c是vector,string或deque时
c.end()); //erase-remove惯用法是去除特定值的元素的最佳方法


这方法也适合于list,但是,list的成员函数remove更高效:
c.remove(1963); //当c是list时,remove成员函数是去除特定值的元素的最佳方法


当c是标准关联容器,即set,multiset,map,multimap时,使用任何叫做remove的东西是完
全错误的,这样的容器没有叫做remove的成员函数,而且使用remove算法可能覆盖容器
值,潜在地破坏容器。试图在map和multimap上使用remove肯定不能编译,而试图在set
和multiset上使用可能不能编译。


对于关联容器,解决问题的适当方法是调用erase:
c.erase(1963); //当c是标准关联容器时,erase成员函数是去除特定值的元素的最佳方法


这不仅是正确的,而且很高效,只花费对数时间。(序列容器的基于删除的技术需要线
性时间。)并且,关联容器的erase成员函数有基于等价而不是相等的优势。


下面我们来消除下面判断式,返回真的每个对象:
bool badValue(int x); //返回x是否是"bad"
对于序列容器vector,deque和string,我们要做的只是把每个remove替换为remove_if,
然后就完成了:
c.erase(remove_if(c.begin(), c.end(), badValue), //当c是vector,string,deque
c.end()); //时这是去掉badValue返回真的对象的最佳方法


c.remove_if(badValue); //当c是list时,这是去掉badValue返回真的对象的最佳方法


对于标准关联容器,它不是很直截了当。有两种方法处理该问题,一个更容易编码,另一个
更高效。“更容易但效率较低”的解决方案用remove_copy_if把我们需要的值拷贝到一个新
的容器中,然后把原容器的内容和新的交换:
AssocContainer<int> c; //c现在是一种标准关联容器
AssocContainer<int> goodValues; //用于容纳不删除的值的临时容器


remove_copy_if(c.begin(), c.end(), //从c拷贝不删除的值到goodValues
inserter(goodValues, goodValues.end()), badValue);


c.swap(goodValues); //交换c和goodValues;


对于这种方法的缺点是它拷贝了所有不删除的元素,而这样的拷贝开销可能大于我们感兴趣
支付的。


我们可以通过直接从原容器删除元素来避开那笔账单。不过,因为关联容器没有提供类
似remove_if的成员函数,所以我们必须写一个循环来迭代c中的元素,和原来一样删除元素。


这看起来很简单单,你或许会很快想到下面的代码:
AssocContainer<int> c;
...
for(AssocContainer<int>::iterator i = c.begin(); //清晰,直截了当
i!=c.end(); ++i) //而漏洞而出的用于删除c中badValue返回真的每个元素的代码
{
if (bacValue(*i))
c.erase(i); 
}
//不要这么做!


这有未定义的行为。因为当容器的一个元素被删除时,指向那个元素的所有迭代器都
失效了。当c.erase(i)返回时,i已经失效。所以for循环里的++i是错误的。


为了避免这个问题,我们必须保证在调用erase之前就得到c中下一个元素的迭代器。最容易
的方法是当我们调用时在i上使用后置递增:
AssocContainer<int> c;
...
for(AssocContainer<int>::iterator i = c.begin();  //for循环的第三部分是空的
i!=c.end(); ) //i在下面自增
{
if (badValue(*i)) c.erase(i++); 
else ++i;
}


现在让我们进一步修改问题。不仅删除badValue返回真的每个元素,而且每当一个元素被
删掉时,我们也想把一条消息写到日志文件中。


对于关联容器,这说多容易就有多容易,因为只需要对我们刚才开发的循环做一个微不足
道的修改就行了:
ofstream logFile; //要写入的日志文件
AssocContainer<int> c;
...
for(AssocContainer<int>::iterator i = c.begin(); 
i!=c.end(); )
{
if (badValue(*i) 
{
logFile<<"Erasing"<<*i<<endl; //写入日志文件
c.erase(i++); //删除元素
}
else
++i;
}


现在是vector、string和deque给我们带来麻烦。我们不能再使用erase-remove惯用法,因
为没有办法让eras!或remove写日志文件。而且,我们不能使用刚刚为关联容器开发的循环,
因为它为vector,string和deque产生未定义的行为。
要记得对于那样的容器,调用erase不仅使所有指向被删除元素的迭代器失效,也使被删除
之后的所有迭代器失效。在我们的情况里,那包括所有i之后的迭代器。我们写i++,++i或
你能想起的其他任何东西都没有用,因为没有能导致迭代器有效的。


我们必须对vector、string和deque采用不同的战略。特别是,我们必须利用erase的返回
值。那个返回值正是我们需要的:一旦删除完成,它就指向紧接在被删除元素之后的元素
的有效迭代器。换句话说,我们这么写:
for(SeqContainer<int>::iterator i = c.begin(); 
i!=c.end();)
{
if (badValue(*i))
{
logFile<<"Erasing"<<*i<<endl;
i = c.erase(i); //通过把erase的返回值赋给i来保持i有效
}
else
++i;
}
这可以很好地工作,但只用于标准序列容器。
标准关联容器的erase的返回类型是void。


对于那些容器,你必须使用“后置递增你要传给erase的迭代器”技术。这也是为序列容器
编码和为关联容器编码之间的这种差别是为什么写容器无关代码一般缺乏考虑的一个例子。


事实表明,对于迭代和删除,你可以像vector/string/deque一样或像关联容器一样对待
list,两种方法都可以为list工作。




结论:
1、去除一个容器中有特定值的所有对象:
如果容器是vectir,string,deque,使用erase-remove惯用法。
如果容器是list,使用list::remove.
如果容器是标准关联容器,使用它的erase成员函数。


2、去除一个容器中满足一个特定判定式的所有对象:
如果容器是vector,string,deque,使用erase-remove_if惯用法。
如果容器是list,使用list::remove_if.
如果容器是标准关联容器,使用remove_copy_if和swap,或写一个循环来遍历容器元素,
当你把迭代器传给erase时记得后置递增它。


3、在循环内做某些事情(除了删除对象之外):
如果容器是标准序列容器,写一个循环来遍历容器元素,每当调用erase时记得都用它的
返回更新你的迭代器。
如果容器是标准关联容器,写一个循环来遍历容器元素,当你把迭代器传给erase时记得
后置递增它。




由此你知道,有效地删除容器元素有更多的东西需要你注意。解决问题的最后方法取决于
你是怎样鉴别出哪个对象是要被去掉的,储存它们的容器的类型,和当你删除它们的时
候你还想要做什么(如果有的话)2。这样你就可以避免产生低效和未定义行为的危险。


这仅对带有迭代器实参的erase形式是正确的。关联容器也提供一个带有一个值的实参的
erase形式,而那种形式返回被删掉的元素个数。




条款10:注意分配器的协定和约束:
分配器是怪异的。它们最初是为抽象内存模型而开发的,允许库开发者忽略在某些16位操作
系统中near和far指针的区别(即,DOS和它的有害产物),但努力失败了。分配器也被设计
成促进全功能内存管理器的发展,但事实表明那种方法在STL的一些部分会导致效率损失。
为了避免效率冲击,C++标准委员会把分配器弱化为对象。


还有更多。正如operator new和operator new[],STL分配器负责分配和回收原始内存,但
分配器的客户接口与operator new,operator new[]甚至malloc几乎没有相似之处。最后,
大多数标准容器从未向它们相关的分配器索要内存。从没有,嗯,分配器是怪异的。


当然,那不是它们的错,而且无论如何,这不意味着它们没用。


分配器的约束的列表从用于指针和引用的残留typedef开始。正如我提到的,分配器最初
被设想为抽象内存模型,在那种情况下,分配器在它们定义的内存模型中提供指针和引用
的typedef才有意义。在C++标准里,类型T的对象的默认分配器(巧妙地称为allocator<T>)
提供typedef allocator<T>::pointer和allocator<T>::reference,而且也希望用户定
义的分配器也提供这些typedef。


C++老手立即发现这有问题,因为在C++里没有办法捏造引用。这样做要求有能力重载
operator.,而那是不允许的。另外,建立行为像引用的对象是使用代理对象的例子,而
代理对象会导致很多问题。


就STL里的分配器而言,没有任何代理对象的技术缺点会导致指针和引用typedef失效,
实际上标准明确地允许库实现每个分配器的pointer typedef是T*的同义词,每个分配
器的reference typedef与T&相同。对,库实现可以忽视typedef并直接使用原始指针和
引用!所以即使你可以设法写出成功地提供新指针和引用类型的分配器的方法,也好不
到哪里去,因为你使用的STL实现将自由地忽视你的typedef。


另外,分配器是对象,那表明它们可能有成员功能,内嵌的类型和typedef(例如pointer
和reference)等等,但标准允许STL实现认为所有相同类型的分配器对象都是等价的而且
比较起来总是相等。考虑下面代码:
template <typename T> //一个用户定义的分配器
class SpecialAllocator{...} //模板
typedef SpecialAllocator<Widget> SAW; 
list<Widget, SAW> L1;
list<Widget, SAW> L2;
...
L1.splice(L1.begin(), L2); //把L2的节点移到L1前端
记住当list元素从一个list被接合到另一个时,没有拷贝什么,只是调整了一些指针。


在上面的例子里,接合前在L2里的节点接合后出现在L1中。


当L1被销毁时,当然,它必须销毁它的所有节点,以及回收它们的内存,而因为它现在
包含最初是L2一部分的节点,L1的分配器必须回收最初由L2的分配器分配的节点。现在
清楚为什么标准允许STL实现认为相同类型的分配器等价。所以由一个分配器对象分配的
内存可以安全地被另一个分配器对象回收。如果没有这样的认为,接合操作将更难实现。


那意味着可移植的分配器对象---在不同的STL实现下都功能正确的分配器---不能有状态。
让我们明确这一点:它意味着可移植的分配器不能有任何非静态数据成员,至少没有会
影响它们行为的。一个都没有。那表示,例如,你不能有从一个堆分配的SpecialAllocator
<int>和从另一个堆分配的另一个SpecialAllocator<int>,这样的分配器不等价。


我早先提及了分配器在分配原始内存方面类似operator new,但它们的接口不同。如果你
看看operator new和allocator<T>::allocate最普通形式的声明,就会很清楚:
void operator new(size_t bytes);
pointer allocator<T>::allocate(size_type numObjects);
//记住事实上pointer总是T*的typedef


两者都带有一个指定要分配多少内存的参数,但对于operator new,这个参数指定的是字
节数,而对于allocator<T>::allocate,它指定的是内存里要能容纳多少个T对象。


大多数标准容器从未调用它们例示的分配器。


这对list和所有标准关联容器set,multiset,map和multimap都是真的。那是因为这些是基
于节点的容器,即,这些容器所基于的数据结构是每当值被储存就动态分配一个新节点。
对于list,节点是列表节点,对于标准关联容器,节点通常是树节点,因为标准关联容器
通常用平衡二叉搜索树实现。


想想list的可能实现。list本身由节点组成,每个节点容纳一个T对象和list中后一个和
前一个节点的指针:
template <typename T, 
typename Allocator = allocator<T> > //模板的模板参数
class list
{
private:
Allocator alloc; //用于T类型对象的分配器
struct ListNode //链表里的节点
{
T data;
ListNode *prev;
ListNode *next;
};
};


如果你想要写自定义分配器,下面是一个总结:
1、把你的分配器做成一个模板,带有模板参数T,代表你要分配内存的对象类型。
2、提供pointer和reference的typedef,但是总是让pointer是T*,reference是T&.
3、决不要给你的分配器每对象状态。通常,分配器不能有非静态的数据成员。
4、记得应该传给分配器的allocate成员函数需要分配的对象个数而不是字节数。也应该
记得这些函数返回T*指针,通过pointer typedef,即使还没有T对象被构造。
5、一定要指针标准容器依赖的内嵌rebind模板。


写你自己的分配器时你必须做的大部分事情是重现大量样板代码,然后修补一些成员函数,
特别是allocate和deallocate。




条款11:理解自定义分配器的正确用法:
例如,假定你有仿效malloc和free的特别程序,用于管理共享内存的堆:
void *mallocShared(size_t bytesNeeded);
void freeShared(void *ptr);


并且你希望参把STL容器的内容放在共享内存中。没问题:
template <typename T>
class ShareMemoryANocator
{
public:
...
pointer allocate(size_type numObjects, const void* localityHint = 0)
{
return static_cast<pointer>(mallocShared(numObjects* sizeof(T)));
}


void deallocate(pointer ptrToMemory, size_type numObjects)
{
freeShared(ptrToMemory);
}
...
};


你可以像这样使用SharedMemoryAllocator:
//方便的typedef
typedef vector<double, SharedMemoryAllocator<double> >
SharedDoubleVec;


SharedDoubleVec v; 




条款12:对STL容器线程安全性的期待现实一些:
标准C++的世界是相当保守和陈旧的。在这个纯洁的世界,所有可执行文件都是静态
链接的。不存在内存映射文件和共享内存。没有窗口系统,没有网络,没有数据库,没有
其他进程。你对STL的线程安全的有第一个想法应该是它将因实现而不同。


在STL容器里对多线程支持的黄金规则已经由SGI定义,大体上说,你能从实现里确定的
最多是下列内容:
1、多个读取者是安全的。多线程可能同时读取一个容器的内容,这将正确地执行。当
然,在读取时不能有任何写入者操作这个容器。


2、对不同容器的多个写入者是安全的。多线程可以同时写不同的容器。


写多线程的代码很难,很多程序员希望STL实现是完全线程安全的。如果那样,程序员
可以不再需要自己做并行控制。但这也非常难实现。一个库可能试图以下列方式实现这
样完全线程安全的容器:
1、在每次调用容器的成员函数期间都要锁定该容器。
2、在每个容器返回的迭代器(例如通过调用begin()或end())的生存期之内都要锁定
该容器。
3、在每个在容器上调用的算法执行期间锁定该容器。


现在考虑下列代码。它搜寻一个vector<int>中第一次出现5这个值的地方,而且,如果它
找到了,就把这个值改为0:
vector<int> v;
vector<nt>::iterator first5(find(v.begin(), v.end(), 5)); //行1
if (find5 != v.end()){ //行2
*first5 = 0; //行3
}


要让上面的代码成为线程安全的,v必须从行1到行3保持锁定,很难想像STL实现怎么能自
动推断出这个。记住同步原语(信号类,互斥量等)通常开销很大。


所以你必须手工对付这些情况中的同步控制。
在这个例子里,你可以像这样做:
vector<int> v;
...
getMutexFor(v);
vector<int>::iterator first5(find(v.begin(), v.end(), 5));
if (first5!=v.end())
{
*first5 = 0;
}
releaseMutexFor(v); //现在是线程安全的了


一个更面向对象的解决方案是创建一个Lock类,在它的构造函数里获得互斥量并在它的析
构函数里释放它,这样使getMutexFor和releaseMutexFor的调用不匹配的机会减到最小。
这样的一个类(其实是一个类模板)基本是这样的:
template<typename Container> //获取和释放容器的互斥量
class Lock 
{
public:
Lock(const Containers container)
:c(container)
{
getMutexFor(c); //在构造函数获取互斥量
}


~Lock()
{
releaseMutexFor(c); //在析构函数里释放它
}


private:
const Container& c;
};


使用一个类(像Lock)来管理资源的生存期(例如互斥量)的办法通常称为资源获得即初
始化。一个工业强度的版本需要很多改进,但是那样的扩充与STL无关,下面我们把它
用在上述代码中:
vector<int> v;
...
{
Lock<vector<int> > lock(v); //获取互斥量
vector<int>::iterator first5(find(v.begin(), v.end(), 5));
if (first5 != v.end())
{
*first5 = 0;
}
}
//关闭块,自动释放互斥量,并且这种基于Lock的方法在有异常的情况下也是没有什么问题的。
因为C++保证如果抛出了异常,局部对象就会被销毁。所以即使当我们正在使用Lock对象时
有异常抛出,Lock也将释放它的互斥量。如果我们依赖手工调用getMutexFor和releaseMutex
For,那么在调用getMutexFor之后,releaseMutexFor之前如果有异常抛出,我们将不会释放
互斥量。


异常和资源管理是重要的。


当涉及到线程安全和STL容器时,你以可确定库实现允许在一个容器上的多读取者和不同
容器上的多写入者。你不能希望库消除对手工并行控制的需要。




vector和string
本章我们看看改进vector和string性能的方法,确定string的实现中重要的变种,检验怎
么传递vector和string数据到只知道C的API,学习怎么除去过剩的内存分配。还有一个
不能使用的vector<bool>。




条款13: 尽量使用vector和string来代替动态分配的数组:
如果你使用new来进行动态分配,你需要完成以下几点:
1、你必须确保以后会delete这个分配,否则你的new就会产生一个资源泄漏。
2、你必须确保你使用了delete的正确形式,对于分配一个单独的对象,使用delete,对于
分配一个数组,使用delete[]。如果对动态分配的数组使用了delete,结果会未定义。
3、你必须确保只delete一次。如果一个分配被删除了不止一次,结果也会未定义。故
我们通常会在delete指针之后将指针置空,来避免对同一块内存释放两次。


一般来说,当T是一个字符类型的时候使用string,否则使用vector。


条款14:使用reserve来避免不必要的重新分配:
关于STL容器,只要不超过它们的最大大小,它们就可以自动增长到足以容纳你放进去的
数据。(要知道这个最大值,只要调用名叫max_size()的成员函数)。


reserve成员函数允许你最小化必须进行的重新分配的次数,因而可以避免真分配的开销和
迭代器/指针/引用失效。但在这之前,我们先看看四个有点奇怪的相关成员函数。在标准
容器中,只有vector和string提供了所有这些函数。


1、size()告诉你容器中有多少元素。而不是容器占有的总的大小。
2、capacity()告诉你容器总的可以容纳元素的大小。如果你想知道一个vector或string中
有多少没有被占用的内存,你必须从capacity()中减去size()。如果size()和capacity()返回
同样的值,容器中就没有剩余空间了,再下次的插入(insert或push_back)会引发内存的
重新分配。
3、resize(Container::size_type n)强制把容器改为容纳n个元素。调用resize之后,size
将返回n。如果n小于当前大小,容器尾部的元素会被销毁。如果n大于当前大小,新默认
构造的元素会添加到容器尾部。如果n大于当前容量,在元素加入之前会发生重新分配。
4、reserve(Container::size_type n)强制容器把它的容量改为至小n,提供的n不小于
当前大小。这一般强迫进行一次重新分配,因为容量需要增加。(如果n小于当前容量,
vector忽略它,这个调用什么都不做,string可能把它的容量减少为size()和n中的大的数,
但string的大小没有改变。一般使用reserve来从一个string中修整多余容量一般不如使用
“交换技巧”。


这个简介明确表示了只要有元素需要插入而且容器的容量不足时就会发生重新分配(包括它
们维护的原始内存分配和回收,对象的拷贝和析构和迭代器、指针和引用的失效)。所以
避免重新分配的关键是使用reserve尽快把容器的容量设置为足够大,最好在容器被构造
之后立刻进行。


例如,假定你想建立一个容纳1-1000值的vector<int>。如果没有使用reserve,在大多数
STL实现中,循环添加过程将会导致2到10次重新分配。因为vector在重新分配发生时一般
把容量翻倍,而1000约等于2^10。



vector<int> v;
v.reserve(1000);
for(int i = ; i <= 1000; ++i) v.push_back(i);
这在循环中不会发生重新分配。


在大小和容量之间的关系让我们可以预言什么时候插入将引起vector或string执行重新分配,
而且,可以预言什么时候插入会使指向容器的迭代器、指针和引用失效。


例如:
string s;
...
if (s.size() < s.capacity())
{
s.push_back('x');
}
push_back的调用不会使用指向这个string中的迭代器、指针或引用失效,因为string的
容量保证大于它的大小。
如果不是执行push_back,代码在string的任意位置进行一个insert,我们仍然可以保
证插入期间没有发生重新分配,但是,与伴随string插入时迭代器失效的一般规则一
致,所有从插入位置到string结尾的迭代器/指针/引用将失效。


记住:调用reserve不改变容器中对象的个数。




条款15:小心string实现的多样性




条款16:如何将vector和string的数据传给遗留的API
如果你有一个vector对象v,而你需要得到一个指向v中数据的指针,以使得它可以被当
作一个数组,只要使用&v[0]就可以了。对于string对象s,相应的是简单的s.c_str(),


如下:
vector<int> v;
表达多v[0]生产一个指向vector中第一个元素的引用,所以,&v[0]是指向那个首元素的
指针。vector中的元素被C++标准限定为存储在连续内存中,就像是一个数组,所以,
如果我们想要传递v给这样的c风格的API:
void doSomething(const int* pInts, size_t numInts);
我们可以这样做:
void doSomething(&v[0], v.size());
唯一的问题是,如果v是空的,v.size()是0,而&v[0]试图产生一个指向根本就不存在的
东西的指针,其结果是未定义的。一个更好的方法是:
if(!v.empty())
{
doSomething(&v[0], v.size());
}


类似从vector上获取指向内部数据的指针的方法,对string不是可靠的,因为一是string
中的数据并没有保证被存储在独立的一块连续内存中;二是string的内部表示形式并没
承诺以一个null字符结束。这也是string的成员函数c_str存在的原因,它返回一个按c风
格设计的指针,指向string的值。


因此,我们可以这样做:
doSomething(s.c_str());


即使是字符串的长度是0也没关系,此时,c_str将返回一个指向null字符的指针。


vector和string的数据只能传给只读取而不修改它的API。即参数使用const修饰的函数。


对于vector,有更多一点的灵活性,如果C风格API没有试图改变vector中元素的个数的
话一般是没有问题的。否则如果对容器插入新元素的话将会产生灾难性的后果。


有序vector经常可以用为关联容器的替代品。


用C风格API返回的元素初始化一个vector:
//C API
size_t fillArray(double *pArray, size_t arraySize);


vector<double> vd(maxNumDoulbes);
vd.resize(fillArray(&vd[0], vd.size());


这个技巧只能工作于vector,因为只有vector承诺了与数组具有相同的潜在内存分布。


如果你想用来自C风格API的数据初始化string对象,只要让API将数据放入一个vector<char>
然后从vector中将数据拷到string:
size_t fillString(char* pArray, size_t arraySize);


vector<char> vc(maxNumChars);
size_t charsWritten = fillString(&vc[0], vc.size());


string s(vc.begin(), vc.begin()+charsWritten);


事实上,让C风格API把数据放入一个vector,然后拷到你实际想要的STL容器中的主意总是
有效的:
size_t fillArray(double *pArray, size_t arraySize);


vector<double> vd(maxNumDouble);
vd.resize(fillArray(&vd[0], vd.size());


deque<double> d(vd.begin(), vd.end());
list<doulbe> l(vd.begin(), vd.end());
set<double> s(vd.begin(), vd.end());


此外,这也提示了vector和string以外的STL容器如何将它们的数据传给C风格API。只要
将容器的每个数据拷到vector,然后将它们传给C API。


void doSomething(const int* pints, size_t numInts);
set<int> intSet;
...
vector<int> v(intSet.bein(), intSet.end());
if(!v.empty()) doSomething(&v[0], v.size));




条款17:使用“交换技巧”来修整过剩容量
如果你的vector有时候容纳了10万个可能的候选人,它的容量会继续保持在至少100,000,
即使后来它容纳了10个。


要避免你的vector持有它不再需要的内存,你需要有一种方法来把它从曾经最大的容量
减少到它现在需要的容量。这样减少容量的方法常常被称为“收缩到合适”。


下面是修整vector过剩容量的方法:
vector<Contestant>(contestants).swap(contestants);//交换所有的信息
vector的拷贝构造函数只分配拷贝的元素需要的内存,所以这个临时vector没有多余的
容量,然后我们让临时vector和contestants交换数据。


同样的技巧可以应用于string:
string s;
...//使s变大,然后删除所有
string(s).swap(s);


交换技巧的变体可以用于清除容器的减少它的容量到你的实现提供的最小值。你可以简单
地和一个默认构造的临时vector或string做个交换:
vector<Contestant> v;
string s;
... //使用v和s
vector<Contestant>().swap(v);  //清除v而且最化它的容量
string().swap(s); //清除s而且最小化它的容量




条款18:避免使用vector<bool>:
做为一个STL容器,vector<bool>有两个问题。第一,它不是一个STL容器。第二,它并不
容纳bool。


一个东西要成为STL容器就必须满足C++标准规定的条件。
如果c是一个T类型对象的容器,且c支持operator[],那么以下代码必须能够编译:
T* p = &c[0];


所以如果vector<bool>是一个容器,则下面代码必须能够编译:
vector<bool> v;
bool* pb = &v[0];


但是它不能编译。因为vector<bool>是一个伪容器,并不保存真正的bool,而是打包bool
以节省空间。每个保存在vector中的bool占用一个单独的比特,而一个8比特的字节将
容纳8个bool。在内部,vector<bool>使用了与位域(bitfield)等价的思想来表示它假装
容纳的bool。


真的bool和化装成bool的位域之间有一个重要的不同:你可以建指向真的bool的指针,但却
禁止有指向单个比特的指针。


引用单个比特也是禁止的,这为vector<bool>接口的设计摆出了难题。vector<bool>::
operator[]需要返回指向一个比特的引用,而不存在这样的东西。
为了解决这个难题,vector<bool>::operator[]返回一个对象,其行为类似于比特的引用,
也称为代理对象。
vector<bool>看起来像这样:
template<typename Allocator>
vector<bool, Allocator>{
public:
class reference{...}; //用于产生引用独立比特的代理类
reference operator[](size_type n); //operator[]返回一个代理
...
}


现在,这段代码不能编译的原因就很明显了:
vector<bool> v;
bool *pb = &v[0]; //错误,右边的表达式是vector<bool>::reference *类型,不是bool*


vector<bool>应避免,标准库提供了两个替代品,它们能满足几乎所有需要。第一个是
deque<bool>。deque提供了几乎所有vector所提供的(唯一值得注意的是reserve和
capacity),而deque<bool>是一个STL容器,它保存真正的bool值。当然,deque内部
内存不是连续的。所以不能传递deque<boo>中的数据给一个希望得到bool数组的C API。


第二个vector<bool>的替代品是bitset。bitset不是一个STL容器,但它是C++标准库的
一部分。与STL容器不同,它的大小(元素数量)在编译期固定,因此它不支持插入和删除
此外,因为它不是一个STL容器,它也不支持iterator。
但就像vector<bool>,它使用一个压缩的表示法,使得它包含的每个值只占用一比特。
它提供vector<bool>特有的flip成员函数,还有一系列其他操作位集所特有的成员函数。
如果不在乎没有迭代器和动态改变大小,你会发现bitset正合你意。




关联容器:
它们自动保持自己有序;它们通过等价而不是相等来查看它们的内容;set和map拒绝重复
实体;map和multimap一般忽略它们包含的每个对象的一半。是的,关联容器也是容器,


STL缺乏基于散列表的容器。




条款19:了解相等的等价的区别:
find算法和set的insert成员函数是很多必须判断两个值是否相同的函数的代表。但它们以
不同的方式完成,find对“相同”的定义是相等,基于operator==。set::insert对“相同”
的定义是等价,通常基于operator<。因为有定义不同,所以有可能一个定义规定了两个
对象有相同的值,而另一个定义判定它们没有。


用于关联容器的比较函数不是operator<或甚至是less,它是用户定义的判断式。
每个标准关联容器通过它的key_comp成员函数来访问排序判断式,所以如果下式求值为真,
两个对象x和y关于一个关联容器c的排序标准有等价值:
!c.key_comp()(x, y) && !key_comp()(y,x)


要完全领会相等和等价的含义,考虑一个忽略大小写的set<string>,这样set需要一个比
较函数的类型,不是真的函数,我们写一个operator()调用了ciStringCompare的仿函数类
struct CIStringCompare:
public binary_function<string, string, bool>{
bool operator()(const string& lhs,
const string &rhs) const
{
return ciStringCompare(lhs, rhs);
}
};


有了CIStringCompare,要建立一个忽略大小写的set<string>就简单了:
set<string, CIStringCompare> ciss;
如果我们向这个set中插入“Persephone"和"persephone",只有第一个字符串加入了,因
为第二个等价于第一个:
ciss.insert("Persephone");
ciss.insert("persephone");


如果,我们现在使用set的find成员函数搜索字符串"persephone",搜索会成功。
if(ciss.find("persephone") != ciss.end())... //这个测试会成功
但如果我们用非成员的find算法,搜索会失败:
if(find(ciss.begin(), ciss.end(), "persephone") != end()) //这个测试会失败


那是因为"persephone"等价于"Persephone",但不等于它,(因为string("persephone")
!= string("Persephone"))。


所以我们建议优先选择容器的成员函数而不是非成员函数。成员和非成员find可能返回不
同结果。


标准关联容器是基于等价而不是相等。


标准关联容器保持有序,所以每个容器必须有一个定义了怎么保持有序的比较函数(默认
是less)。等价是根据这个比较函数定义的。




条款20:为指针的关联容器指定比较类型
假定你有一个string*指针的set,你把一些动物的名字插入进set:
set<string*> ssp; 
ssp.insert(new string("Anteater"));
ssp.insert(new string("Wombat"));
ssp.insert(new string("Lemur"));
ssp.insert(new string("Penguin"));


for(set<string*>::const_iterator i = ssp.begin();
i != ssp.end(); ++i)
cout<<*i<<endl;


结果打印出来的是由个十六进制的数。它们是指针的值。因为set容纳指针,*i不是一个
string,它是一个string的指针。


你可能会把显式循环中的*i改为**i,可以输出字符串,但它们按你想要的顺序输出的机
会只是24分之1。因为它容纳的是指针,所以它以指针的值排序,而不以string值。


通过前面的讲解,你可能回忆起:
set<string*> ssp;
是这个的简写:
set<string*, less<string*> > ssp;
好,为了完全正确,它是:
set<string*, less<string*>, allocator<string*> > ssp; //呵呵 
的简化,


如果你想要string*指针以字符串值确定顺序被储存在set中,你不能使用默认比较仿函数
less<string*>。你必须改为写你自己的比较仿函数类,它的对象带有string*指针并按
照指向的字符串值来进行排序。这样:
struct StringPtrLess: public binary_function<const string*, const string*, bool>
{
bool operator()(const string *ps1, const string* ps2) const
{
return *ps1 < *ps2;
}
}
然后你可以使用StringPtrLess作为ssp的比较类型。
typedef set<string*, StringPtrLess> StringPtrSet;
StringPtrSet ssp; 


for(StringPtrSet::const_iterator i = ssp.begin();
i != ssp.end(); ++i)
cout<<**i<<endl;


上面的代码可以用如下的方法进行替换:
一是,自定义一个解引用的函数对象,然后和for_each联用那个函数:
void print(const string* ps)
{
cout<<*ps<<endl;
}


for_each(ssp.begin(), ssp.end(), print); //ssp中的每个元素上调用print


二是,写一个泛型的解引用仿函数类,然后让它和transform与ostream_iterator连用:
//当本类型的仿函数被传入一个T*时,它们返回一个const T&
struct Dereference
{
template <typename T>
const T& operator()(const T*ptr) const
{
return *ptr;
}
};


transform(ssp.begin(), ssp.end(), ostream_iterator<string>(cout, "\n"),
Dereference());


至此,你必须记住,无论何时你建立一个指针的标准关联容器,你必须记住容器会以指针
的值排序。这基本上不是你想要的,所以你几乎总需要建立自己的仿函数类作为比较类型。


你可能会奇怪为什么必须特意创造一个仿函数类而不是简单地为set写一个比较函数。
例如,你可能想试试:
bool stringPtrLess(const string* ps1,
const string* ps2)
{
return *ps1 < *ps2;
}


set<string*, stringPtrLess> ssp; //但这并不能编译
这是因为set模板的三个参数都是一种类型。但stringPtrLess不是一种类型,它是一个函数
set不要一个函数,它要的是能在内部用实例化建立函数的一种类型。


再次强调,无论何时你建立指针的关联容器,注意你也得指定容器的比较类型。大多数时
候,你的比较类型只是解引用指针并比较所指向的对象。
鉴于这种情况,你手头最好也能有一个用于那种比较的仿函数模板,就像这样:
struct DereferenceLess
{
template <typename PtrType> //使用模板成员函数的优点是,我们不用Dereference<>写
bool operator()(PtrType pT1, 
PtrType pT2) const //参数是值传递的,因为我们希望它们是指针
{
return *pT1<*pT2;
}
};


set<string*, DereferenceLess> ssp;






条款21:永远让比较函数对相等的值返回false
确保你用在关联容器上的比较函数总是对相等的值返回false。


建立一个set,比较类型用less_equal,然后插入一个10:
set<int, less_equal<int> > s; //s以"<="排序
s.insert(10);


现在再插入一次10:
s.insert(10);


关联的比较操作使用的是等价,当执行两个10的比较时,它自然使用的是set的比较函数。
在这里就是operator<=。set将计算这个表达式是否为真:
!(10A<=10B) && !(10B<=10A) //测试10B和10A是否等价
所以上述表达式简化为
!(true)&&!(true)
结果当然是false。也就是说,set得子网的结论是10A与10B不等价,因此不一样,于是
set将两个10插入进来了,也就是说它不再是一个set了。通过使用less_equal作为我们的
比较类型,我们破坏了容器!此外,任何对相等的值返回true的比较函数都会做同样的
事情。


你需要确保你用在关联容器上的比较函数总是对相等的值返回false。


例如,上面描述了该如何写一个比较函数以使得容纳string*指针的容器根据string的值
排序,而不是对指针的值排序。上面的那个比较函数是按升序排序的,但我们现在假设你
需要string*指针的容器的降序排序的比较函数。你大概可能会这么做:
struct StringPtrGreater:
public binary_function<const string*,
const string*,
bool>
bool operator()(const string* ps1, const string* ps2) const
{
return !(*ps1 < *ps2); //把前面小于的代码取反
}
}; //这是不对的,因为取反<返回的是>=,这样,它将对相等的值返回true,对关联
容器来说,它是一个无效的比较函数。


你真正需要的比较类型是这个:
struct StringPtrGreater:
public binary_function<const string*,
const string*,
bool>{
bool operator()(const string* ps1, const string* ps2) const
{
return *ps2 < *ps1;
}
};


比较函数总应该对相等的值返回false。


同样multiset和multimap的比较函数也应该在相等时返回false。


例如:
multiset<int, less_equal<int> > s;  //s仍然以"<="排序


s.insert(10); //10A
s.insert(10); //10B


现在,s里有两个10的拷贝,因此我们期望如果我们在它上面做一个equal_range,我们将会
得到一个对指出包含这两个拷贝的范围的迭代器。但那是不可能的。前面我们说过,STL的
标准容器都是使用等价而不是相等,所以此处equal_range指示出的不是相等的值的范围,
而是等价的值的范围。在这里,s的比较函数说10A和10B是不等价的,所以不可能让它们
同时出现在equal_range所指示的范围内。


明白了吗?除非你的比较函数总是为相等的值返回false。


用于排序关联容器的比较函数必须在它们所比较的对象上定义了个“严格的弱序化(strict 
weak ordering)"。传给sort等算法的比较函数也有同样的限制。




条款22:避免原地修改set和multiset的键:
正如所有标准关联容器,set和multiset保持它们的元素有序,这些容器的正确行为依赖于它
们保持有序。如果你改了关联容器里的一个元素的值,新值可能不在正确的位置,而且那
将破坏容器的有序性。很简单,不是吗?


这对map和multimap特别简单,因为试图改变这些容器里的一个键值的程序将不能编译:


map<int, string> m;
...
m.begin()->first = 10; //错误! map键不能改变
multimap<int, string> mm;
...
mm.being()->first = 20; //错误!multimap键也不能改变


那是因为map<K,V>或multimap<K, V>类型的对象中元素的类型是pair<const K,V>。因为
键的类型const K,它不能改变。(嗯,如果你使用一个const_cast,你或许能改变它)。


本条款的标题没有提及map或multimap。那是因为,原地修改键对map和multimap来说是不可
能的(除非你使用映射),但是它对set和multiset却是可能的。对于set<T>或multiset<T>
类型的对象来说,储存在容器里的元素类型只不过是T,并非const T。因此,set或multiset
里的元素可能在你想要的任何时候改变。不需要映射。


为什么set或multiset里的元素不是常数开始。假设我们有一个雇员的类:
class Employee{
public:
...
const string& name() const; //获取雇员名
void setName(const string& name);
const string& getTitle() const;
void setTitle(string &title);
int idNumber() const;
...
}


建立一个雇员的set,很显然应该只以ID号来排序set:
struct IDNumberLess:
public binary_function<Employee, Employee, bool>
{
bool operator()(const Employee& lhs, 
const Employee& rhs) const
{
return lhs.id.Number() < rhs.idNumber();
}
};


typedef set<Employee, IDNumberLess> EmpIDSet;
EmpIDSet se;


实际上,只有雇员的ID号是set中元素的键。所以,没有理由不能把一个特定雇员信息里的头
衔改成某个有趣的东西。像这样:
Employee selectedID; 
...
EmpIDSet::iterator i = se.find(selectedID);
if (i != se.end())
{
i->setTitle("Corporate Deity"); //修改set内元素的信息
}


本条款的目的是提醒你如果你改变set或multiset里的元素,你必须确保不改变一个键部分--
影响容器有序性的元素部分。否则,你这样会破坏容器,再使用那个容器将产生未定义的结
果,而且那是你的错误。这个限制只针对于被包含对象的键部分,元素的其他部分是开放的
是可以随便改变的。


即使set和multiset的元素不是const,实现仍然有很多方式可以阻止它们被修改。例如,实
现可以让用于set<T>::iterator的operator*返回一个常数T&。即,它可以让set的迭代器
解引用的结果是set元素的常量引用。在这样的实现下,将没有办法修改set或multiset的
元素,因为所有访问那些元素的方法都将在让你访问之前加一个const。


例如,下在实现,在某些STL实现上是不能编译的:
EmpIDSet se;
Employee selectedID;
...
EmpIDSet::iterator i = se.find(selectedID);
if (i != se.end())
{
i->setTitle("Corporate Deity"); //有些STL实现会拒绝这行
}
所以试图修改set或multiset中的元素的代码不可移植。


映射。也就是怎样做才能既正确又可移植。它不难,但是它用到了太多程序员忽略的一个细
节:你必须映射到一个引用。
在下面代码中:
EmpIDSet::iterator i = se.find(selectedID);
if (i != se.end())
{
i->setTitle("Corporate Deity"); //有些STL实现会拒绝这样做,因为*i是const
}
为了让它可以编译并且行为正确,我们必须映射掉*i的常量性,这是正确的用法:
if (i != se.end())
{
const_cast<Employee&>(*i).setTitle("Corporate Deity"); //*i的常量性
} //映射到引用
通过将对象映射到非常量的Employee的引用,我们可以达到这一目的。


下面再看一个看似可能的办法:
一定会有很多人想到如下的代码:
if (i != se.end())
{
const_cast<Employee>(*i).setTitle("Corporate Deity");//映射到一个
//非引用对象
}
它也等价于如下的代码:
if (i != se.end())
{
((Employee)(*i)).setTitle("Corporate Deity"); //但使用C映射语法
}
这两个都能编译,但它们都是错误的。在运行期,它们不能修改*i。在这两种情况里,
映射的结果都是一个*i的副本的临时匿名对象,而setTitle是在匿名的物体上调用的
所以*i并没有被修改。上面两个句法等价于这个:
if (i!=se.end())
{
Employee tempCopy(*i);
tempCopy.setTitle("Corporate Deity");
}
所以这里需要注意,我们是映射到引用。


但我们不能试图映射掉map或multimap键的常量性。


映射是危险的,进行映射将临时剥去类型系统的安全性。


大多数映射可以避免,包括我们刚刚考虑的。如果你要总是可以安全地改变set,
multiset,map或multimap里的元素,下面是正确的方法:
1、定位你想要改变的容器元素。如果你不确定最好的方法,请看45,怎样进行适当搜寻
的指导。
2、拷贝一份要被修改的元素。对map或multimap而言,确定不要把副本的第一个元素
声明为const。毕竟,你想要改变它。
3、修改副本。
4、从容器里删除元素,通常通过调用erase。
5、把新值插入容器。如果新元素在容器的排序顺序中的位置正好相同或相邻于删除的
元素,使用insert的“提示”形式把插入的效率从对数时间改进到分摊的常数时间。
使用你从第一步获得的迭代器作为提示。


看下面最为合适的写法:
EmpIDSet se;
Employee selecetID;


EmpIDSet::iterator i = se.find(selectID);
if(i != se.end())
{
Employee e(*i);
se.erase(i++); //自增这个迭代器以保持它有效
e.setTitle("Corporate Deity");
se.insert(i, e);
}




条款23:考虑用有序的vector代替关联容器
当需要一个提供快速查找的数据结构时,很多STL程序员会立刻想到标准的关联容器:set
multiset,map和multimap。如果查找速度真的很重要,得考虑使用非标准的散列容器,
对于多数应用,被认为是常数时间查找的散列容器要好于保证了对数时间查找的set,
map和它们的multi同事。


标准关联容器的典型实现是平衡二叉查找树。一个平衡二叉树是一个对插入、删除和
查找的混合操作优化的数据结构。


概要:在有序vector中存储数据很有可能比在标准关联容器中保存相同的数据消耗更
少的内存;当页面错误值得重视的时候,在有序vector中通过二分法查找可能比在
一个标准关联容器中查找更快。


但vector的插入和删除都很昂贵,关联容器的插入和删除则很轻量。所以只有当你知道
你的数据结构使用的时候查找几乎不和插入和删除混合时,使用有序vector代替关
联容器才有意义。


在很多应用中,对数据结构的使用可以总结为这样的三个阶段:建立,查找,重组。


下面我们来看看一个使用有序vector代替set的代码骨架:
vector<Widget> vw; //代替set<Widget>
... //建立阶段:很多插入,几乎没有查找。
sort(vw.begin(), vw.end());  //结束建立,模拟一个multiset,你可能更喜欢用
//stable_sort来代替。
Widget w; //用于查找的值的对象
... //开始查找阶段
if(binary_search(vw.begin(), vw.end(), w)) ... //通过binary_search查找
vector<Widget>::iterator i = 
lower_bound(vw.begin(), vw.end(), w); //通过lower_bound查找
if(i != vw.end() && !(w<*i))... //!(w<*i)测试 见条款19
pair<vector<Widget>::iterator,
vector<Widget>::iterator> range = equal_range(vw.begin(), vw.end(), w);
//通过equal_range查找
if (range.first != range.second)
... //结束查找阶段,开始重组阶段
sort(vw.begin(), vw.end()); //开始新的查找阶段


这里最难的是怎么在搜索算法中做出选择,比如binary_search,lower_bound等,见
条款45.


当你决定用vector代替map或multimap时,事情会变得更有趣,因为vector必须容纳
pair对象。毕竟,那是map和multimap所容纳的。但要有注意,如果你声明一个
map<K,V>的对象,或等价的multimap,保存在map中的元素类型是pair<const K, V>。
如果要用vector模拟map或multimap,你必须去掉const,因为当你对vector排序时,它的
元素的值会通过赋值移动,那意味着pair的两个组件都必须是可赋值的。当使用
vector来模拟map<K,V>时,保存在vector中数据的类型将是pair<K,V>,而不是
pair<const K, V>。


map,multimap排序时只作用于元素的key部分(pair的第一个组件),所以当排序vector
时,你必须做一样的事情。你需要为你的pair写一个自定义的比较函数,因为pair的
operator<作用于pair的两个组件。


typedef pair<string, int> Data; 
class DataCompare{
public:
bool operator()(const Data& lhs,
const Data& rhs) const
{
return keyLess(lhs.first, rhs.first);
}


bool operator()(const Data::first_type& k,
const Data& rhs) const //第二种形式
{
return keyLess(k, rhs.first);
}


private:
bool keyLess(const Data::first_type& k1,
const Data::first_type& k2) const //"真的"的比较函数
{
return k1 < k2;
}
};




好了,把有序vector用作map本质上和用作set一样,唯一大的区别是必须把DataCompare
对象用作比较函数:
vector<Data> vd; //代替map<string, int>
... 建立阶段,很多插入
sort(vd.begin(), vd.end(), DataCompare()); //结束建立阶段
//当模拟multimap时,你可能更喜欢用stable_sort来代替。


string s; //用于查找的值的对象
if (binary_search(vd.begin(), vd.end(), s, DataCompare()))...
vector<Data>::iterator i = lower_bound(vd.begin(), vd.end(),
s, DataCompare());


if (i!=vd.end()) && !DataCompare()(s, *i))...


pair<vector<Data>::iterator,
vector<Data>::iterator> range = 
equal_range(vd.begin(), vd.end(), s, DataCompare());
if (range.first != range.end()...
sort(vd.begin(), vd.end(), DataCompare()); 




条款24:当关乎效率时应该在map::operator[]和map-insert之间仔细选择:
假设我们有一个支持默认构造函数以及一个double构造和赋值的Widget类:
class Widget
{
public:
Widget();
Widget(double weight);
Widget &operator=(double weight);
...
};


现在让我们建立一个从int到Widget的map,而且我们想有初始化特定值的映射。如下:
map<int, Widget> m;
m[1] = 1.5;
m[2] = 3.67;
m[3] = 10.5;
m[4] = 45.8;
m[5] = 0.0003;


map的operator[]函数是个奇怪的东西。它与vector,deque和string的operator[]函数
无关,也和内建的数组operator[]无关。相反,map::operator[]被设计为简化“添加
或更新”功能。即,给定:
map<K, V> m;
m[k] = v;
会检查键k是否已经在map里。如果不,就添加上,以v作为这的对应值。如果k已经在map
里,它的关联值被更新成v。


map<int, Widget> m;
m[1] = 1.50;
在这里,m里面还没有任何东西,所以键1在map里没有入口。因此operator[]默认构造
一个Widget来作为关联到1的值,然后返回到那个Widget的引用。最后,Widget成为赋
值目标:被赋值为1.50;


即m[1] = 1.50等价于:
typedef map<int, Widget> IntWidgetMap;
pair<IntWidgetMap::iterator, bool> result = 
m.insert(IntWidgetMap::value_type(1, Widget()));
result.first->second = 1.50;
如果用想要的值构造Widget比默认构造Widget然后进行赋值显然更高效,我们就应该用
直截了当的insert调用来替换operator[]的使用(包括它的构造加赋值):
m.insert(IntWidgetMap::value_type(1, 1.50));
它节省了三次函数调用:一个建立临时的默认构造Widget对象,一个销毁那个临时的
对象和一个对Widget的赋值操作。


每个标准容器都提供了value_type typedef。对于map和multimap(以及非标准容器
的hash_map和hash_multimap也很重要),对这些容器来说,容器元素类型总是某种pair.


现在我们知道当“增加”被执行时,insert比operator[]更高效。当我们做更新时,情
形正好相反。


因此,出于对效率的考虑,当给map添加一个元素时,我们断定insert比operator[]好;
当更新已经在map里的元素值时operator[]更好。


template <typename MapType,
typename KeyArgType,
typename ValueArgType,
typename ValueArgType>
typename MapType::iterator efficientAddOrUpdate(MapType &m,
const KeyArgType &k,
const ValueArgType &v)
{
typename MapType::iterator Ib = 
m.lower_bound(k);


if (Ib != m.end() && !(m.key_comp()(k, Ib->first))){
Ib->second = v;
return Ib;
}
else
{
typedef typename MapType::value_type MVT;
return m.insert(Ib, MVT(k, v));
}
}


当关乎效率时应该在map::operator[]和map::insert之间仔细选择。如果你要更新已存
在的map元素,operator[]更好,但如果你要增加一个新元素,insert则有优势。




条款25:熟悉非标准散列容器
在标准C++库里没有任何散列表。hash_set,hash_multiset,hash_map和hash_multimap。
在C++标准委员会的议案中,散容器的名字是unordered_set,unordered_multiset,
unordered_map和unordered_multimap。恰好是为了避开现存的hash_*名字。


散列容器是关联容器,因此你不该惊讶,正如所有关联容器,它们需要知道储存在容器
中的对象类型,用于这些对象的比较函数,以及用于这些对象的分配器。另外,散列
容器需要散列函数的说明。下面是散列容器声明:
template <typename T,
typename HashFunction,
typename CompareFunction,
typename Allocator = allocator<T>>
class hash_container;


这与SGI的差别在于SGI为HashFunction和CompareFunction提供了默认类型:
template <typename T,
typename HashFunction = hash<T>,
typename CompareFunction = equal_to<T>,
typename Allocator = allocator<T>>
chass hash_set;
注意,SGI设计使用equal_to作为默认比较函数。这违背标准关联容器的约定---默认
比较函数是less。SGI的散列容器确定在一个散列容器中的两个对象是否有相同的值是
通过相等测试,而不是等价。


hash_compare(HashingInfo的默认值)看起来像这样:
template <typename T, typename CompareFunction = less<T> >
class hash_compare{
public:
enum{
bucket_size = 4; //元素对桶的最大比率
min_buckets = 8; //桶的最小数量
};


size_t operator()(const T&) const; //散列函数
bool operator()(const T&,
const T&)const;
...
};
重载operator()在这里是实现散列和比较函数。




迭代器
标准STL容器提供了四种不同的迭代器:iterator,const_iterator,reverse_iterator和
const_reverse_iterator。容器的insert和erase和某些形式只接受其中一种。


还有一个需要注意的迭代器类型:istreambuf_iterator。如果你喜欢STL,但你不喜欢
读取字符流时istream_iterator的性能,istreambuf_iterator可能就是你正在寻找的
工具。




条款26:尽量用iterator代替const_iterator,reverse_iterator和const_reverse_iterator
首先,我们看看vector<T>的insert和erase的样式:
iterator insert(iterator position, const T& x);
iterator erase(iterator position);
iterator erase(iterator rangeBegin, iterator rangeEnd);
每个标准容器都包含了和这差不多的函数,虽然返回类型因容器类型的不同而不同。
需要注意的是:这些方法只接受iterator类型的参数,而不是const_iterator,
reverse_iterator和const_reverse_iterator。总是iterator。虽然容器类支持四种
类型迭代器,但其中的一种类型有着其他所没有的特权。那就是iterator,iterator比
较特殊。


从iterator到const_iterator,从iterator到reverse_iterator和从reverse_iterator到
const_reverse_iterator可以进行隐式转换。reverse_iterator可以通过调用其base
成员函数转换为iterator。const_reverse_iterator也可以类型地通过base转换成
const_iterator。


你应该发现了没有办法从一个const_iterator转换得到一个iterator,也无法从
const_reverse_iterator得到reverse_iterator。而这意味着如果你有一个
const_iterator或者const_reverse_iterator,你会发现很难让它们和容器的一些成员
函数合作。那些成员函数要求iterator,而你无法从const迭代器反过来得到iterator,
当你需要指出插入位置或删除的元素时,const迭代器几乎没有用。


你应该尽量使用iterator代替const或reverse类型的迭代器,可以使得容器的使用更
简单,更高效而且可以避免潜在的bug。


从reverse_iterator转换而来的iterator在转换之后可能需要相应的调整。


减少混用不同类型的迭代器的机会。尽量用iterator代替const_iterator。




条款27:用distance和advance把const_iterator转化成iterator
我们不能把const_iterator映射成iterator,因为对于一些容器而言,iterator和
const_iterator是完全不同的类。在两个毫无关联的类之间进行const_cast映射是
荒谬的,所以reinterpret_cast,static_cast甚到C风格的映射也会导致同样的结果。


不能编译的代码对于vector和string容器来说也许能够通过编译。那是因为通常情况下
大多数实现都会采用真实的指针作为那些容器的迭代器。


下面是一种方法从const迭代器得到iterator:
typedef deque<int> IntDeque;
typedef IntDeque::iterator Iter;
typedef IntDeque::const_iterator ConstIter;


IntDeque d;
ConstIter ci;
...
Iter i(d.begin());
advance(i, distance<ConstIter>(i, ci));


template <typename InputIterat>
typename iterator_traits<InputIterator>::difference_type
distance(InputIterator first, InputIterator last);


InputIterator不可能同时有两种不同的类型,所以直接这样调用
distance(i, ci);是不正确的。
所以这里我们只能显式指明distance调用的模板参数类型:
advance(i, distance<ConstIter>(i, ci));




条款28:了解如何通过reverse_iterator的base得到iterator:
要实现在一个reverse_iterator ri指出的位置上插入新元素,在ri.base()指向的位置
插入就行了。对于insert()操作而言,ri和ri.base()是等价的,而且ri.base()真
的是ri对应的iterator.


要实现在一个reverse_iterator ri指出的位置上删除元素,就应该删除ri.base()的前
一个元素。对于删除操作而言,ri和ri.base()并不等价。


vector<int> v;
...
vector<int>::reverse_iterator ri = 
find(v.rbegin(), v.rend(), 3);
v.erase(--ri.base()); //对于vector,一般来说编译不通过


--ri.base()确实能够指出我们需要删除的元素,而且,它们能够处理除了vector和
string之外的其他所有容器。但对于大多数vector和string的实现,它无法通过编译。
因为在这样的实现下,iterator(const_iterator)会采用内建的指针来实现,所以
ri.base()的结果是一个指针。


而C和C++都规定了不能直接修改函数返回的指针,所以在string和vector的迭代器是
指针的STL平台上,像--ri.base()这样的表达式无法通过编译。


你可以通过下面的方法达到:如果你不能减少调用base()的返回值,只需要先增加
reverse_iterator的值,然后再调用base。
...
v.erase((++ri).base());
因为这个方法适用于所有的标准容器,这是删除一个由reverse_iterator指出的元素的
首选的技巧。




条款29:需要一个一个字符输入时考虑使用istreambuf_iterator:
使用istream_iterator来把一个文本文件拷贝到一个字符串对象中:
ifstream inputFile("interestingData.txt");
string fileData((istream_iteratmr<char>(inputFile)),
istream_iterator<char>());
但这种方法是错误的,因为istream_iterators使用operator>>函数来进行读取,而且
operator>>函数在默认情况下忽略空格。


假如你想保留空格,只要清除输入流的skipws标志就行了:
ifstream inputFile("interestingData.txt");
inputFile.unset(ios::skipws);
string fileData((istream_iterator<char>(inputFile)),
istream_iterator<char>());


但istream_iterators所依靠的operator>>函数进行的是格式化输入,这意味着每次你
调用的时候它们都必须做大量的工作。它们必须建立和销毁岗哨(sentry)对象(为每个
operator>>调用进行建立和清除活动的特殊的iostream对象),它们必须检查可能影
响它们行为的流标志,它们必须进行全面的读取错误检查。
更好的办法是使用istreambuf_iterator来从输入流下读取一个个的字符。


ifstream inputFile("interestingData.txt");
string fileData((istreambuf_iterator<char>(inputFile)),
istreambuf_iterator<char>());
这里不需要unset,istreambuf_iterator不忽略任何字符。




算法
copy_if,一个来自最初的HP STL的算法,但在标准化的过程中去掉了。


你不能随意调用remove或它的兄弟remove_if和unique,除非你真的知道这些算法做和
不做什么。特别是当你删除的区间容纳的是指针的时候。同样,很多算法只和有序区间
配合。




条款30:确保目标区间足够大
STL容器在被添加时(通过insert,push_frong,push_back等)会自动扩展它们自己来容纳
新对象。
例如,将一个vector内的元素处理之后接到另一个vector容器的后面:
vector<int> results;
transform(values.begin(), values.end(),
back_inserter(results), fun));


我们还可以调用front_inserter适配器来将元素逆序安插在容器前面,还可以通过调用
inserter来将元素安插在容器的任意位置。


但不管是你否使用了back_inserter,front_inserter或inserter,每次对目的区间的
插入只完成一个对象。这对于连续内存容器(vector,string,deque)来说可能很昂贵。


无论何时你使用一个要求指定目的区间的算法,确保目的区间足够大或者算法执行
时可以增加大小。如果你选择增加大小,就使用插入迭代器,比如
ostream_iterator,back_inserter,front_iterator和inserter返回的迭代器。




条款31:了解你的排序选择
sort是一个令人称赞的算法,但有时候你不需要完全排序。比如,你有一个Widget的
vector,现在你只需要找出vector中的最高的20个,你需要做的只是排序以鉴别也
20个最好的Widget,剩下的可以保持无序。你需要的是部分排序,有一个算法叫做
partial_sort:


bool qualityCompare(const Widget& lhs, const Widget& rhs)
{
//返回lhs的质量是不是比rhs的质量好
}
...
partial_sort(widgets.begin(),
widgets.begin()+20, //把最好的20个元素(按顺序)放在widgets的前端
widgets.end(),
qualityCompare);


调用partial_sort之后,widgets的前20个元素是容器中最好的而且它们按顺序排列,也
就是,质量最高的Widget是widgets[0],第二高的是widgets[1]等。


如果你只关心能把20个最好的给你的20个客户,但你不关心哪个Widget给哪个客户,
partial_sort就给了你多于需要的东西。在这种情况下,你需要的只是任意顺序的20
个最好的Widget。STL有一个算法精确的完成了你需要的,它叫做nth_element。


nth_element(widgets.begin(),
widgets.begin() + 19,
widgets.end(),
qualityCompare);
如你所见,调用nth_element本质上等价于调用partial_sort。它们结果的唯一区别是
partial_sort排序了在位置1-20的元素,而nth_element不。但是两个算法都把20个
质量最高的Widget移动到vector前端。


partial_sort和nth_element以任何它们喜欢的方式排序值等价的元素,而且你不能控制
它们在这方面行为。


对于完整的排序,你有稍微多一些的控制权。有些排序算法是稳定的。在稳定排序中,
如果一个区间中的两个元素有等价的值,它们的相对位置在排序后不改变。不稳定
的算法没做这个保证。


partial_sort是不稳定的。nth_element,sort也是不稳定的。如果当你排序的时候你
需要稳定性,你可能要使用stable_sort。STL并不包含partial_sort和nth_element
的稳定版本。


nth_element除了能帮你找到区间顶部的n个元素,它也可以用于找到区间的中值或者
找到在指定百分点的元素:


vector<Widget>::iterator begin(widgets.begin());
vector<Widget>::iterator end(widgets.end());


vector<Widget>::iterator goalPosition;


goalPosition = begin + widgets.size() / 2;
nth_element(begin, goalPosition, end, qualityCompare);


vector<Widget>::size_type goalOffset = 
0.25*widgets.size();
nth_element(begin, begin+goalOffset, end, qualityCompare); 
现在begin+goalOffset指向质量等级是75%的Widget。


如果你真的需要把东西按顺序放置,sort,partial_sort,stable_sort都很优秀。


假设你现在要从一个容器中鉴别出所有质量等级为1或2的。当然你可以按照质量排序
这个vector,然后搜索第一个质量等级比2差的。那就可以鉴别出质量差的Widget的区间
起点。


一个更好的策略是使用partition算法,它重排区间中的元素以使所有满足某个标准的
元素都在区间的开头。


比如,移动所有质量等级为2或更好的Widget到widgets前端,我们定义一个函数来鉴别
哪个Widget是这个级别。
bool hasAcceptableQuality(const Widget& w)
{
//返回w质量等级是否是2或更高
}


vector<Widget>::iterator goodEnd = 
partition(widgets.begin(), widgets.end(), hasAcceptableQuality);
返回一个指向第一个不满足的widget的迭代器


partition提供了一个稳定的版本stable_partition。


算法sort, stable_sort,partial_sort,nth_element需要随机访问迭代器,所以它们可
能只能用于vector, string, deque和数组。list不能使用上面的这些算法,但list提供
sort成员函数来对它进行排序。


但partition和stable_partition与上面的排序算法不同,它们只需要双向迭代器。因此
你可以在任何标准序列迭代器上使用partition和stable_partition。


需要更小资源(时间和空间)的排序算法排序:
partition
stable_partition
nth_element
partial_sort
sort
stable_sort。


只有容器成员函数可以除去容器元素。




条款35:通过mismatch或lexicographical比较实现简单的忽略大小写字符串比较。
想要使用字符串忽略大小写比较的程序员通常需要两种不同的调用接口,一种类似
strcmp(返回一个负数、零或正数),另一种类似operator(返回true或false)。
下面演示用STL算法实现两种调用接口:


名字最长的算法是:set_symmetric_difference
名字第二长的算法是lexicographical




条款36:了解copy_if的正确实现:
STL有很多有趣的地方,其中一个虽然有11个名字带copy的算法:
copy copy_backward
replace_copy reverse_copy
replace_copy_if unique_copy
remove_copy rotate_copy
remove_copy_if partial_sort_copy
unintialized_copy
但没有一个是copy_if。


下面是一个正确的copy_if的实现:
templat <typename InputIterator,
typename OutputIterator,
typename Predicate>
OutputIterator copy_if(InputIterator begin,
InputIterator end,
OutputIterator destBegin,
Predicate p)
{
while(begin != end)
{
if(p(*begin)) *destBegin++ = *begin;
++begin;
}
return destBegin;
}




条款37:用accumulate或for_each来统计区间:
count告诉你区间中有多少等于某个值的元素,而count_if告诉你有多少元素满足一个
判断式。区间中的最小和最大值可以通过min_element和max_element获得。


但有时,你需要用一些自定义的方式统计summarize区间,而且在那些情况中,你需要比
count,count_if,min_lement或max_element更灵活的东西。比如,你可能想要对一个
容器中的字符串长度求和。你可能想要数的区间的乘积。你可能想要point区间的平均
坐标。在那些情况中,你需要统计一个区间,但你需要有定义你需要统计的东西的能力
STL为你准备了那样的算法,它叫作accumulate。不像大部分算法,它不存在于
<algorithm>。它和其他三个“数值算法”都在<numeric>中,那三个其他算法是
inner_product, adjacent_difference和partial_sum。


就像很多算法accumulate存在两种形式。带有一对迭代器和初始值的形式可以返回初始
值加迭代器划分出的区间中值的和:
list<double> ld;
...
double sum = accumulate(ld.begin(), ld.end(), 0.0);
注意初始值指定为0.0,不是简单的0。这很重要。0.0的类型是double,所以accumulate
内部使用了一个double类型的变量来存储计算的和。如果这么写:
double sum = accumulate(ld.begin(), ld.end(), 0);
如果初始值是int 0,所以accumulate内部就会使用一个int来保存它计算的值。那个int
最后变成accumulate的返回值,而且它用来初始化和变量。且可能它把每次加法后的结
果转换为一个int。


accumulate只需要输入迭代器,所以你甚至可以使用istream_iterator和
istreambuf_iterator。




仿函数、仿函数类、函数等。
在STL中的每个地方,你都可以看见仿函数和仿函数类。
ptr_fun、mem_fun和mem_fun_ref。




条款38:把仿函数类设计为用于值传递
C和C++都不允许你真的把函数作为参数传递给其他函数。取而代之的是,你必须传指针
给函数。并且C和C++标准库都遵循函数指针是值传递。


STL函数对象在函数指针之后成型,所以STL中的习惯是当传给函数和从函数返回时函数
对象也是值传递的(也就是拷贝)。
最好的证据是标准的for_each声明,这个算通过值传递获取和返回函数对象。


函数对象以值传递和返回,这样你的函数对象必须满足以下的两个条件:一是,你的函数
对象应该很小。二是,你的函数对象必须单态。


但如果确实需要太多数据或多态,你可以带着你要放进你的仿函数类的数据或多态,把
它们移到另一个类中。然后给你的仿函数一个指向这个新类的指针。但这种技术就要
注意你的仿函数类必须支持合理的拷贝方式。即要防止资源泄漏。最简单的是使用引用
计数,使用类似Boost的shared_ptr。




条款39:用纯函数做判断式
1、判断式是返回bool。
2、纯函数是返回值只依赖于参数的函数。在C++中,由纯函数引用的所有数据不是作为
参数传进就是在函数生存期内是常量。
3、一个判断式类是一个仿函数类,它的operator()函数是一个判断式。




条款40:使仿函数类可适配
ptr_fun做的唯一的事是使用一些typedef有效。就是这样。notl需要这些typedef,这就
是为什么可以把notl就用于ptf_fun,但不能直接对函数对象应用notl。因为是低级的
函数指针,缺乏notl需要的typedef。


not1不是STL中唯一有那些要求的组件。由个标准函数适配器not1,not2,bind1st,
bind2nd都需要存在某些typedef,一些其他人写的非标准SLT兼容的适配器也需要。
提供这些必要的typedef的函数对象称为可适配的,而缺乏那些typedef的函数对象不
可适配。可适配的比不可适配的函数对象可以用于更多的场景,所以只要能做到你就
应该使你的函数对象可适配。这也不花费你任何的东西。


那些typedef是argument_type、first_argument_type,second_argument_type和
result_type。你可以通过继承一些结构来使用你的仿函数是可适配的,如
std::unary_function、std::binary_function。
但unary_function和binary_function是模板,所以你不能直接继承它们。取而代之的是
你必须从它们产生的类继承,因此你需要指定上些类型实参。这些实参包括所有参数以
及它的operator()的返回值。


一个类结构内部如果没有私有的数据,则我们通常用struct来声明,这样的类称为无态
状类。可以看看STL中无状态的仿函数类,如less<T>,plus<T>等,也是用struct.


一般来说,传给unary_function或binary_function的非指针类型都去掉了const和引用。
但当operator()的参数是指针时这个规则就变了,用于带有或返回指针的仿函数的一般
规则是传给unary_function或binary_function的类型与operator()的参数类型和返回
值类型是一样的。


如果你的仿函数重载了operator()的调用形式,你就不能从unary_function或
binary_function继承来使仿函数对象获得可适配性。




条款41:了解使用ptr_fun、mem_fun和mem_fun_ref的原因:


当函数对象的typedef必要时,你就要使用ptr_fun。


mem_fun和mem_fun_ref的情况则完全不同。只要你传一个成员函数给STL组件,你就必
须使用它们,因为除了增加typedef之处,它们把调用语法从一个通常用于成员函数的
适配到在STL中到处使用的。




条款42:确定less<T>表示operator<
试图修改std里的组件确实是禁止的(而且这么做通常被认为是行为未定义的范畴),但
是在一些情况下修补是允许的。具体来说,程序员被允许自定义类型特化std内的模板。
特化std模板的选择几乎总是更为优先。例如,智能指针类的作都经常想让他们的类在排
序的时候行为表现得像内建指针,因些用于智能指针类型的std::less特化并不罕见。
例如下面内容,是Boost库的shared_ptr的一部分:
namespace std{
template <typename T>
struct less<boost::shared_ptr<T> >: //boost是一个namespace
public 
binary_function<boost::shared_ptr<T>,
boost::shared_ptr<T>,
bool>
{
bool operator()(const boost::shared_ptr<T> &a,
const boost::shared_ptr<T> &b) const
{
return less<T*>()(a.get(), b.get*());
}
};


如果你不特例化less:例如下
struct MaxSpeedCompare:
public binary_function<Widget, Widget, bool>{
bool operator()(const Widget& lhs, const Widget& rhs) const
{
return lhs.maxSpeed() < rhs.maxSpeed();
}
};


我们使用MaxSpeeCompare作为比较类型,因此避免了默认比较类型的使用(当然也就是 
less<Widget>:
multiset<Widget, MaxSpeedCompare> widgets;


使用特例化的less<Widget>:
multiset<Widget> widgets; //默认使用特例化的less来排序


如果你使用less(明确或者隐含),保证它表示operator<。如果你想要使用一些其他
标准排序对象,建立一个特殊的不叫做less的仿函数类。




使用STL编程:
条款43:尽量用算法调用代替手写循环:
每个算法至少接受一对用来指示将被操作的对象区间的迭代器。比如,min_element可以
找出此区间中的最小的值,而accumulate则对区间内的元素作某种形式的整体求和运算,
partition将区间内的元素分割为满足和不满足某判决条件的两个部分。当算法被执行时
它们必须检查指示给它的区间中的每个元素,并且是按你所期望的方式进行的:从区间
的起始点循环到结束点。有一些算法,比如find和find_if,可能在遍历完成前就返回了
但即使是这些算法,内部都包含一个循环。


所以算法内部是一个循环。




条款44:尽量使用成员函数代替同名的算法
有些容器拥有和STL算法同名的成员函数。关联容器提供了count、find、lower_bound、
upper_bound和equal_range,而list提供了remove、remove_if,unique、sort、merge
和reverse。大多数情况下,你应该使用成员函数代替算法。


例如find算法,ste成员函数只需要执行对数时间,set::find只需执行不超过40次比较
来查找它,而一般只需要大约20次。相反,find算法运行花费线性时间。


大多数的关联容器的实现都是使用红黑树---平衡树的一种---失衡度可能达到2。在这样
的实现中,对一百万个元素的set进行搜索所需要最多的比较次数是38次。但对绝大部
分的搜索情况而言,只需要不超过22次。一个基于完全平衡树的实现绝不需要超过21次
比较,但在实践中,完全平衡树的效率总不来说不如红黑树。所以大多数的STL实现都
使用红黑树。


find算法使用的是相等,而find成员函数用的是等价。


这一差别对map和multimap尤其明显,因为它们容纳的是对象对(pair object),而它们
的成员函数只在意对象对的key部分。因此,count成员函数只统计key值匹配的对象对
的数目(所谓“匹配”,自然是检测等价情况);对象对的value部分被忽略。成员函数
find、lower_bound、upper_bound和equal_range也是如此。但算法find、count、
lower_bound等的是基于对象对的全部组成部分。


对于标准的关联容器,选择成员函数而不是同名的算法有几个好处。首先,你得到的是
对数时间而不是线性时间的性能。其次,你判断两个元素“相同”使用的是等价,这是
关联容器的默认定义。第三,当操纵map和multimap时,你可以自动处理key值而不是
(key,value)对。


再看看list的与算法同名的成员函数身上。这几乎全部是关于效率的。每个被list作了
特化的算法(remove,remove_if,unique,sort,merge和reverse)都要拷贝对象,而list
的特别版本什么都没有拷贝;它们只是简单的操纵指针。


记住:list成员函数的行为和它们的算法兄弟的行为经常不相同。


如果你真的想从容器中清除对象的话,调用remove,remove_if和unique算法后,必
须紧接着调用erase函数;但list的remove,remove_if和unique成员函数真的去掉了元素
后面不需要接着调用erase。


在sort算法和list的sort成员函数间的一个重要区别是前者不能用于list。作为单纯的
双向迭代器,list的迭代器不能传给sort算法。merge算法和list的merge成员函数之间
也存在着巨大差异。算法merge被限制为不能修改源范围,但list::merge总是修改它
的宿主list。




条款45:注意count,find,binary_search,lower_bound,upper_bound和equal_range的
区别:
要选择搜索策略,必须依赖于你的迭代器是否定义了一个有序区间。如果是,你就可以
通过binary_search,lower_bound,upper_bound和equal_range来加速(通常是对数时间)
搜索。如果迭代器并没有划分一个有序区间,你就只能用线性时间的算法count,
count_if,find和find_if。


如果你有一个无序区间,你的选择是count或着find。


如果是为了检查一个值是否在一个容器中存在,你即可以使用find也可以使用count:
就像这样:
if(find(lw.begin(), lw.end(), w) != lw.end())
或if(count(lw.begin(), lw.end(), w))
但当搜索成功时,count的效率比较低,因为当找到匹配的值后find就停止了,而count
必须继续搜索,直到区间的结尾以寻找其他匹配的值。


count和find算法都使用相等来搜索,而binary_search,lower_bound,upper_bound,
equal_range则用等价。


要测试在有序区间是否存在一个值,使用binary_search。不像标准C(C++)库中的
bsearch。binary_search只返回一个bool:表示这个值是否在容器中。
就像问:“这在吗?”


这样使用binary_search:
if(binary_search(vw.begin(), vw.end(), w))


当你用lower_bound来寻找一个值的时候,它返回一个迭代器,这个迭代器指向这个值
的第一个拷贝(如果找到的话)或者到可以插入这个值的位置(如果没有找到)。
就像问:“它在吗,如果是,第一个拷贝在哪里?如果不是,它将在哪里?”
和find一样,你必须测试lower_bound的结果,但不像find要检测它的返回值是否等于
end迭代器。取而代之的是,你必须检测lower_bound所标示出的对象是不是你需要的值


很多程序员这么用lower_bound:
vector<Widget>::iterator i = lower_bound(vw.begin(), vw.end(), w);
if (i != vw.end() && *i == w)


但上面的代码有一个bug,就是*i == w,lower_bound搜索用的是等价。你必须确认使用
了和lower_bound相同的比较函数。


这儿有一个简单的方法:使用equal_range。equal_range返回一对迭代器,第一个等于
lower_bound返回的迭代器,第二个等于upper_range返回的(也就是,等价于要搜索
值区间的末迭代器的下一个)。
如果这两个迭代器相同,就意味着对象的区间是空的,没有这个值。在一个容器内查找
某个值:
VWIterPari p = equal_range(vw.begin(), vw.end(), w);
if (p.first != p.second)


第二个注意的是equal_range返回的东西是两个迭代器,对它们作distance就等于区间
中对象的数目。结果,equal_range不光完成了搜索有序区间的任务,而且完成了计数。
计数功能:
cout<<distance(p.first, p.second);




如果我们有一个容器,而不是一个区间。在这种情况下,我们必须区别序列和关联容器。
对于标准的序列容器(vector,string,deque和list),你就该容器的begin和end迭代
器来划分出区间。
对于标准关联容器(set,multiset,map和multimap)来说是不同的,因为它们提供了
搜索的成员函数,它们往往是比用STL算法更好的选择。并且,成员函数通常和相应
的算法有同样的名字。所以前面的讨论推荐你使用的算法count,find,equal_range,
lower_bound,upper_bound,在关联容器中都存在同名的成员函数来代替。


要测试在set或map中是否存在某个值,可以使用count的惯用法来进行检测:
set<Widget> s;
if (s.count(w))


要测试某个值在multiset或multimap中是否存在,find往往比count好,因为一旦找到等
于期望值的单个对象,find就可以停下了,而count,在最遭的情况下,必须检测容器里
的每个对象。对于set和map,这个不是问题,因为set不允许重复的值,而map不允许
重复的键。


但是count给关联容器计数是可靠的。特别的,它比调用equal_range然后应用distance
到结果迭代器更好。


对于multi容器,如果不只有一个值存在,find并不保证能识别出容器里的等于给定值的
第一个元素;它只识别这些元素中的一个。你应该使用lower_bound来进行这个工作。




条款46:考虑使用函数对象代替函数作算法的参数:
把STL函数对象传递给算法所产生的代码一般比传递真的函数高效。
sort(v.begin(), v.end(), greater<double>());
sort(v.begin(), v.end(), doubleGreater);


原因是:内联。


我们不可能把一个函数作为参数传给另一个函数。当我们试图把一个函数作为参数时,
编译器默默地把函数转化为一个指向那个函数的指针。


当sort模板实例化时,这是产生函数的声明:
void sort(vector<double>::iterator first,
vector<double>::iterator last,
bool (*comp)(double, double));


把函数指针作为参数会抑制内联。


把函数对象作为算法的参数带来的不仅仅是巨大的效率提升。也能让你的代码变得更具
有移植性。




条款47:避免产生只写代码
很容易写,但很难读和理解。




条款48:总是#include适当的头文件:
几乎所有的容器都在同名的头文件里,比如,vector在<vector>声明,list在<list>中
声明等。例外的是<set>和<map>。<set>声明了set和multiset,<map>声明了map和
multimap。


除了由个算法外,所有的算法都在<algorithn>中声明。例外的是accumulate,
inner_product,adjacent_difference和partial_sum,这些算法在<numeric>中声明。


特殊的迭代器,包括istream_iterator和istreambuf_iterator,在<iterator>中声明。


标准仿函数(比如less<T>)和仿函数适配器(比如not1,bind2nd)在<functional>中声明




条款49:学习破解有关STL的编译器诊断信息:
string不是一个类,它是typedef。实际上,它是这个的typedef:
basic_string<char, char_traits<char>, allocator<char> >


。如果你使用的是命令行编译器,通常可以很容易地用一个类似
sed的程序或一种脚本语言比如perl、python或ruby来完成。(你可以在Zolman的
文章——《Visual C++的STL错误信息解码器》[26]——里找到一个这样的脚本的
例子。)


名字中的前导下划线随后有一个大写字母,这样的名字是为实现而保留。这是用来实现
STL一些部分的一个内部模板。


实际上,几乎所有STL实现都使用某种内在的模板来实现标准关联容器set,map,multiset
multimap。
0 0