每一个算法都至少需要使用一对迭代器来指定一个对象区间。算法的操作也是在这个区间上运行,算法是要在这个区间上,检查每一个对象,是否满足自己的期望。
因此,算法的本质就是运行一个简单的循环,在指定的区间内遍历每一个对象,并对对象做出自己期望的检查或者调用。
但并不是所有的算法都会完整的遍历完整个区间,比如,find和find_if。这两个算法,如果在区间的某个位置找到了自己想要找的对象,则会直接返回。但无论如何,它还是进行了小循环的遍历。只有当他遍历完区间中所有的对象后,他才会知道我们想要找的对象并不存在。
如果算法的内部都是循环,并且stl提供的算法涉及到的面非常广,那么我们本该需要编写循环来完成的任务是不是都能够用算法来实现?
#include <iostream>
#include <vector>
#include <algorithm>
#include <iterator>
using namespace std;
class Widget{
public:
Widget()
{
}
bool reload() const
{
//@todo ...
cout << "reload ..." << " ";
return true;
}
private:
int m_index;
};
int main()
{
typedef vector<Widget> WidgetVec;
typedef vector<Widget>::iterator WidgetIter;
WidgetVec vW;
for(int index = 10; index > 0; --index)
{
Widget w;
vW.push_back(w);
}
for(WidgetIter it = vW.begin(); it != vW.end(); ++it)
{
it->reload();
}
return 0;
}
上面的例子,我们有一个支持重加载的类Widget
,如果想对容器vector
中的每个对象都进行重加载,通常情况下,我们会用简单地循环来实现。
同样地,如果我们用算法来实现呢:
for_each(vW.begin(), vW.end(), mem_fun_ref(&Widget::reload));
通常情况下,对于我们来说,编写一个循环会比调用一个算法来的更自然和和谐。至少在阅读代码上来看,简单的循环代码比算法更直接了当,因为使用算法,可能还要去弄明白 mem_fun_ref
的含义。
但是通过上面的对比我们可以看出:
1、效率:算法调用通常比我们手写循环效率更高。
通过上面的例子,手写循环的循环条件是,it != vW.end();
并且每次循环都会进行一次判断;也就是说,我们指定的区间对象的数量就是vector::end()
函数的调用次数。
但是算法中呢:只调用了一次。这个我们可以通过查看算法内部for_each
的实现来判断出来。
template <class _InputIter, class _Function>
_Function for_each(_InputIter __first, _InputIter __last, _Function __f) {
for ( ; __first != __last; ++__first)
__f(*__first);
return __f;
}
而在stl中,像 begin(), end(),size()
等这些可能被频繁使用的函数,都会尽最大可能来提升他们的性能。
2、正确性:手写循环比使用算法更容易出错
为什么说手写循环会比使用算法更容易出错呢?这是因为,如果我们对容器中的元素使用迭代器的方式新增或者删除,则原本的迭代器会失效,如果没有重新赋值迭代器,很容易出现意料之外的事情。
所以在编写代码时,最重要的莫过于保证迭代器有效,并且迭代器指向你期望的位置。我们看下面的例子。
double data[maxSize];
...
deque<double> d;
typedef deque<double>::iterator dequeIter;
... //初始化d
for(int index = 0; index < maxSize; ++index)
{
d.insert(d.begin(), data[index] + 10);
}
面的例子中,首先我们有一个已知大小的double
类型的数组,同时有一个队列容器,假设我们需要将数组中的每个元素先加10,然后将数组插入到容器d的最前面。上面的循环中,我们其实是得不到我们想要的结果,因为在顺序遍历数组之后,我们将每个元素的顺序搞反之后插入了队列中。并且每次都要调用begin()
函数。
紧接着我们想到了迭代器的做法,顺手也就有了下面的循环,虽然下面的循环看似是正常的,因为我们不仅定义了通过自增来指示插入位置的迭代器,并且也避免了上一个循环中调用begin()
函数的效率问题。
int main()
{
typedef deque<double> DataDeque;
typedef deque<double>::iterator DataIter;
double data[5] = {2.04, 1.05, 42.01, 5.14, 23.152};
DataDeque d;
DataIter it = d.begin();
for(int index = 0; index < 5; ++index)
{
d.insert(it++, data[index] + 10);
}
DataIter iter = d.begin();
for(; iter != d.end(); ++iter)
{
cout << *iter << " "; // 12.04 12.04 12.04 12.04 12.04
}
return 0;
}
但其实这个循环存在着比较严重的问题,因为每次调用insert
函数之后,队列容器的迭代器都会失效,包括我们前面定义的迭代器 it
。也就产生了未定义的行为。
当理解清楚之后,我们每次在调用insert函数的时候将返回的迭代器重新进行赋值,以保证迭代器的有效性。
for(int index = 0; index < 5; ++index, ++it)
{
it = d.insert(it, data[index] + 10);
}
而上面的这些问题,如果我们使用stl的算法则会避免。
transform(data, data + 5, inserter(d, it), bind2nd(plus<double>(), 10));
我们可以看下transform算法的实现。
template <class _InputIter1, class _InputIter2, class _OutputIter,
class _BinaryOperation>
_OutputIter transform(_InputIter1 __first1, _InputIter1 __last1,
_InputIter2 __first2, _OutputIter __result,
_BinaryOperation __binary_op) {
__STL_REQUIRES(_InputIter1, _InputIterator);
__STL_REQUIRES(_InputIter2, _InputIterator);
__STL_REQUIRES(_OutputIter, _OutputIterator);
for ( ; __first1 != __last1; ++__first1, ++__first2, ++__result)
*__result = __binary_op(*__first1, *__first2);
return __result;
}
它的四个参数,前两个是输入容器的一个区间,第三个是返回结果容器的迭代器,最后一个是一个类型或者是函数模版,它的实现了是将某个函数应用到某个容器中的某个区间的每个元素上,并将将结果写如一个其他的容器。
而plus
已经是stl中的一个仿函数。所以,上面的例子我们通过调用算法,就可以避免上述出现迭代器失效的问题。
3、可维护性:使用算法通常比手写循环的代码更加简洁,且可读性强
stl有70个算法名称,并且有100多个不同的函数模版,每个算法都完成一个特定的任务。而stl算法的名称基本上包含了该算法的功能解释,这也就使得调用算法比任何循环都更直接清地表达代码的含义。这就好像是C和C++库中的函数一样。
- 有人已经实现了,没必要再写一遍
- 这些名字已经形成了标准,每个人都知道他们的功能
- 库函数的实现总会比我们实现的功能效率更高
算法的名称比普遍的循环更能显示实际的意义,但是,并不是在所有的场景中使用算法均比手写循环看起来更间接。
typedef vector<int> VecInt;
typedef vector<int>::iterator VecIntIter;
int x, y;
VecInt v;
for(int index = 0; index < 50; ++index)
{
v.push_back(index);
}
比如上面的例子,我们想找到在一个容器中,大于x,并且小于y的第一个元素,使用循环,则:
for(VecIntIter it = v.begin(); it != v.end(); ++it)
{
if(*it > x && *it < y)
{
cout << *it << " ";
break;
}
}
同样的,上面的例子是查找容器中满足某个条件的第一个元素,这在我们的映象中,find_if
就是干这个事的。
template <class _InputIter, class _Predicate>
inline _InputIter find_if(_InputIter __first, _InputIter __last,
_Predicate __pred,
input_iterator_tag)
{
while (__first != __last && !__pred(*__first))
++__first;
return __first;
}
所以,我们只需要写出满足上面判断条件的函数就行了。SGI stl中有一个compose2
的私有函数能够满足我们的要求。
template <class _Operation1, class _Operation2, class _Operation3>
inline binary_compose<_Operation1, _Operation2, _Operation3>
compose2(const _Operation1& __fn1, const _Operation2& __fn2,
const _Operation3& __fn3)
{
return binary_compose<_Operation1,_Operation2,_Operation3>
(__fn1, __fn2, __fn3);
}
则按照上面的要求:
VecIntIter it = find_if(v.begin(), v.end(),
compose2(
logical_and<bool>(),
bind2nd(greater<int>(), x),
bind2nd(less<int>(), y)
)
);
但是前面我特地对部分说明加粗额,compose2
是SGI STL独有的函数,如果我们使用的版本不是SGI,我们也可以通过以前的经验来实现自有适配函数子的方法来实现这个功能。
template<typename T>
class BetweenValue : public unary_function<T, bool>
{
public:
BetweenValue(const T& lhs, const T& rhs) : m_lhs(lhs), m_rhs(rhs){}
bool operator()(const T& val) const
{
return m_lhs < val && val < m_rhs;
}
private:
T m_lhs;
T m_rhs;
};
VecIntIter it = find_if(v.begin(), v.end(), BetweenValue<int>(x, y) );
虽然我们能够使用算法的方式来实现上面的需求,但是和手写循环相比较呢?
- 代码数量上涨明显,手写循环只需要一行代码即可完成,但是
BetweenValue
模板需要10多行; - 虽然能够通过
find_if
算法名称得知一部分的含义,但是如果想知道具体的调用做了什么,还是要看完BetweenValue
模版才能知道。
所以,并不是任何时候调用算法逗比手写循环来的直接简单明了的。但任何时候,我们都应该使用较高层次的insert,find,for_each
来代替手写for,while
等较低层次的调用。