⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 item_43.html

📁 制作本书的目的是为了方便大家的阅读。转载时请保持本电子书的完整性。 前言、条款2、16、21、44根据从Addison-Wesley出版社下载的开放条款翻译。条款26、27、28、45根据从Sc
💻 HTML
📖 第 1 页 / 共 2 页
字号:
<p>看起来象双赢,它不只是递增了指示插入位置的,还避免了循环每次对begin的调用;这就像我们前面讨论过的一样,消除了影响效率的次要因素。唉,这种方法陷入了另外一个的问题中:它导致了未定义的结果。每次调用deque::insert,都将导致所有指向deque内部的迭代器无效,包括上面的insertLocation。在第一次调用insert后,insertLocation就无效了,后面的循环迭代器可以直接让人发疯。</p> <p>注意到这个问题后(可能得到了STLport调试模式的帮助,在<a href="item_50.html">条款50</a>描述),你可能会这样做:</p> <pre>deque&lt;double&gt;::iterator insertLocation =	d.begin();						// 同上for (size_t i = 0; i &lt; numDoubles; ++i) {			// 每次调用insert后	insertLocation =					// 都更新insertLocation		d.insert(insertLocation, data[i] + 41);		// 以保证迭代器有效	++insertLocation;					// 然后递增它}</pre> <p>这样的代码确实完成了你相要的功能,但回想一下费了多大劲才达到这一步!和调用算法transform对比一下:</p> <pre>// 把所有元素从data拷贝到d的前端// 每个增加上41transform(data, data + numDoubles,          inserter(d, d.begin()),          bind2nd(plus&lt;double&gt;(), 41));</pre> <p>这个“bind2nd(plus&lt;double&gt;(), 41)”可能会花上一些时间才能看明白(尤其是如果不常用STL的bind族的话),但是与迭代器相关的唯一烦扰就是指出源区间的起始点和结束点(而这从不会成为问题),并确保在目的区间的起始点上使用inserter(参见<a href="item_30.html">条款30</a>)。实际上,为源区间和目的区间指出正确的初始迭代器通常都很容易,至少比确保循环体没有于无意中将需要持续使用的迭代器变得无效要容易得多。</p> <p>难以正确实现循环的情况太多了,这个例子只是比较有代表性。因为在使用迭代器前,必须时刻关注它们是否被不正确地操纵或变得无效。要看忽略迭代器失效会导致的麻烦的另一个例子,转到<a href="item_09.html">条款9</a>,那里描述了手写循环和调用erase只见的微妙之处。假设使用无效的迭代器会导致未定义的行为,又假设未定义的行为在开发和测试期间会有表现出令人讨厌的行为,为什么要冒不必要的危险?将迭代器扔给算法,让它们去考虑操纵迭代器时的各种诡异行为吧。</p> <p>我已经解释了算法为什么可以比手写的循环更高效,也描述了为什么循环将艰难地穿行于与迭代器相关的荆棘丛中,而算法正避免了这一点。运气好的话,你现在已是一个算法的信徒了。然而运气是变化无常的,在我休息前,我想更确定些。因此,让我们继续行进到代码清晰性的议题。毕竟,最好软件应该是那些最清晰的、最容易懂的、能容易增强、维护和适用于新的环境的软件。虽然循环比较熟悉,但算法在这个长期的竞争中具有优势。</p> <p>关键在于已知词汇的力量。在STL中约有70个算法的名字——总共超过100个不同的函数模板,每个重载都算一个。每个算法都完成一些精心定义的任务,而且有理由认为专业的C++程序员知道(或应该去看一下)每个算法都完成了什么。因此,当程序员调用transform时,他们认为对区间内的每个元素都施加了某个函数,而结果将被写到另外一个地方。当程序员调用replace_if时,他(她)知道区间内满足判定条件的对象都将被修改。当调用partition时,她(他)明白区间中所有满足判定条件的对象将被聚集在一起(参见<a href="item_31.html">条款31</a>)。STL算法的名字传达了大量的语义信息,这使得它们比随机的循环清晰多了。</p><p>当你看见for、while或do的时候,你能知道的只是这是一种循环。如果要获得哪怕一点关于这个循环作了什么的信息,你就得审视它。算法则不用。一旦你看见调用一个算法,独一无二的名字勾勒出了它所作所为的轮廓。当然要了解它真正做了什么,你必须检查传给算法的实参,但这一般比去研究一个普通的循环结构要轻松得多。</p><p>明摆着,算法的名字暗示了其功能。“for”、“while”和“do”却做不到这一点。事实上,这一点对标准C语言或C++语言运行库的所有组件都成立。毫无疑问地,你能自己实现strlen, memset或bsearch,但你不会这么做。为什么不会?因为(1)已经有人帮你实现了它们,因此没必要你自己再做一遍;(2)名字是标准的,因此,每个人都知道它们做什么用的;和(3)你猜测程序库的实现者知道一些你不知道的关于效率方面的技巧,因此你不愿意错过熟练的程序库实现者可能能提供的优化。正如你不会去写strlen等函数的自己的版本,同样没道理用循环来实现出已存在的STL算法的等价版本。</p><p>我很希望故事就此结束,因为我认为这个收尾很有说服力的。唉,有句老话叫好事多磨。算法的名字比光溜溜的循环有意义多了,这是事实,但使用循环更能让人明白加诸于迭代器上的操作。举例来说,假设想要找出vector中第一个比x大又比y小的元素。这是使用循环的实现:</p> <pre>vector&lt;int&gt; v;int x, y;...vector&lt;int&gt;::iterator i = v.begin();	 // 从v.begin()开始迭代,直到for( ; i != v.end(); ++i) {			// 找到了适当的值或  if (*i &gt; x &amp;&amp; *i &lt; y) break;	// 到v.end()了}...						// i现在指向那个值或等于v.end()</pre> <p>将同样的逻辑传给find_if是可能的,但是需要使用一个非标准函数对象适配器,比如SGI的compose2<sup><a id="Note1Back" href="#Note1">[1]</a></sup>(参见<a href="item_50.html">条款50</a>):</p> <pre>vector&lt;int&gt;::iterator i =  find_if(v.begin(), v.end(),			// 查找第一个find the first value val       compose2(logical_and&lt;bool&gt;(),	// 使val &gt; x           bind2nd(greater&lt;int&gt;(), x),	// 和val &lt; y都           bind2nd(less&lt;int&gt;(), y)));	// 为真的值val</pre> <p>即使没使用非标准组件,许多程序员也会反对说它远不及循环清晰,我也不得不同意这个观点(参见<a href="item_47.html">条款47</a>)。</p> <p>find_if的调用可以不显得那么复杂,只要将测试的逻辑封装入一个独立的仿函数类中:</p> <pre>template&lt;typename T&gt;class BetweenValues: public unary_function&lt;T, bool&gt; {		// 参见<a href="item_40.html">条款40</a> public: 	BetweenValues(const T&amp; lowValue, 		const T&amp; highValue)			// 使构造函数保存上下界		:lowVal(lowValue), highVal(highValue)	{} 	bool operator()(const T&amp; val) const	//返回val是否在保存的值之间 	{		return val &gt; lowVal &amp;&amp; val &lt; highVal;	}private: 	T lowVal; 	T highVal; }; ...vector&lt;int&gt;::iterator i = find_if(v.begin(), v.end(), 				BetweenValues&lt;int&gt;(x, y));</pre> <p>但这种方法有它自己的缺陷。首先,创建BetweenValues模板比写循环体要多出很多工作。就光数一下行数。循环体:1行;BetweenValues模板:24行。太不成比例了。第二,find_if正在找寻是什么的细节被从调用上完全割裂出去了,要想真的明白对find_if的这个调用,还必须得查看BetweenValues的定义,但BetweenValues一定被定义在调用find_if的函数之外。如果试图将BetweenValues声明在这个函数的调用内部,就像这样,</p> <pre>{ // 函数开始	...	template &lt;typename T&gt;	class BetweenValues:		public std::unary_function&lt;T, bool&gt; { ... };	vector&lt;int&gt;::iterator i =		find_if(v.begin(), v.end(),			BetweenValues&lt;int&gt;(x, y));	...} // 函数结束</pre> <p>你会发现这不能编译,因为模板不能声明在函数内部。如果试图把BetweenValues做成一个类而不是模板来避开这个限制:</p> <pre>{ // 函数开始  ...  class BetweenValues:    public std::unary_function&lt;int, bool&gt; { ... };  vector&lt;int&gt; iterator i =    find_if(v.begin(), v.end(),            BetweenValues(x, y));  ...} // 函数结束</pre> <p>你会发现仍然运气不佳,因为定义在函数内部的类是个局部类,而局部类不能绑定在模板的类型实参上(比如find_if所需要的仿函数类型)。很失望吧,仿函数类和仿函数类模板不能被定义在函数内部,不管它实现起来有多方便。</p><p>在算法调用与手写循环正在进行的较量中,关于代码清晰度的底线是:这完全取决于你想在循环里做的是什么。如果你要做的是算法已经提供了的,或者非常接近于它提供的,调用泛型算法更清晰。如果循环里要做的事非常简单,但调用算法时却需要使用绑定和适配器或者需要独立的仿函数类,你恐怕还是写循环比较好。最后,如果你在循环里做的事相当长或相当复杂,天平再次倾向于算法。因为长的、复杂的通常总应该封装入独立的函数。只要将循环体一封装入独立函数,你几乎总能找到方法将这个函数传给一个算法(通常是for_each),以使得最终代码直截了当。</p><p>如果你同意算法调用通常优于手写循环这个条款,并且如果你也同意<a href="item_05.html">条款5</a>的区间成员函数优于循环调用单元素的成员函数<sup></sup>,一个有趣的结论出现了:使用STL容器的C++精致程序中的循环比不使用STL的等价程序少多了。这是好事。只要能用高层次的术语——如insert、find和for_each,取代了低层次的词汇——如for、while和do,我们就提升了软件的抽象层次,并因此使得它更容易实现、文档化、增强和维护。</p> <hr /><p><sup><a id="Note1" href="#Note1Back">[1]</a></sup>要了解更多关于compose2的信息,请参考SGI的STL网站<a href="bibliography.html#bi21">[21]</a>或Matt Austern的书《Generic Programming and the STL》(Addison-Wesley,1999)<a href="bibliography.html#bi04">[4]</a> </p></body></html>

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -