《Effective STL》条款1-条款2

来源:互联网 发布:linux 查看cpu日志 编辑:程序博客网 时间:2024/05/16 05:27

  • 条款1仔细选择你的容器
  • 条款2小心对容器无关代码的幻想

条款1:仔细选择你的容器

先回顾一下STL中的容器:

  • 标准STL序列容器:vector、string、deque、list
  • 标准STL关联容器:set、multiset、map、multimap
  • 非标准序列容器slist、rope
  • 非标准关联容器hash_set、hash_multiset、hash_map、hash_multimap。条款 25介绍基于散列的容器和标准关联容器的不同。
  • vector作为string的替代品。条款 13 有详细介绍。
  • vector作为标准关联容器的替代品。条款 23有介绍,有时vector在时间和空间上的表现比关联容器好。
  • 集中标准非STL容器,包括数组、bitset、valarray、stack、queue、priority_queue。它们是非STL容器,因此讲的比较少。注意,数组可以使用STL算法,因为数组指针可以当做迭代器。
如何选择使用的容器是非常重要的一个话题,STL标准给提供了在vector、deque、list之间的选择方案:vector、list、deque提供了不同的复杂度,应该这样用:vector是可以默认使用的序列,当频繁对序列中部进行插入删除时应该使用list,当大部分插入和删除发生在序列头或尾时可以使用deque。

上面这么做的原因是:vector是连续内存,当在中部插入删除都要移动元素;list是双向链表,可以在中部方便插入删除;deque是双端队列,内存是一段一段连续的,在头部或尾部插入删除不需移动元素,且它预先分配了空间,不像list需要频繁动态开辟内存。

STL容器的分类:基于连续内存容器和基于节点的容器

连续内存容器(基于数组容器):容器的内存是一个或多个连续的内存块;如果插入或删除一个元素,就需要移动一部分元素。这样影响了效率(条款 14、15)和异常安全。标准连续内存容器有vector、string、deque;非标准连续内存容器有rope。

基于节点额容器:每个节点是个内存块,且只保存一个元素,节点之间通过指针连接起来,这样插入和删除元素只需要改变指针即可,不需要移动元素。list和slist是基于节点的链表容器,标准关联容器(底层为平衡树)也是基于节点的。非标准散列容器也是基于节点,可参考条款 25。

下面是关于使用标准STL容器的准则:

1、如果可以在容器的任何一个位置插入一个新元素的能力,那么使用序列容器,关联容器做不到。关联容器底层是RB-Tree,元素插入的位置和key值相关。
2、如果关心元素在容器中的顺序,不要使用散列容器。散列容器通过散列函数映射元素位置,元素在容器中顺序具有随机性。
3、必须使用标准C++容器吗?如果是,取出散列容器、slist和rope。
4、需要什么类型的迭代器。需要随机访问迭代器,那么只能使用vector、string、deque,还可以考虑rope(条款 50)。需要双向迭代器,略过slist(条款 50)和散列容器的一般实现(条款 25)。
5、插入或删除元素时,是否在意元素的移动?是,就不要选择连续内存容器(条款 5)。
6、容器中的数据内存布局是否需要兼容C? 是的话,只能使用vector(条款 16)。
7、查找速度是否重要?是的话,可以使用散列容器(条款 25)、排序的vector(条款 23)和标准关联容器。
8、是否介意容器底层使用了引用计数器?如果是,不要使用string(条款 13),也不要使用rope,rope的权威实现是基于引用计数的(条款 50)。重新审核string,可以使用vector。
9、是否需要插入和删除的事务性语义?即是否需要可靠的回退插入和删除的能力。如果需要,使用基于节点的容器;如果需要多元素插入(例如-以范围方式插入条款 5),使用list,它是唯一提供多元素插入事务性语义的标准容器。事务性语义对异常安全比较重要,连续内存容器实现事务性语义的性能开销比较大,具体实现可以参考《Effective C 》资源管理:条款25–考虑写出一个不抛出异常的swap函数和条款29:为“异常安全”而努力是值得的。
10、需要把迭代器、指针、引用失效的次数减到最少吗?如果是,最好使用基于几点的容器,因为在连续内存容器上插入删除,有时引起内存的动态重新分配,造成当前迭代器失效。
11、需要具有以下特性的序列容器吗:1)可以文集访问迭代器;2)只要不删除元素,且插入只在容器结尾,指针或引用的数据不会失效。这是特殊情况,但是如果遇到这种情况,deque是完美选择。

上面的讨论只是一部分,例如没有关注不同容器使用不同内存配置策略的影响(条款 10、14)。但是上面内存已经足够让你信服:元素顺序、标准一致性、迭代器能力、内存布局和C兼容、查找速度、引用计数器的行为、事务性语义、迭代器失效。

STL给了许多容器给我们选择,在选择一个容器前,要考虑上面的方方面面。

条款2:小心对“容器无关代码”的幻想

STL建立在泛化的基础上,例如数组泛化为容器,参数泛化为包含对象的类型;函数泛化为算法,参数泛化为所用迭代器的类型泛化为其所指向的对象类型。

容器可以泛化为序列容器和关联容器,且类似的容器拥有类似的功能。连续内存容器提供随机访问迭代器,基于节点的容器提供双向迭代器。序列容器支持push_back或push_front,但关联容器不支持;关联容器提供对数时间复杂度的lower_bound、upper_bound和equal_range成员函数,序列容器没有。

基于泛化的思想,我们可能想泛化容器,即写和容器无关的代码。例如,现在用的是一个vector,再不修改代码情况下可以用deque或list代替。这样的泛化可能会带来麻烦。

要写既适用于序列容器,又适用于关联容器的代码并没有什么意义。有些成员函数只存在某类特定容器,例如序列容器支持push_front、push_back,关联容器支持count、lower_bound。即使相同的成员函数,对于不同的容器,其意义也不同。例如insert函数,把对象插入到序列容器时,它保留你放置的位置;但插入到关联容器时,容器会按照规则把对象插入到合适位置。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都会使splice和成员函数方式的sort失败,这意味着不能再“泛化的容器”上调用任何一种sort。

违背上面的限制,代码可能在某个容器上发生编译错误。这是因为不同的序列容器对应不同的迭代器、指针和引用的失效规则。如果想要写vector、deque和list配合的代码,必须假设那些操作使容器的迭代器、指针或引用失效了。所以,必须假设每次调用insert都使迭代器失效了,因为deque::insert会使所有迭代器失效;因为缺少capacity,也要假设vector::insert也会使迭代器、指针和引用失效。同理,erase也要假设使所有东西失效。

还有一些限制。不能把容器的数据传递给C风格界面,因为只有vector支持(条款 16)。不能用bool保存的对象来实例化容器(条款 18),vector并非总是表现为一个vector,实际上它没有真正保存bool值。list插入删除时间复杂度O(1),但vector和deque为O(n)。

如果不是全部泛化,只是泛化一部分。例如放弃对list的支持,这时你仍然放弃了reserve、capacity、push_front、pop_front,仍然假设insert和erase有线性时间复杂度且会使迭代器、指针、引用失效,仍然不能存储bool。

如果泛化关联容器,上面的问题仍然存在。例如,同时支持set和map几乎不可能,set保存单个对象,map保存键值对。即使兼容set/multiset或map/multimap也很难,set/map的insert只返回一个值,multiset/multimap的insert返回类型不同,且要避免对一个保存在容器中的值得拷贝份数作出任何假设。对于map/multimap,避免使用operator[],因为它只存在map中。

实际上没必要泛化容器,不同的容器优缺点不同;它们没有被设计成相互替换的,我们也无法做什么包装工作。

当你决定使用某一容器时,这个容器可能不是最理想的,且你可能需要使用不同类型的容器。当改变容器时,不仅仅修改编译器诊断出的错误,还要检查迭代器、指针、引用是否失效。如果从vector切换到其他容器,要确认不再依赖C兼容的内存布局;如果切换到vector,要保证不用来存储bool。

如果有改变容器的需求,可以这样做:使用封装,封装,再封装。最简单的一个方法通过对容器和迭代器使用typedef。所以不要这样写:

class Widget {...};vector<Widget> vw;Widget bestWidget;...//bestWidget赋值vector<Widget>::iterator i=//找和bestWidget相等的    find(vw.begin(), vw.end(), bestWidget);

要这样写:

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

这样,改变容器只需修改很少代码。
typedef带来的封装可能对你没有意义,但是它可以节省许多工作,例如有如下容器,

map<string, vector<Widget>::iterator, CIStringCompare>;

要使用const_iterator来遍历这个map,那么可能多次需要使用:

map<string, vector<Widget>::iterator, CIStringCompare>::const_iterator

typedef是其他类型的同义词,它提供的封装是纯词法。如果要更深层次的封装,例如不像暴露出所使用的容器,可以使用class。
限制用一个容器替换另一个容器可能需要修改的代码,就需要在类中隐藏那个容器,通过接口限制容器的特殊信息可见性。例如要建一个用户列表,可以封装list:

class CustomerList{private:    typedef list<Customer> CustomerContainer;    typedef CustomerContainer::iterator CCIterator;    CustomerContainer customers;public:    ...//限制list特殊信息可见。

一开始这么做可能看不出有什么意义,因为CustomerList就是一个list。实际上并不是这样,例如我们发现插入和删除并不频繁,但我们需要频繁定位元素nth_element(条款 31)。nth_element需要随机访问迭代器,这时可以把list替换为vector或deque。

当决定把list改为vector或deque时,要检查CustomerList的每个成员函数和友元,看它们是否有影响。做好对CustomerList的封装,那么客户端可以写出和容器无关的代码。

0 0