条款 2: 小心对“容器无关代码”的幻想
STL是建立在泛型之上的。数组泛化为容器,参数化了所包含的对象的类型。函数泛化为算法,参数化了所用的迭代器的类型。指针泛化为迭代器,参数化了所指向的对象的类型。
这只是个开始。独立的容器类型泛化为序列或关联容器,而且类似的容器拥有类似的功能。标准的内存相邻容器(参见条款1)都提供随机访问迭代器,标准的基于节点的容器(再参见条款1)都提供双向迭代器。序列容器支持push_front或push_back,但关联容器不支持。关联容器提供对数时间复杂度的lower_bound,upper_bound和equal_range成员函数,但序列容器却没有。
随着泛化的继续,你会自然而然地想加入这个运动。这种做法值得赞扬,而且当你写你自己的容器、迭代器和算法时,你会自然而然地推行它。唉,很多程序员试图把它推行到不同的样式。他们试图在他们的软件中泛化容器的不同,而不是针对容器的特殊性编程,以至于他们可以用,可以说,现在是一个vector,但以后仍然可以用比如deque或者list等东西来代替——都可以在不用改变代码的情况下来使用。也就是说,他们努力去写“容器无关代码”。这种可能是出于善意的泛化,却几乎总会造成麻烦。
最热心的“容器无关代码”的鼓吹者很快发现,写既要和序列容器又要和关联容器一起工作的代码并没有什么意义。很多成员函数只存在于其中一类容器中,比如,只有序列容器支持push_front或push_back,只有关联容器支持count和lower_bound,等等。在不同种类中,甚至连一些如insert和erase这样简单的操作在名称和语义上也是天差地别的。举个例子,当你把一个对象插入一个序列容器中,它保留在你放置的位置。但如果你把一个对象插入到一个关联容器中,容器会按照的排列顺序把这个对象移到它应该在的位置。举另一个例子,在一个序列容器上用一个迭代器作为参数调用erase,会返回一个新迭代器,但在关联容器上什么都不返回。(条款9给了一个例子来演示这点对你所写的代码的影响。)
假设,然后,你希望写一段可以用在所有常用的序列容器上——vector, deque和list——的代码。很显然,你必须使用它们能力的交集来编写,这意味着不能使用reserve或capacity(参见条款14),因为deque和list不支持它们。由于list的存在意味着你得放弃operator[],而且你必须受限于双向迭代器的性能。这意味着你不能使用需要随机访问迭代器的算法,包括sort,stable_sort,partial_sort和nth_element(参见条款31)。
另一方面,你渴望支持vector的规则,不使用push_front和pop_front,而且用vector和deque都会使接合方式和成员函数方式的sort彻底失败。在上面约束的联合下,后者意味着你不能在你的“泛化的序列容器”上调用任何一种sort。
这是显而易见的。如果你冒犯里其中任何一条限制,你的代码会在至少一个你想要使用的容器配合时发生编译错误。可见这种代码有多阴险。
这里的罪魁祸首是不同的序列容器所对应的不同的迭代器、指针和引用的失效规则。要写能正确地和vector, deque和list配合的代码,你必须假设任何使那些容器的迭代器,指针或引用失效的操作符真的在你用的容器上起作用了。因此,你必须假设每次调用insert都使所有东西失效了,因为deque::insert会使所有迭代器失效,而且因为缺少capacity,vector::insert也必须假设使所有指针和引用失效。(条款1解释了deque是唯一一个在使它的迭代取失效的情况下指针和引用仍然有效的东西)类似的理由可以推出一个结论,所有对erase的调用必须假设使所有东西失效。
想要知道更多?你不能把容器里的数据传递给C风格的界面,因为只有vector支持这么做(参见条款16)。你不能用bool作为保存的对象来实例化你的容器,因为——正如条款18所阐述的——vector并非总表现为一个vector,实际上它并没有真正保存bool值。你不能期望享受到list的常数时间复杂度的插入和删除,因为vector和deque的插入和删除操作是线性时间复杂度的。
当这些都说到做到了,你只剩下一个“泛化的序列容器”,你不能调用reserve,capacity,operator[],push_front,pop_front,splice或任何需要随机访问迭代器的算法;调用insert和erase会有线性时间复杂度而且会使所有迭代器、指针和引用失效;而且不能兼容C风格的界面,不能存储bool。难道这真的是你想要在你的程序里用的那种容器?我想不是吧。
如果你控制住了你的野心,决定愿意放弃对list的支持,你仍然放弃了reserve,capacity,push_front,and pop_front;你仍然必须假设所有对insert和erase的调用有线性时间复杂度而且会使所有东西失效;你仍然不能兼容C风格的布局;而且你仍然不能储存bool。
如果你放弃了序列容器,把代码改为只能和不同的关联容器配合,这情况并没有什么改善。要同时兼容set和map几乎是不可能的,因为set保存单个对象,而map保存对象对。甚至要同时兼容set和multiset(或map和multimap)也是很难的。set/map的insert成员函数只返回一个值,和他们的multi兄弟的返回类型不同,而且你必须避免对一个保存在容器中的值的拷贝份数作出任何假设。对于map和multimap,你必须避免使用operator[],因为这个成员函数只存在于map中。
面对事实吧:这根本没有必要。不同的容器是不同的,而且它们的优点和缺点有重大不同。它们并不被设计成可互换的,而且你做不了什么包装的工作。如果你想试试看,你只不过是在考验命运,但命运并不想被考验。
接着,当天黑以后你认识到你决定使用的容器,嗯,不是最理想的,而且你需要使用一个不同的容器类型。你现在知道当你改变容器类型的时候,不光要修正编译器诊断出来的问题,而且要检查所有使用容器的代码,根据新容器的性能特征和迭代器,指针和引用的失效规则来看看那些需要修改。如果你从vector切换到其他东西,你也需要确认你不再依靠vector的C兼容的内存布局;如果你是切换到一个vector,你需要保证你不用它来保存bool。
既然有了要一次次的改变容器类型的必然性,你可以用这个常用的方法让改变得以简化:使用封装(encapsulating),封装,再封装。其中一种最简单的方法是通过自由地对容器和迭代器类型使用typedef。因此,不要这么写
class Widget { ... };
vector<Widget> vw;
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 vw;
Widget bestWidget;
...
WCIterator i = find(vw.begin(), vw.end(), bestWidget);
这是改变容器类型变得容易得多,如果问题的改变是简单的加上用户的allocator时特别方便。(一个不影响对迭代器/指针/参考的失效规则的改变)
class Widget { ... };
template<typename T> // 关于为什么这里需要一个template
SpecialAllocator { ... }; // 请参见条款 10
typedef vector<Widget, SpecialAllocator<Widget> > WidgetContainer;
typedef WidgetContainer::iterator WCIterator;
WidgetContainer vw; // 仍然能用
Widget bestWidget;
...
WCIterator i = find(vw.begin(), vw.end(), bestWidget); // 仍然能用
如果typedef带来的代码封装作用对你来说没有任何意义的话,你仍然会称赞它们可以节省许多工作。比如,你有一个如下类型的对象
map<string,
vectorWidget>::iterator,
CIStringCompare> // CIStringCompare是“忽略大小写的字符串比较”
// 参见条款19
而且你要用const_iterator遍历这个map,你真的想不止一次地写下
map<string, vectorWidget>::iterator, CIStringCompare>::const_iterator
? 当你使用STL一段时间以后,你会认识到typedef是你的好朋友。
typedef只是其它类型的同义字,所以它提供的的封装是纯的词法(译注:不像#define是在预编译阶段替换的)。typedef并不能阻止用户使用(或依赖)任何他们不应该用的(或依赖的)。如果你不想暴露出用户对你所决定使用的容器的类型,你需要更大的火力,那就是class。
要限制如果用一个容器类型替换了另一个容器可能需要修改的代码,就需要在类中隐藏那个容器,而且要通过类的接口限制容器特殊信息可见性的数量。比如,如果你需要建立一个客户列表,请不要直接用list。取而代之的是,建立一个CustomerList类,把list隐藏在它的private区域:
class CustomerList {
private:
typedef list<Customer> CustomerContainer;
typedef CustomerContainer::iterator CCIterator;
CustomerContainer customers;
public: // 通过这个接口
... // 限制list特殊信息的可见性
};
一开始,这样做可能有些无聊。毕竟一个customer list是一个list,对吗?哦,可能是。稍后你可能发现从列表的中部插入和删除客户并不像你想象的那么频繁,但你真的需要快速确定客户列表顶部的20%——一个nth_element算法特制的任务(参见条款31)。但nth_element需要随机访问迭代器,不能兼容list。在这种情况下,你的客户“list”可能更应该用vector或deque来实现。
当你决定作这种更改的时候,你仍然必须检查每个CustomerList的成员函数和每个友元,看看他们受影响的程度(根据性能和迭代器/指针/引用失效的情况等等),但如果你做好了对CustomerList地实现细节做好封装的话,那对CustomerList的客户的影响将会很小。你写不出容器无关性代码,但他们可能可以。