这是《Effective STL》笔记最后一期,不能涵盖全部内容,书后仍然有些附加内容,不在附加,有兴趣可以找原书来读读,一则是区域设置后的忽略大小写比较,另一则是MSVC4-5编译器下STL注意事项
条款41:了解使用ptr_fun、mem_fun和mem_fun_ref的原因
函数和函数对象总使用用于非成员函数的语法形式调用。mem_fun带有一个到成员函数的指针,pmf,并返回一个mem_fun_t类型的对象。这是一个仿函数类,容纳成员函数指针并提供一个operator(),它调用指向在传给operator()的对象上的成员函数。mem_fun_t这样的类被称为函数对象适配器。类似的mem_fun_ref可以完成成员函数的调用。无论何时都可以使用ptr_fun完成,这两者的调用,但是会使得代码语义不明。
条款42:确定less<T>表示operator<
试图修改std里的组件确实是禁止的(而且这么做通常被认为是行为未定义的范畴),但是在一些情况下,修补是允许的。具体来说,程序员被允许用自定义类型特化std内的模板。特化std模板的选择几乎总是更为优先,但很少发生,这确实合理的。例如,智能指针类的作者经常想让他们的类在排序的时候行为表现得像内建指针,因此用于智能指针类型的std::less特化并不罕见。当然这是允许的,因为这个less的特化仅仅在排序上保证智能指针的行为与它们的内建兄弟相同。
operator<不仅是实现less的默认方式,它还是程序员希望less做的。让less做除operator<以外的事情是对程序员预期的无故破坏。如果你使用less(明确或者隐含),保证它表示operator<。如果你想要使用一些其他标准排序对象,建立一个特殊的不叫做less的仿函数类。
条款43:尽量用算法调用代替手写循环
每个算法接受至少一对用来指示将被操作的对象区间的迭代器。当算法被执行时,它们必须检查指示给它的区间中的每个元素,并且是按你所期望的方式进行的:从区间的起始点循还到结束点。所以,算法内部是一个循环。此外,STL算法的广泛涉及面意味着很多你本来要用循环来实现的任务,现在可以改用算法来实现了。
- 效率:算法通常比程序员产生的循环更高效。每次循环都要和出循环条件作比较,而算法for_each只需要比较一次;库的实现者可以利用他们知道容器的具体实现的优势,用库的使用者无法采用的方式来优化遍历;除了最微不足道的STL算法,所有的STL算法使用的计算机科学都比一般的C++程序员能拿得出来的算法复杂且高效。
- 正确性:写循环时比调用算法更容易产生错误。难以正确实现循环的情况太多了,因为在使用迭代器前,必须时刻关注它们是否被不正确地操纵或变得无效。
- 可维护性:算法通常使代码比相应的显式循环更干净、更直观。当你看见for、while或do的时候,你能知道的只是这是一种循环。如果要获得哪怕一点关于这个循环作了什么的信息,你就得审视它。算法则不用。一旦你看见调用一个算法,独一无二的名字勾勒出了它所作所为的轮廓。当然要了解它真正做了什么,你必须检查传给算法的实参,但这一般比去研究一个普通的循环结构要轻松得多。
在算法调用与手写循环正在进行的较量中,关于代码清晰度的底线是:这完全取决于你想在循环里做的是什么。如果你要做的是算法已经提供了的,或者非常接近于它提供的,调用泛型算法更清晰。如果循环里要做的事非常简单,但调用算法时却需要使用绑定和适配器或者需要独立的仿函数类,你恐怕还是写循环比较好。最后,如果你在循环里做的事相当长或相当复杂,天平再次倾向于算法。因为长的、复杂的通常总应该封装入独立的函数。只要将循环体一封装入独立函数,你几乎总能找到方法将这个函数传给一个算法(通常是for_each),以使得最终代码直截了当。
条款44:尽量用成员函数代替同名的算法
有些容器拥有和STL算法同名的成员函数。大多数情况下,你应该用成员函数代替算法。这样做有两个理由。首先,成员函数更快。其次,比起算法来,它们与容器结合得更好(尤其是关联容器)。那是因为同名的算法和成员函数通常并不是是一样的。
对于标准的关联容器,选择成员函数而不是同名的算法有几个好处。首先,你得到的是对数时间而不是线性时间的性能。其次,你判断两个元素“相同”使用的是等价,这是关联容器的默认定义。第三,当操纵map和multimap时,你可以自动地只处理key值而不是(key, value)对。
list成员函数的行为和它们的算法兄弟的行为经常不相同。如果你真的想从容器中清除对象的话,调用remove、remove_if和unique算法后,必须紧接着调用erase函数;但list的remove、remove_if和unique成员函数真的去掉了元素,后面不需要接着调用erase。在sort算法和list的sort成员函数间的一个重要区别是前者不能用于list。作为单纯的双向迭代器,list的迭代器不能传给sort算法。merge算法和list的merge成员函数之间也同样存在巨大差异。这个算法被限制为不能修改源范围,但list::merge总是修改它的宿主list。
条款45:注意count、find、binary_search、lower_bound、upper_bound和equal_range的区别
你想知道的 | 使用的算法 | 使用的成员函数 | ||
无序区间 | 有序区间 | set/map | multiset/map | |
期望值是否存在 | find | binary_search | count | find |
期望值是否存在? 如果有,第一个等 于这个值的对象在 哪里? |
find | equal_range | find |
lower_bound find |
第一个不在期望值 之前的对象在哪 里? |
find_if | lower_bound | lower_bound | lower_bound |
第一个在期望值之 后的对象在哪里? |
find_if | upper_bound | upper_bound | upper_bound |
有多少对象等于期 望值? |
count |
equal_range 然后distance |
count | count |
等于期望值的所有 |
find(迭代) | equal_range | equal_range | equal_range |
条款46:考虑使用函数对象代替函数作算法的参数
一个关于用高级语言编程的抱怨是抽象层次越高,产生的代码效率就越低。操作包含一个double的类产生的代码效率比对应的直接操作一个double的代码低。因此你可能会奇怪地发现把STL函数对象——化装成函数的对象——传递给算法所产生的代码一般比传递真的函数高效。这个行为的解释很简单:内联。如果一个函数对象的operator()函数被声明为内联(不管显式地通过inline或者隐式地通过定义在它的类定义中),编译器就可以获得那个函数的函数体,而且大部分编译器喜欢在调用算法的模板实例化时内联那个函数。在上面的例子中,greater<double>::operator()是一个内联函数,所以编译器在实例化sort时内联展开它。结果,sort没有包含一次函数调用,而且编译器可以对这个没有调用操作的代码进行其他情况下不经常进行的优化。
还有另一个使用函数对象代替函数作为算法参数的理由,STL平台经常完全拒绝有效代码,即使编译器或库或两者都没问题。解决方式就是创建仿函数类,仿函数类的创造不仅避开了编译器一致性问题,而且可能会带来性能提升。
另一个用函数对象代替函数的原因是它们可以帮助你避免细微的语言陷阱。有时候,看起来合理代码被编译器由于合法性的原因——但很模糊——而拒绝。
条款47:避免产生只写代码
当你写代码时,它似乎很直截了当,因为它是一些基本想法(也就是,erase-remove惯用法加上使用逆向迭代器调用find的想法)的自然产物。但是,读者们很难把最后的产物分解回它基于的想法。这就被称为只写代码:很容易写,但很难读和理解。代码的读比写更经常,这是软件工程的真理。也就是说软件的维护比开发花费多得多的时间。不能读和理解的软件不能被维护,不能维护的软件几乎没有不值得拥有。你用STL越多,你会感到它越来越舒适,而且你会越来越多的使用嵌套函数调用和即时(on the fly)建立函数对象。
条款48:总是#include适当的头文件
- 几乎所有的容器都在同名的头文件里,比如,vector在<vector>中声明,list在<list>中声明等。例外的是<set>和<map>。<set>声明了set和multiset,<map>声明了map和multimap。
- 除了四个算法外,所有的算法都在<algorithm>中声明。例外的是accumulate、inner_product、adjacent_difference和partial_sum。这些算法在<numeric>中声明。
- 特殊的迭代器,包括istream_iterators和istreambuf_iterators,在<iterator>中声明。
- 标准仿函数(比如less<T>)和仿函数适配器(比如not1、bind2nd)在<functional>中声明。
条款49:学习破解有关STL的编译器诊断信息
举个例子:
1 string s(10); // 常识建立一个大小为10的string
这段代码是不能通过编译的,提示信息如下:
error C2664:
‘__thiscall std::basic_string<char, structstd::char_traits<char>,class std::allocator<char> >::std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > (const class std::allocator<char> &)‘: cannot convert parameter 1 from ‘const int‘ to ‘constclass std::allocator<char> &‘
Reason: cannot convert from ‘const int‘ to ‘constclass std::allocator<char>
No constructor could take the source type, or constructor overload resolution was ambiguous
string不是一个类,它是typedef。实际上,它是这个的typedef:basic_string<char, char_traits<char>, allocator<char> >。用文字“string”全局替换冗繁难解的basic_string:
error C2664:
‘__thiscall string::string(const classstd::allocator<char> &)‘: cannot convert parameter 1 from ‘const int‘ to const class std::allocator<char> &‘
就可以比较容易的看出是没有相应的参数转换,而实际上string没有传int参数的构造函数。
- 对于vector和string,迭代器有时是指针,所以如果你用迭代器犯了错误,编译器诊断信息可能会提及涉及指针类型。
- 提到back_insert_iterator、front_insert_iterator或insert_iterator的消息经常意味着你错误调用了back_inserter、front_inserter或inserter,一一对应。
- 类似地,如果你得到的一条消息提及binder1st或binder2nd,你或许错误地使用了bind1st或bind2nd。(c++11很少使用这一条,多使用bind)
- 输出迭代器(例如ostream_iterator、ostreambuf_iterators,和从back_inserter、front_inserter和inserter返回的迭代器)在赋值操作符内部做输出或插入工作,所以如果你错误使用了这些迭代器类型之一,你很可能得到一条消息,抱怨在你从未听说过的一个赋值操作符里的某个东西。
- 你得到一条源于STL算法实现内部的错误信息(即,源代码引发的错误在<algorithm>中),也许是你试图给那算法用的类型出错了。
- 你使用常见的STL组件比如vector、string或for_each算法,而编译器说不知道你在说什么,你也许没有#include一个需要的头文件
条款50:让你自己熟悉有关STL的网站
- SGI STL网站,http://www.sgi.com/tech/stl/。
- STLport网站,http://www.stlport.org/
- Boost网站,http://www.boost.org/。(译注:如果访问不了,可以试试http://boost.sourceforge.net/)这个很有用,有很多需要学习的,比如regex、graph、math、gil。
作者还推了一堆书,我觉得要看完估计很久,如果真的需要的话,建议去原著上面找找,这里就不发了。
《Effective STL》到此结束。