条款50:让你自己熟悉有关STL的网站
因特网充满了STL的信息。用你最喜欢的搜索引擎寻找“STL”,它一定会返回几百个链接,其中有一些可能实际上是相关的。不过,对于大多数STL程序员,没有必要搜寻。下列网站应该要提升到几乎每个人的最常使用列表的顶端:
SGI STL网站,http://www.sgi.com/tech/stl/。
STLport网站,http://www.stlport.org/。
Boost网站,http://www.boost.org/。(译注:如果访问不了,可以试试http://boost.sourceforge.net/)
下面是为什么这些网站值得收藏的简要描述。
SGI STL网站
SGI的STL网站名列前茅,而且有很好的理由。它对STL的每个组件提供了全面的文档。对于很多程序员,不管他们使用的是哪个STL 平台,这个网站都是他们的在线参考手册。(参考文档由Matt Austern编制,后来把它扩充并精炼为《Generic Programming and the STL》[4]。)这里的材料不仅包括了STL的组件本身。例如,《Effective STL》里关于容器线程安全的讨论(参见条款12)就是基于SGI STL网站上的一个主题。
SGI网站为STL程序员提供了其它东西:一个可以自由下载的STL实现。这个实现只被移植到少数编译器,但广泛移植的STLport分发也是基于SGI分发的,我马上要写更多关于STLport的东西。此外,STL的SGI实现提供了可以让STL编程更强大、更灵活而且更有趣的许多非标准组件。其中最著名的是这些:
散列关联容器hash_set、hash_multiset、hash_map和hash_multimap。关于这些容器的更多信息,转向条款25。
单链表容器,slist。正如你所想的那样实现,而且迭代器指向你期望它们指向的列表节点。不幸的是,这使实现insert和erase成员函数变得昂贵,因为两者都要求调整迭代器指向的节点前一个节点的next指针。在双向链表里(比如标准list容器),这不是问题,但在单链表里,“后退”一个节点是线性时间的操作。对于SGI的slist,insert和erase花费线性而不是常数时间,这是一个相当大的缺点。SGI通过非标准(但常数时间)成员函数insert_after和erase_after解决这个问题。SGI注解到,
如果你发现insert_after和erase_after不能满足你的需要,而且你经常需要在列表中间使用insert和erase,你或许应该使用list来代替slist。
Dinkumware也提供了一个叫做slist的单链表容器,但是它使用了一个保持线性时间性能的insert和erase的不同迭代器实现。关于Dinkumware的更多信息,参考附录B。
用于超长字符串的类似字符串的容器。这个容器叫做rope,因为绳子(rope)是重型的线(string),看见了吗?SGI以这种方式描述rope:
rope是可伸缩的string实现:它们被设计为用于把string看作一个整体的高效操作。比如赋值、串联和子串的操作所花的时间差不多不依赖字符串的长度。与C的字符串不同,rope是超长字符串的一个合理的表现,比如编辑缓冲区或邮件信息。
在后端,rope被实现为引用计数子串的树,而且每个子串都存储为字符数组。rope接口的一个有趣方面是begin和end成员函数总是返回const_iterator。这是为了阻止客户进行改变单个字符的操作。这样的操作是昂贵的,rope针对涉及整个字符串的动作(如上所述,例如,赋值、串联和获取子串)优化;单个字符操作表现很差。
多个非标准函数对象和适配器。原先的HP STL实现比它成为标准C++后包含更多的函数对象。有两个是被很多老STL黑客所想念的,select1st和select2nd,因为它们对于map和multimap非常有用。给定一个pair,select1st返回它的第一个组件,而select2nd返回它的第二个。这些非标准仿函数类模板可以这么使用:
map m;
...
// 把所有的map键写到cout
transform(m.begin(), m.end(),
ostream_iterator(cout, "\n"),
select1st::value_type());
// 建立一个vector,把map中所有的值拷贝进去
vector v;
transform(m.begin(), m.end(), back_inserter(v),
select2nd::value_type());
如你所见,select1st和select2nd简化了用算法调用代替你可能必须写自己的循环(参见条款43),但是,如果你使用这些仿函数,它们是非标准的事实使你可能写出不可移植和无法维护的代码(参见条款47)。狂热的STL迷不在乎。他们认为select1st和select2nd没有首先进入标准是不公平的。
作为SGI实现一部分的其它非标准函数对象包括identity、project1st、project2nd、compose1和compose2。要知道这些做了什么,你可以访问网站,虽然你可以在本书的第187页找到使用compose2例子。现在,我希望你清楚访问SGI网站必然是有益的。
SGI的库实现超越了STL。他们的目标是开发一个标准C++库的完整实现,除了从C继承的部分(SGI认为你已经有一个可供支配的标准C库)。因此,可以从SGI获得的另一个值得注意的下载是C++ iostream库的实现。正如你期望的,这个实现与STL的SGI实现结合得很好,但在特征性能方面也优于很多伴随C++编译器的iostream实现。
STLport网站
STLport的主要卖点是,它提供一个可以移植到超过20个编译器的SGI STL实现(包括iostream等)的修改版本。和SGI的库一样,STLport可以免费下载。如果你写的代码必须在多个平台工作,你可以通过以STLport实现为标准并让你所有的编译器都使用它的方法来给自己节省一堆麻烦。
大多数STLport对SGI代码基础的修改都专注于改进移植性上,但STLport的STL也是我知道的唯一一个提供“调试模式”来帮助侦测STL的不正确用法的实现——可以编译但导致未定义的运行期行为的用法。例如,条款30关于在超过容器结尾的地方写入这个常见错误的讨论中用到了这个例子:
int transmogrify(int x);// 这个函数从x
// 产生一些新值
vector values;
...// 把数据放入values
vector results;
transform(values.begin(), values.end(),// 这回尝试在
results.end(),// 超过results结尾
transmogrify);// 的地方写入!
这可以编辑,但是当运行时,它产生未定义结果。如果你运气好,可怕的东西将在transform调用内部发生,而调试这个问题相对简单。如果你不运气好,transform调用会把数据倾泻在你的地址空间某处,但你得到后来才会发现。在那时,确定内存错误的原因——我们将说?——挑战。
STLport的调试模式会消除这个挑战。当上面的transform执行时,会产生下列消息(假设STLport安装在目录C:\STLport):
C:\STLport\stlport\stl\debug iterator.h:265 STL assertion failure : _Dereferenceable(*this)
然后程序停止了,因为如果STLport的调试模式遇到用法错误就会调用abort。如果你喜欢改为抛出一个异常,你可以把STLport配置成你的方式。
无可否认,上述错误信息没有它可能的清楚,而且不幸的是,报告的文件和行对应于内部STL断言的位置而不是调用transform的行, 但这也仍然比越过transform调用的运行,然后试图指出你的数据结构为什么是错误的要好。通过STLport的调试模式,你需要做的所有事情就是发动你的调试器,把调用堆栈返回到你写的代码,然后确定你做错了什么。发现厌恶的源代码行一般不是问题。
STLport的调试模式检测多种常见错误,包括把无效的区间传给算法,试图从一个空的容器里读取,使用来自一只容器的迭代器作为第二个迭代器成员函数的实参,等等。它通过迭代器和它们的容器间彼此跟踪来完成这个魔术。给定两个迭代器,这样就使检查它们是否来自同一个容器成为可能,而且当一个容器被修改时,这使适当的迭代器集失效成为可能。
因为STLport在调试模式使用特殊的迭代器实现,vector和string的迭代器就是类对象而不是原始指针。因此,使用STLport并在调试模式编译是确认没有人草率地处理指针和这些容器类型的迭代器之间区别的一种好方法。单单这点就足够成为给STLport调试方式一个机会的理由。
Boost网站
在1997年,当关闭通向C++国际标准的道路的钟声响起时,一些人对他们提倡的库特性没有入选而感到失望。这些人中的一部分本身就是委员会的成员,所以他们开始在第二轮标准化期间为标准库的扩充打下基础。结果就是Boost,一个任务是“提供免费、同行评议的C++库。重点在于可以和C++标准库配合良好的可移植库”的网站。在任务后面是一个动机:
一个库变成“现有实践”的程度,某人把它提交给未来标准的可能性就增加了。提交一个库给Boost.org是建立现有实践的一种方法……
换句话说,当一个库可能增加到标准C++库时,Boost把自己作为帮助区分好坏的鉴别机制。这是一个可敬的服务,而且我们全都应该感激。
让人感激的另一个原因是你可以在Boost中找到的库集合。我并不想在这里全部解释它们,尤其是因为当你读这些话时,无疑已经增加了许多新的库。不过,对于STL用户,两个库特别有用。第一个是智能指针库,包含用于引用计数智能指针的模板shared_ptr,与标准库的auto_ptr不同,它可以安全的储存在STL容器里(参见条款8)。Boost的智能指针库也提供shared_array,一个用于动态分配数组的引用计数智能指针,但条款13论述了动态分配数组不如vector和string,而且我希望你发现它的论点有说服力。
Boost第二个吸引STL迷的是它有关STL的函数对象和相关工具的群。这些库包含的基本原则是重新设计和重新实现STL函数对象和适配器后面的思想,这个结果消除了很多对标准仿函数功效的人为约束。作为这样的一个约束的例子,你将发现如果你试图把bind2nd和mem_fun或mem_fun_ref一起用(参见条款41)来把一个对象绑定到一个成员函数的参数,而且那个成员函数通过引用获取它的参数,你的代码不可能编译。如果你试图把not1或not2和ptr_fun一起用而且一个函数声明了通过引用的参数,你将发现一样的结果。在两中情况里的原因是在模板实例化的过程中,大多数STL平台产生到引用的引用,而到引用的引用在C++里不合法。(标准委员会正在酝酿标准里的一个改变来解决这个问题。)这里有一个被认为是“到引用的引用的问题”的例子:
class Widget {
public:
...
int readStream(istream& stream);// readStream用
...// 引用获取参数
};
vector vw;
...
for_each(// 大多数STL平台
vw.begin(), vw.end(),// 试图在这个调用中
bind2nd(mem_fun(&Widget::readStream), cin)// 产生一个
// 到引用的引用;
// 那样的代码
// 不能编译
Boost的函数对象避免了这个和其它问题,此外它们相当地扩大了函数对象的表现力。
如果你对STL函数对象的潜力感兴趣而且你想要更进一步探索它,赶快马上转到Boost。如果你痛恨函数对象而且认为它们的存在只是为了安慰少数转为C++程序员的Lisp拥护者,那也赶快转向Boost。Boost的函数对象库很重要,但它们只是你能在那个网站发现的一小部分而已。