Item 44:优先使用与泛型算法同名的成员函数
一些容器拥有和STL泛型算法同名的成员函数。关联容器提供count()、find()、lower_bound()、upper_bound(),和equal_range(),而list提供了remove()、remove_if()、unique()、sort()、merge(),和reverse()。这样做有两个理由。首先,成员函数更快。其次,它们与容器结合得更好(尤其是关联容器)。 那是因为同名的泛型算法和成员函数通常并不是以完全相同的方式来做同样的事。
我们以对关联容器的试验开始。假如有一个set<int>,它存储了一百万个元素,而你想找到元素727的第一个发生位置(如果它存在的话)。有两个最然的方法来执行搜索:
set<int> s; // create set, put
... // 1,000,000 values
// into it
set<int>::iterator i = s.find(727); // use find member
if (i != s.end()) ... // function
set<int>::iterator i = find(s.begin(), s.end(), 727); // use find algorithm
if (i != s.end()) ...
成员函数的find()运行对数时间,所以不管727是否存在于set中,set::find()只需执行不超过40次比较来查找它,而平均只需要20次。相比而言,泛型算法的find()运行线性时间,所以如果727不在set中时它需要执行1,000,000次比较。即使727在set中,也平均需要执行500,000次比较来定位它。效率得分如下:
成员的find():大约40(最坏情况)到大约20(平均情况)
泛型算法find():1,000,000(最坏情况)到500,000(平均情况)
和高尔夫比赛一样,分值低的赢。如你所见,比赛结果毫无争议可言。
我必须对成员函数的find()所需的比较次数表示小小的谨慎,因为它有些依赖于关联容器的实现。绝大部分的实现是使用的红黑树,平衡树的一种,失衡度可能达到2(that may be out of balance by up to a factor of two)。在这样的实现中,对一百万个元素的set进行搜索,所需的最大比较次数是38次。但对绝大部分的搜索情况而言,不超过22次。一个基于完全平衡树的实现绝不需要超过21次比较,但在实践中,完全平衡树的效率总得来说不如红黑树。这就是为什么绝大多数的STL实现都使用红黑树。
效率不是成员函数的find()和泛型算法find()间的唯一差别。如Item 19所解释,STL泛型算法判断两个对象是否相同,检查的是equality(WQ注:逻辑相等,使用operator==),而关联容器检测的是equivalence(WQ注:数学相等,two elements are equal if neither is less than the other)。 因此,泛型算法find()搜索727用的是逻辑相等,而成员函数的find()用的是数学相等。逻辑相等和数学相等间的区别可以造成成功搜索和不成功搜索的区别。举例来说,Item 19演示了用泛型算法find()在关联容器搜索失败而用成员函数的find()搜索却成功的情况!因此,如果操纵关联容器的话,你应该优先使用成员函数形式的find()、count()、lower_bound()等函数,而不是同名的泛型算法,因为这些成员函数版本提供了和其它成员函数相一致的行为。由于逻辑相等和数学相等间的差别,泛型算法不能提供这样的一致行为。
这一差别对map和multimap尤其明显,因为它们容纳的是pair型对象,而它们的成员函数只在意pair对象的key部分。因此,成员函数count()只统计key值匹配的pair对象的数目 (“匹配”,自然是检测含义相等);pair的value部份被忽略。成员函数的find()、lower_bound()、upper_bound(),以及equal_range()同样如此。如果你使用泛型算法find(),于是,它的寻找将基于(a)逻辑相等和(b)pair的全部组成部分;泛型算法的find()、lower_bound()等同样如此。要想让泛型算法只关注于pair的key部分,必须要跳出Item 23描述了的限制(那儿介绍了用数学相等代替逻辑等价作测试的方法)。
另一方面,如果你真的关心效率,你可以决定采用Item 23中的技巧,协同Item34中讲的对数时间搜索算法。相对于性能的提升,这只是一个很小的代价。再者,如果你真的在乎效率,你应该考虑非标的hash容器(在Item 25进行了描述),只是,你将再次面对逻辑相等和数学相等的区别。
对于标准的关联容器,选择成员函数而不是泛型算法,有几个好处。首先,你得到的是对数时间而不是线性时间的性能。其次,你判断两个元素“相同”使用的是数学相等,这是关联容器的默认定义。第三,当操纵map和multimap时,你自动地只处理key值而不是(key, value)对。这三点给了优先使用成员函数充分的理由。
让我们转到list的与泛型算法同名的成员函数身上。这里的故事几乎全部是关于效率的。每个被list作了特别化的泛型算法(remove()、remove_if()、unique()、sort()、merge(),和reverse())都要拷贝对象,而list的特别版本不拷贝任何东西;它们只是简单地操纵连接list的节点的指针。泛型算法和成员函数的算法复杂度是相同的,但如果操纵指针比拷贝对象的代价小的话,list的版本应该提供更好的性能。
牢牢记住这一点很重要:list的成员函数版本的行为和同名的泛型算法版本,行为经常不相同。如Item 32所解释,调用泛型算法的remove()、remove_if(),和unique(),必须紧跟着调用erase()函数(如果你真的想从容器中清除对象的话);但list的remove()、remove_if()和unique()成员函数真的去掉了元素,不需要接着调用erase()。
在泛型算法sort()和list的sort()成员函数间的一个重要区别是:前者不能用于list。作为单纯的双向iterator,list的iterator不能传给泛型算法sort()。Merge()泛型算法和list的merge()成员函数之间也同样存在巨大差异。这个泛型算法不被允许修改源范围,但list::merge()同样可以修改它的宿主list。
所以,你明白了吧。 当面临在同名的STL泛型算法和容器成员函数间进行选择时,你应该优先使用成员函数。它几乎肯定更高效,而且它看起来和容器的惯常行为集成得更好。