条款4:用empty()来代替检查size()是否为0
对于任意容器c,写下
if (c.size() == 0)...
本质上等价于写下
if (c.empty())...
这就是例子。你可能会奇怪为什么一个构造会比另一个好,特别是事实上empty()的典型实现是一个返回size()是否返回0的内联函数。
你应该首选empty()的构造,而且理由很简单:对于所有的标准容器,empty()是一个常数时间的操作,但对于一些list实现,size()花费线性时间。
但是什么造成list这么麻烦?为什么不能也提供一个常数时间的size()?答案是对于list特有的splice()有很多要处理的东西。考虑这段代码:
list<int> list1; list<int> list2; ... list1.splice( // 把list2中 list1.end(), list2, // 从第一次出现5到 find(list2.begin(), list2.end(), 5), // 最后一次出现10 find(list2.rbegin(), list2.rend(), 10).base() // 的部分移到list1的结尾。 ); // 关于调用的 // "base()"的信息,请参见条款28
除非list2在5的后面有一个10,否则这段代码无法工作,但是咱们假设这不是问题。取而代之的是,咱们关注于这个问题:合并后list1有多少元素?很明显,合并后list1的元素个数等于合并之前list1的元素个数加上合并进去的元素个数。但是有多少元素合并进去了?那等于由find(list2.begin(), list2.end(), 5)和find(list2.rbegin(), list2.rend(), 10).base()所定义的区间的元素个数。OK,那有多少?在没有遍历这个区间并计数之前无法知道。那就是问题所在。
假设你现在要负责实现list,list不只是一个普通的容器,它是一个标准容器,所以你知道你的类会被广泛使用。你自然希望你的实现越高效越好。你指出客户常常会想知道list中有多少元素,所以你把size()做成常数时间的操作。因此你要把list设计为它总是知道包含有多少元素。
与此同时,你知道对于所有标准容器,只有list提供了不用拷贝数据就能把元素从一个地方合并到另一个地方的能力。你的推论是,很多list用户会特别选择list,因为它提供了高效的合并。他们知道从一个list合并一个区域到另一个list可以在常数时间内完成,而你知道他们了解这点,所以你确定需要符合他们的期望,那就是合并是一个常数时间的成员函数。
这让你进退两难。如果size()是一个常数时间操作,当操作时每个list成员函数必须更新list的大小。也包括了splice()。但让splice()更新他所更改的list的大小的唯一的方法是算出合并进来的元素的个数,但是这么做就会使splice()不可能有你所希望的常数时间的性能。如果你去掉了splice()要更新他所修改的list的大小的需求,splice()就可以是常数时间,但size()就变成线性时间的操作。一般来说,它必须遍历它的整个数据结构来才知道它包含多少元素。不管你如何看待它,有的东西——size()或者splice()——必须让步。一个或者另一个可以是常数时间操作,但不能都是。
不同的list实现用不同的方式解决这个矛盾,依赖于他们的作者选择的是让size()或splice()达到最高效率。如果你碰巧使用了一个常数时间的splice()比常数时间的size()优先级更高的list实现,调用empty()比调用size()更好,因为empty()总是常数时间操作。即使你现在用的不是这样的实现,你可能发现自己会在未来会使用一个这样实现。比如,你可能把你的代码移植到一个使用不同的STL实现的不同的平台,你也可能只是决定在你现在的平台上切换到一个不同的STL实现。
不管发生了什么,如果你用empty()来代替检查size()是否为0,你都不会出错。所以在想知道容器是否包含0个元素的时候都应该调用empty()。