有效使用STL的经验

摘自《effective stl》

关于STL的使用原则,以后还会再补充。

容器

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

当面对容器时,STL给了你很多选项。如果你的视线超越了STL的范围,那就会有更多的选项。在选择一个容器前,要保证考虑了所有你的选项。一个“默认容器”?我不这么认为。


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

STL是建立在泛化之上的。数组泛化为容器,参数化了所包含的对象的类型。函数泛化为算法,参数化了所用的迭代器的类型。指针泛化为迭代器,参数化了所指向的对象的类型。


条款3:使容器里对象的拷贝操作轻量而正确

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


条款4:用empty来代替检查size()是否为0

事实上empty的典型实现是一个返回size是否为0的肉联函数。你应该首选empty,对于所有的标准容器,empty是一个常数时间的操作,但对于一些list实现,size花费线性时间。


条款5:尽量使用区间成员函数代替它们的单元素兄弟

区间成员函数更容易写,它们更清楚地表达你的意图,而且它们提供了更高的性能。


条款6:警惕C++最令人恼怒的解析

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


条款7:当使用new得指针的容器时,记得在销毁容器前delete那些指针

指针没有析构函数,它不会调用delete。最好用智能指针替代。


条款8:永不建立auto_ptr的容器

这个不会编译通过的。


条款9:在删除选项中仔细选择(移除算法的选择)

与仅仅调用erase相比,有效地删除容器元素有更多的东西。解决问题的最好方法取决于你是怎样鉴别出哪个对象是要被去掉的,储存它们的容器的类型,和当你删除它们的时候你还想要做什么(如果有的话)。只要你小心而且注意了本条款的建议,你将毫不费力。如果你不小心,你将冒着产生不必要低效的代码或未定义行为的危险。


条款10:注意分配器的协定和约束

写你自己的分配器时你必须做的大部分事情是重现大量样板代码,然后修补一些成员函数,特别是allocate和deallocate。我建议你从Josuttis的样例allocator]或Austern的文章《What Are Allocators Good For?》的代码开始,而不是从头开始写样板。


条款11:理解自定义分配器的正确用法

你用了基准测试,性能剖析,而且实验了你的方法得到默认的STL内存管理器(即allocator <T>)在你的STL需求中太慢、浪费内存或造成过度的碎片的结论,并且你肯定你自己能做得比它好。或者你发现allocator<T>对线程安全采取了措拖,但是你只对单线程的程序感兴趣,你不想花费你不需要的同步开销。或者你知道在某些容器里的对象通常一同被使用,所以你想在一个特别的堆里把它们放得很近使引用的区域性最大化。或者你想建立一个相当共享内存的唯一的堆,然后把一个或多个容器放在那块内存里,因为这样它们可以被其他进程共享。 这些情况正好对应于一种适合于自定义分配器解决的方案。


条款12:对STL容器线程安全性的期待现实一些

多个读取者是安全的,多线程可能同时读取一个容器的内容,这将正确地执行。当然,在读取时不能有任何写入者操作这个容器。

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

vector和string

条款13:尽量使用vector和string来代替动态分配的数组

如果你在使用动态分配数组,你可能比需要的做更多的工作。要减轻你的负担,就使用vector或string来代替。


条款14:使用reserve来避免不必要的重新分配

通常有两情况使用reserve来避免不必要的重新分配。第一个可用的情况是当你确切或者大约知道有多少元素将最后出现在容器中。那样的话,就像上面的vector代码,你只是提前reserve适当数量的空间。第二种情况是保留你可能需要的最大的空间。


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

string实现的自由度比乍看之下多得多,也很显然,不同的实现以不同的方式从它们的设计灵活性中得到好处。让我们总结一下:

1.字符串值可能是或可能不是引用计数的。默认情况下,很多实现的确是用了引用计数,但它们通常提供了关闭的方法,一般是通过预处理器宏。条款13给了一个你可能要关闭的特殊环境的例子,但你也可能因为其他原因而要那么做。比如,引用计数只对频繁拷贝的字符串有帮助,而有些程序不经常拷贝字符串,所以没有那个开销。 

2.string对象的大小可能从1到至少7倍char*指针的大小。 

3.新字符串值的建立可能需要0、1或2次动态分配。 

4.string对象可能是或可能不共享字符串的大小和容量信息。 

5.string可能是或可能不支持每对象配置器。 

6.不同实现对于最小化字符缓冲区的配置器有不同策略。 


条款16: 如何将vector和string的数据传给遗留的API

简单来说,就是利用vector,此外,如果要将vector和string以外的STL容器如何将它们的数据传给C风格API。可以将容器的每个数据拷到vector,然后将它们传给API。


条款17:使用“交换技巧”来修整过剩容量

语言警察要求我告诉你并没有保证这个技术会真的消除多余的空间。如果vector和string想要的话,实现可以自由地给予它们过剩的空间,而且有时候它们想要。比如,它们可能必须有一个最小容量限制,或者它们可能强制vector或string的容量是2的整数次方。(在我的经历中,这样不规则的string实现比vector实现更常见。例子参见条款15。)这近似于“收缩到合适”,然而,并不是真的意味着“使容量尽可能小”,它意味着“使容量和这个实现可以尽量给容器的当前大小一样小”。但是,只要没有切换不同的STL实现,这是你能做的最好的方法。所以当你想对vector和string进行“收缩到合适”时,就考虑“交换技巧”。

另外,交换技巧的变体可以用于清除容器和减少它的容量到你的实现提供的最小值。你可以简单地和一个默认构造的临时vector或string做个交换。

条款18:避免使用vector<bool>

vector<bool>不满足STL容器的必要条件,你最好不要使用它;而deque<bool>和bitset是基本能满足你对vector<bool>提供的性能的需要的替代数据结构。

关联容器

条款19:了解相等和等价的区别

通过只使用一个比较函数并使用等价作为两个值“相等”的意义的仲裁者,标准关联容器避开了很多会由允许两个比较函数而引发的困难。一开始行为可能看起来有些奇怪(特别是当你发现成员和非成员find可能返回不同结果),但最后,它避免了会由在标准关联容器中混用相等和等价造成的混乱。


条款20:为指针的关联容器指定比较类型

无论何时你建立指针的关联容器,注意你也得指定容器的比较类型。大多数时候,你的比较类型只是解引用指针并比较所指向的对象。鉴于这种情况,你手头最好也能有一个用于那种比较的仿函数模板。


条款21: 永远让比较函数对相等的值返回false

除非你的比较函数总是为相等的值返回false,你将会打破所有的标准关联型容器,不管它们是否允许存储复本。


条款22:避免原地修改set和multiset的键

正如所有标准关联容器,set和multiset保持它们的元素有序,这些容器的正确行为依赖于它们保持有序。 如果你改了关联容器里的一个元素的值(例如,把10变为1000),新值可能不在正确的位置,而且那将破坏容器的有序性。


条款23:考虑用有序vector代替关联容器

当需要一个提供快速查找的数据结构时,很多STL程序员立刻会想到标准关联容器:set、multiset、map和multimap。直到现在这很好,但不是永远都好。如果查找速度真得很重要,的确也值得考虑使用非标准的散列容器(参见条款25)。如果使用了合适的散列函数,则可以认为散列容器提供了常数时间的查找。(如果选择了不好的散列函数或表的太小,散列表的查找性能可能明显下降,但在实际中这相对少见。)对于多数应用,被认为是常数时间查找的散列容器要好于保证了对数时间查找的set、map和它们的multi同事。


条款24:当关乎效率时应该在map::operator[]和map-insert之间仔细选择

如果你要更新已存在的map元素,operator[]更好,但如果你要增加一个新元素,insert则有优势。


条款25:熟悉非标准散列容器

哪个设计最有利于你和你的程序?我不可能知道。只有你能确定,而且本条款并不试图给你足以得出一个合理结论的信息。取而代之的是,本条款的目标是让你知道虽然STL本身缺乏散列容器,兼容STL的散列容器(有不同的接口、能力和行为权衡)不难得到。

迭代器

条款26:尽量用iterator代替const_iterator,reverse_iterator和const_reverse_iterator
从常量正确性的角度来看(一个固然有价值的角度),仅仅为了避免一些潜在的STL实现的弊端(而且,这些弊端都有变通办法)而抛弃const_iterator显得有欠公允。但综合考虑到iterator与一些容器类成员函数的粘连关系,从实践得出const_iterator没有iterator好用的结论是很难避免的。更何况,有时并不值得卷入const_iterator的麻烦中去。

条款27:用distance和advance把const_iterator转化成iterator
我们现在知道了怎么通过advance和distance获取const_iterator相应的iterator了。但另一个我们现在一直避开却很值的考虑的实际问题是:这个技巧的效率如何?答案很简单。取决于你所转换的究竟是什么样的迭代器。对于随机访问的迭代器(比如vector、string和deque的)而言,这是常数时间的操作。对于双向迭代器(也就是,所有其它容器和包括散列容器的一些实现[4](参见条款25))而言,这是线性时间的操作。
因为它可能花费线性时间的代价来产生一个和const_iterator等价的iterator,并且因为如果不能访问const_iterator所属的容器这个操作就无法完成。从这个角度出发,也许你需要重新审视你从const_iterator产生iterator的设计。事实上那样的考虑帮助激发了条款26,它建议你当处理容器时尽量用iterator代替const和reverse迭代器。

条款28:了解如何通过reverse_iterator的base得到iterator
现在已经很清楚了,reverse_iterator的base成员函数返回一个“对应的”iterator的说法并不准确。对于插入操作而言,的确如此;但是对于删除操作,并非如此。当需要把reverse_iterator转换成iterator的时候,有一点非常重要的是你必须知道你准备怎么处理返回的iterator,因为只有这样你才能决定你得到的iterator是否是你需要的。

条款29:需要一个一个字符输入时考虑使用istreambuf_iterator

当你了解它之后,你也应该考虑把ostreambuf_iterator用于相应的无格式一个一个字符输出的作。它们没有了ostream_iterator的开销(和灵活性),所以它们通常也做得更好。

算法

条款30:确保目标区间足够大
无论何时你使用一个要求指定目的区间的算法,确保目的区间已经足够大或者在算法执行时可以增加大小。如果你选择增加大小,就使用插入迭代器,比如ostream_iterators或从back_inserter、front_inserter或inserter返回的迭代器。

条款31:了解你的排序选择
我对于在这些排序算法之间作选择的建议是让你的选择基于你需要完成的任务上,而不是考虑性能。如果你选择的算法只完成了你需要的(比如用partition代替完全排序),你能得到的不仅是可以最清楚地表达了你要做的代码,而且是使用STL最高效的方法来完成它。

条款32:如果你真的想删除东西的话就在类似remove的算法后接上erase
一旦你知道了remove不能“真的”从一个容器中删除东西,和erase联合使用就变成理所当然了。你要记住的唯一其他的东西是remove不是唯一这种情况的算法。另外有两种“类似remove”的算法:remove_if和unique。
remove和remove_if之间的相似性很直截了当。所以我不会细讲,但unique行为也像remove。它用来从一个区间删除东西(邻近的重复值)而不用访问持有区间元素的容器。结果,如果你真的要从容器中删除元素,你也必须成对调用unique和erase,unique在list中也类似于remove。正像list::remove真的删除东西(而且比erase-remove惯用法高效得多)。list::unique也真的删除邻近的重复值(也比erase-unique高效)。

条款33:提防在指针的容器上使用类似remove的算法
不管你怎么选择处理动态分配指针的容器,通过引用计数智能指针、在调用类似remove的算法前手动删除和废弃指针或者一些你自己发明的技术,本条款的指导意义依然一样:提防在指针的容器上使用类似remove的算法。没有注意这个建议的人只能造成资源泄漏。

条款34:注意哪个算法需要有序区间
不是所有算法可以用于任意区间。11个需要有序区间的算法为了比其他可能性提供更好的性能而这么做。只要你记住只传给它们有序区间,只要你保证用于算法的比较函数和用于排序的一致,你就会酷爱没有麻烦的查找、设置和合并操作,加上你会发现unique和unique_copy除去了所有的重复值,正如你要它们完成的一样。

条款35:通过mismatch或lexicographical比较实现简单的忽略大小写字符串比较
有的人可能称此为技巧(hack),但stricmp/strcmpi被优化为只做一件事情,对长字符串运行起来一般比通用的算法mismatch和lexicographical_compare快得多。如果那对你很重要,你可能不在乎你用非标准C函数完成标准STL算法。有时候最有效地使用STL的方法是认识到其他方法更好。

条款36:了解copy_if的正确实现
告诉你copy_if多有用,加上新STL程序员趋向于希望无论如何它应该存在的事实,所以好的做法是把copy_if——正确的那个!——放在你局部的STL相关工具库中,而且只要合适就使用。

条款37:用accumulate或for_each来统计区间
你可能想知道为什么for_each的函数参数允许有副作用,而accumulate不允许。这是一个刺向STL心脏的探针问题。唉,尊敬的读者,有一些秘密总是在我们的知识范围之外。为什么accumulate和for_each之间有差别?我尚待听到一个令人信服的解释。

仿函数、仿函数类、函数等

条款38:把仿函数类设计为用于值传递
从STL的视角看来,要记住的最重要的东西是使用这种技术的仿函数类必须支持合理方式的拷贝。如果你是上面BPFC的作者,你就必须保证它的拷贝构造函数对指向的BPFCImpl对象做了合理的事情。也许最简单的合理的东西是引用计数,使用类似Boost的shared_ptr,你可以在条款50中了解它. 
实际上,对于本条款的目的,唯一你必须担心的是BPFC的拷贝构造函数的行为,因为当在STL中被传递或从一个函数返回时,函数对象总是被拷贝——值传递,记得吗?那意味着两件事。让它们小,而且让它们单态。

条款39:用纯函数做判断式

判断式是返回bool(或者其他可以隐式转化为bool的东西)。判断式在STL中广泛使用。标准关联容器的比较函数是判断式,判断式函数常常作为参数传递给算法,比如find_if和多种排序算法。纯函数是返回值只依赖于参数的函数。如果f是一个纯函数,x和y是对象,f(x, y)的返回值仅当x或y的值改变的时候才会改变。

因为这是避免我们刚测试过的问题的一个直截了当的方法,我几乎可以把本条款的题目改为“在判断式类中使operator()成为const”。但那走得不够远。甚至const成员函数可以访问multable数据成员、非const局部静态对象、非const类静态对象、名字空间域的非const对象和非const全局对象。一个设计良好的判断式类也保证它的operator()函数独立于任何那类对象。在判断式类中把operator()声明为const对于正确的行为来说是必要的,但不够充分。一个行为良好的operator()当然是const,但不只如此。它也得是一个纯函数。

条款40:使仿函数类可适配

STL函数对象模仿了C++函数,而一个C++函数只有一套参数类型和一个返回类型。结果,STL暗中假设每个仿函数类只有一个operator()函数,而且这个函数的参数和返回类型要被传给unary_function或binary_function(与我们刚讨论过的引用和指针类型的规则一致)。这意味着,虽然可能很诱人,但你不能通过建立一个单独的含有两个operator()函数的struct试图组合WidgetNameCompare和PtrWidgetNameCompare的功能。如果你那么做了,这个仿函数可能可以和最多一种它的调用形式(你传参数给binary_function的那个)适配,而一个只能一半适配的仿函数可能只比完全不能适配要好。


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

现在只留下成员函数适配器的名字,而在这里,最后,我们有一个真实的STL历史产物。当对这些种适配器的需求开始变得明显时,建立STL的人们正专注于指针的容器。(这种容器的缺点在条款7、20和33描述,这看起来可能很惊人,但是记住指针的容器支持多态,而对象的容器不支持。)他们需要用于成员函数的适配器,所以他们选择了mem_fun。但很快他们意识到需要一个用于对象的容器的另一个适配器,所以他们使用了mem_fun_ref。是的,它非常不优雅,但是这些事情发生了。


条款42:确定less<T>表示operator<

不要通过把less的定义当儿戏来误导那些程序员。如果你使用less(明确或者隐含),保证它表示operator<。如果你想要使用一些其他标准排序对象,建立一个特殊的不叫做less的仿函数类。它真的很简单。

使用STL编程

条款43:尽量用算法调用代替手写循环
在算法调用与手写循环正在进行的较量中,关于代码清晰度的底线是:这完全取决于你想在循环里做的是什么。如果你要做的是算法已经提供了的,或者非常接近于它提供的,调用泛型算法更清晰。如果循环里要做的事非常简单,但调用算法时却需要使用绑定和适配器或者需要独立的仿函数类,你恐怕还是写循环比较好。最后,如果你在循环里做的事相当长或相当复杂,天平再次倾向于算法。因为长的、复杂的通常总应该封装入独立的函数。只要将循环体一封装入独立函数,你几乎总能找到方法将这个函数传给一个算法(通常是for_each),以使得最终代码直截了当。
如果你同意算法调用通常优于手写循环这个条款,并且如果你也同意条款5的区间成员函数优于循环调用单元素的成员函数,一个有趣的结论出现了:使用STL容器的C++精致程序中的循环比不使用STL的等价程序少多了。这是好事。只要能用高层次的术语——如insert、find和for_each,取代了低层次的词汇——如for、while和do,我们就提升了软件的抽象层次,并因此使得它更容易实现、文档化、增强和维护。

条款44:尽量用成员函数代替同名的算法

有些容器拥有和STL算法同名的成员函数。关联容器提供了count、find、lower_bound、upper_bound和equal_range,而list提供了remove、remove_if、unique、sort、merge和reverse。大多数情况下,你应该用成员函数代替算法。这样做有两个理由。首先,成员函数更快。其次,比起算法来,它们与容器结合得更好(尤其是关联容器)。那是因为同名的算法和成员函数通常并不是是一样的。

条款45:注意count、find、binary_search、lower_bound、upper_bound和equal_range的区别

在count、find、binary_search、lower_bound、upper_bound和equal_range中做出选择很简单。当你调用时,选择算法还是成员函数可以给你需要的行为和性能,而且是最少的工作。按照这个建议做(或参考那个表格),你就不会再有困惑。


条款46:考虑使用函数对象代替函数作算法的参数

把函数对象作为算法的参数所带来的不仅是巨大的效率提升。在让你的代码可以编译方面,它们也更稳健。当然,真函数很有用,但是当涉及有效的STL编程时,函数对象经常更有用。


条款47:避免产生只写代码

代码的读比写更经常,这是软件工程的真理。也就是说软件的维护比开发花费多得多的时间。不能读和理解的软件不能被维护,不能维护的软件几乎没有不值得拥有。你用STL越多,你会感到它越来越舒适,而且你会越来越多的使用嵌套函数调用和即时(on the fly)建立函数对象。


条款48:总是#include适当的头文件

无论何时你使用了一个头文件中的任意组件,就要确定提供了相应的#include指示,就算你的开发平台允许你不用它也能通过编译。当你发现移植到一个不同的平台时这么做可以减少压力,你的勤奋将因而得到回报。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值