for_each为什么不这样实现?

时间:2022-12-13 18:02:55
//for_each函数,sgi stl的一个实现版本
template<typename _InputIterator, typename _Function>
    _Function
    for_each(_InputIterator __first, _InputIterator __last, _Function __f)
    {
      // concept requirements
      __glibcxx_function_requires(_InputIteratorConcept<_InputIterator>)
      __glibcxx_requires_valid_range(__first, __last);
      for ( ; __first != __last;  ++__first)
__f(*__first);
      return __f;
    }

由于先执行了_f函数,才对迭代器进行++,这样如果_f中的操作,使迭代器失效,则++后的行为未定义,导致出错。

比如以下代码:
main.cpp

#include <iostream>
#include <list>
#include <algorithm>

using namespace std;

list<int> testlist;

void testfun(int elem)
{
    cout<<elem<<" ";
    if(elem == 5)
    testlist.remove(elem);
}

int main(int argc, char *argv[])
{
    for(int i=0; i<10; ++i)
     testlist.push_back(i);
   
    for_each(testlist.begin(),testlist.end(),testfun);

    system("PAUSE");
    return 0;
}

运行结果为 0 1 2 3 4 5 之后可能内存访问异常,coredump。

而如果for_each的实现版本改为:
template<typename _InputIterator, typename _Function>
    _Function
    for_each_my(_InputIterator __first, _InputIterator __last, _Function __f)
    {
      // concept requirements
      __glibcxx_function_requires(_InputIteratorConcept<_InputIterator>)
      __glibcxx_requires_valid_range(__first, __last);
      for ( ; __first != __last;)
__f(*__first++);
      return __f;
    }

先迭代器++,然后执行_f函数,这样即使_f使当前迭代器失效,也不会影响下面元素的操作。
上面的代码,如果用这个for_each实现,就能正确打印 0 1 2 3 4 5 6 7 8 9

请问为什么for_each用第一种实现方法?第二种岂非更安全些?不可能是stl的实现者没想到,那是什么原因?请大侠指点。

20 个解决方案

#1


效率的考虑,++前缀直接运算后返回,后缀需要返回之前的值,很明显,你这里的例子是一种非正常的编程行为

#2


++做为后缀,会先对当前值进行一次备份,然后对原有值++,实际操作的则是备份的数据

对于大量数据的便利操作,foreach或许是出于对效率的考虑吧,个人猜测

#3


引用 1 楼 booxiong 的回复:
效率的考虑,++前缀直接运算后返回,后缀需要返回之前的值,很明显,你这里的例子是一种非正常的编程行为


对,后++需要返回自加之前的值,效率低一些。但明显更安全一些,在迭代过程中删除修改元素,是使用stl常犯的错误,为什么stl不通过内部实现来降低使用者的风险呢?

是因为,效率和安全性相比,效率更重要一些?
还是stl专门要如此实现,就是防止程序员有类似使用?在迭代过程中同时删除和处理元素,感觉也是比较正常的需求呀?

#4


lz, 你提的问题很好。sgi之所这样实现有前提的。先看sgi中for_each的模板声明:
  for_each(_InputIterator __first, _InputIterator __last, _Function __f)

你应该注意到,for_each要求输入的迭代器是InputIterator类型的迭代器。而InputIterator迭代器的要求是:能读不能写,并且支持自增运算。这也要求你传递给for_each模板的函数或者函数对象__f不能对容器进行修改删除。所以也就不存在调用fun后,迭代器失效的情况。上述,解释你第一个疑问。
此外我再提一点:
  for ( ; __first != __last; ++__first)
__f(*__first);

  for ( ; __first != __last;)
__f(*__first++);

以上两种写法完全等价。只是编码风格不一样。我举一个例子:
supertool@supertool-desktop:~/test$ cat test.cpp 
/*
* File: test.cpp
*/
#include <iostream>
using namespace std;

int main()
{
for (int i = 0; i != 10; ++i)
cout << i << " ";
cout << endl;
for (int i = 0; i != 10; )
cout << i++ << " ";
cout << endl;

return 0;
}
supertool@supertool-desktop:~/test$ c++ -g -Wall test.cpp -o test
supertool@supertool-desktop:~/test$ ./test 
0 1 2 3 4 5 6 7 8 9 
0 1 2 3 4 5 6 7 8 9 

最后,说明一点:在使用sgi算法的时候一定要关注迭代器的种类。迭代器的种类一共有五种。(InputIterator, OutputIterator, ForwwardIterator, bidirectionalIterator, random-access Iterator)
参考文献:
     c++ primer 中文版 第四版, 11.3.5 五种迭代器

#5


这个是防君子不防小人的。

#6


for_each 是用来遍历的,不是用来修改的,它也只不过提醒你用input iterator ,你用不用就是你的事了。

#include <iostream> 
#include <algorithm>
#include <vector>
using   namespace   std; 

int arr[]=
{
1,2,3,4,5
};

vector<int> vec(arr,arr+5);

void func(int &n)
{
++n;
}

void print(int n)
{
cout<<n<<" ";
}

int main() 

for_each(vec.begin(),vec.end(),func);
for_each(vec.begin(),vec.end(),print);
cout<<endl;

    return 0; 
}


它不做任何检查,只要你敢用.

#7


引用 4 楼 huer0625 的回复:
lz, 你提的问题很好。sgi之所这样实现有前提的。先看sgi中for_each的模板声明:

C/C++ code
  for_each(_InputIterator __first, _InputIterator __last, _Function __f)

你应该注意到,for_each要求输入的迭代器是InputIterator类型的迭代器。而InputIterator迭代器的……


这点也是我疑惑的问题,既然是声明为InputIterator类型的迭代器,为什么可以修改删除元素?
《C++标准模板库》中描述for_each算法时,描述为"它可以不同的方式存取、处理、修改每一个元素"。

另外你说的两种写法完全等价,我不认同,起码效率上就不一样。
而且行为上也可能造成差异,你看我1楼的例子,测试代码完全一样,就是因为for_each的两种不同实现,就是不同的结果。

#8


声明为InputIterator类型

表示这个算法能接受的最低阶的迭代器类型。没没有其他含义。

for_each是一个不应该引起质变的算法。

#9


该回复于2011-04-13 11:18:48被版主删除

#10


for_each 是一个函数,它没办法限制迭代器的类型,重载不现实,因为迭代器类型千千万万 .

特化不可能,因为只有类可以特化. 所以,它只能这样办,告诉用户请传入一个只读迭代器,告诉用户别修改数

据.

#11


"它可以不同的方式存取、处理、修改每一个元素"。
而不是删除元素,因为那是在操作容器!

#12


早上起来又思考了一下,其实除了效率之外,for_each根本无法保证对迭代器操作的后果

以lz的例子来说,list<>中remove一个元素是摘除操作,也就是不影响++之后的迭代器的有效性

但是,如果我写一个新的模板,其中对于remove的实现是在摘除之后对剩余元素进行重新排序,那么,使用后缀++也不能避免_f中使用remove后下个元素的失效问题

毕竟,stl讲究的是泛用性和效率,如果希望库或者语言本身来替代程序员做很多安全性方面的检查,C/C++并非一个很好的选择

#13


f must not modify the order of the sequence.

人家在算法前面就提醒过使用者了,不应该修改元素顺序。所以这个属于防君子不防小人的。

#14


C++标准程序库  作者说了,STL更注重的是效率,不是安全。。。

#15


引用 8 楼 pengzhixi 的回复:
声明为InputIterator类型

表示这个算法能接受的最低阶的迭代器类型。没没有其他含义。

for_each是一个不应该引起质变的算法。


谢谢提醒,才搞明白申明inputIterator是表示算法能接受的最低阶的迭代器类型,这也说明for_each可作用于所有标准容器上,不表示算法对元素只读。

#16


引用 11 楼 taodm 的回复:
"它可以不同的方式存取、处理、修改每一个元素"。
而不是删除元素,因为那是在操作容器!


谢谢大侠,原来我搞混了这两个概念,对,操作元素和操作容器是有区别的。
《C++标准模板库》把for_each归为变动性算法,是指存取、处理、修改元素,而不是容器。

#17


正常来说任何对容器造成重新排序的操作都是被严格禁止的

#18


引用 12 楼 pamtry 的回复:
早上起来又思考了一下,其实除了效率之外,for_each根本无法保证对迭代器操作的后果

以lz的例子来说,list<>中remove一个元素是摘除操作,也就是不影响++之后的迭代器的有效性

但是,如果我写一个新的模板,其中对于remove的实现是在摘除之后对剩余元素进行重新排序,那么,使用后缀++也不能避免_f中使用remove后下个元素的失效问题

毕竟,stl讲究的是泛用性和效……


这个后来我也想到了,如果对op函数对容器的改变,造成了所有迭代器都无效,那么先++迭代器也没用了。
看来,stl的确是要求程序员自己保证迭代器的有效性了!

#19


谢谢回复的各位,多谢了。

#20


引用 7 楼 typecsdn 的回复:
引用 4 楼 huer0625 的回复:
lz, 你提的问题很好。sgi之所这样实现有前提的。先看sgi中for_each的模板声明:

C/C++ code
for_each(_InputIterator __first, _InputIterator __last, _Function __f)

你应该注意到,for_each要求输入的迭代器是InputIterator类型的迭……

lz,针对你的疑问,我重新审核前面的解释,发现之前的解释存在问题。看了你这个回复,跟同事探讨后。发现我自己也混淆了两个概念。for_each中要求输入的迭代器是input iteraor, 应该理解成for_each要求的迭代器类型最低标准是InputerIterator, 因为这个算法要求进行++操作和*(引用,即读)操作。第二:我混淆了针对容器操和元素的操作的概念。准确的说是标准库里面的算法不执行针对容器的操作,但是允许修改容器里面的元素。当然如果你要修改你的元素,那么你传递给for_each的迭代器同时必须是OutputIterator类型。
总结:for_each支持针对容器元素的修改,前提是你传递给它的迭代器也支持写操作;但是直接使用for_each对容器进行删除操作是不允许的。
此外,lz那两种写法,在迭代器合法的前提下,是等价的,而且也不存在性能上的差别。如果在迭代器出现非法的情况,那两种写法存在差别。先解释那种写法不存在性能的差别,再解释如果迭代器非法时,那两种的写法的差别。
第一种写法:
  for ( ; __first != __last; ++__first)
__f(*__first);

第二种写法:
  for ( ; __first != __last;)
__f(*__first++);

上面第一种写法的流程如下:
先判断__first != __last
接下来对__first进行解引用 __f(*__first);
然后对__first 直接自加1: ++__first
之后再进行__first != __last的判断。。。。
(这里罗嗦两句,++__first, 在底层实现的时候是直接递增1,并不对__first的值进行保存;
而__first++, 在底层实现的时候首先把__first值先保存起来,然后才对__first的值进行自加1操作。
这两个的区别在于如果你不需要使用到__first的值的时候,使用++__first会比__first++会省掉保存那个步骤的开销。这也是大家经常建议使用++i代替i++的理由,大这些都是有前提的。)
上面第二种写法的流程如下:
先判断__first != __last
接下来对__first++操作,正如刚才的解释,__first++再对自己进行自加1操作时候,会把自己的值先报存起来;再对__first++操作后,进行解引用操作,这时候引用的对象是__first自加1时所保存的值所指向的对象: __f(*__first++);
然后再进行__first != __last的判断。。。。
综上:可以得出这样的一个结论:当迭代器始终是合法的时候,这两种写法是等价的,性能上也是等价的。
下面解释当迭代器如果遇到非法的时候两者不同的反应:
在你所举的例子中:当元素5被删除的时候,其对应的迭代器也失效:
对于第一种写法:
当*__first = 5时,执行__f(*__first)后,__first已经失效,所以之后针对__first进行++__first操作就属于非法的操作了。
对于第二种写法:
当*__first = 5时,再执行函数__f(*__first++)之前,根据刚才的解释__first已经指向元素6,当函数后,指向5的迭代器失效,但是这时候__first已经指向元素6了。所以程序可以被继续执行下去。
在你的例子,程序之所以能够执行下去还有一个重要的原因。那就是因为,在删除list元素的时候,只有指向这个元素本身的迭代器失效,这个是由list(链表)的特性决定的。如果换一种数据类型或者容器,如果针对某个元素进行删除会导致整个容器所有迭代器都失效,第二种写法,也可能会core掉。
综上: 如果某种操作会导致迭代器部分失效,则上述两种写法可能存在差别。
ps:
  本人更倾向第一种写法,因为这种写法能够把这种可能出现的误用尽快的暴露出来。

#1


效率的考虑,++前缀直接运算后返回,后缀需要返回之前的值,很明显,你这里的例子是一种非正常的编程行为

#2


++做为后缀,会先对当前值进行一次备份,然后对原有值++,实际操作的则是备份的数据

对于大量数据的便利操作,foreach或许是出于对效率的考虑吧,个人猜测

#3


引用 1 楼 booxiong 的回复:
效率的考虑,++前缀直接运算后返回,后缀需要返回之前的值,很明显,你这里的例子是一种非正常的编程行为


对,后++需要返回自加之前的值,效率低一些。但明显更安全一些,在迭代过程中删除修改元素,是使用stl常犯的错误,为什么stl不通过内部实现来降低使用者的风险呢?

是因为,效率和安全性相比,效率更重要一些?
还是stl专门要如此实现,就是防止程序员有类似使用?在迭代过程中同时删除和处理元素,感觉也是比较正常的需求呀?

#4


lz, 你提的问题很好。sgi之所这样实现有前提的。先看sgi中for_each的模板声明:
  for_each(_InputIterator __first, _InputIterator __last, _Function __f)

你应该注意到,for_each要求输入的迭代器是InputIterator类型的迭代器。而InputIterator迭代器的要求是:能读不能写,并且支持自增运算。这也要求你传递给for_each模板的函数或者函数对象__f不能对容器进行修改删除。所以也就不存在调用fun后,迭代器失效的情况。上述,解释你第一个疑问。
此外我再提一点:
  for ( ; __first != __last; ++__first)
__f(*__first);

  for ( ; __first != __last;)
__f(*__first++);

以上两种写法完全等价。只是编码风格不一样。我举一个例子:
supertool@supertool-desktop:~/test$ cat test.cpp 
/*
* File: test.cpp
*/
#include <iostream>
using namespace std;

int main()
{
for (int i = 0; i != 10; ++i)
cout << i << " ";
cout << endl;
for (int i = 0; i != 10; )
cout << i++ << " ";
cout << endl;

return 0;
}
supertool@supertool-desktop:~/test$ c++ -g -Wall test.cpp -o test
supertool@supertool-desktop:~/test$ ./test 
0 1 2 3 4 5 6 7 8 9 
0 1 2 3 4 5 6 7 8 9 

最后,说明一点:在使用sgi算法的时候一定要关注迭代器的种类。迭代器的种类一共有五种。(InputIterator, OutputIterator, ForwwardIterator, bidirectionalIterator, random-access Iterator)
参考文献:
     c++ primer 中文版 第四版, 11.3.5 五种迭代器

#5


这个是防君子不防小人的。

#6


for_each 是用来遍历的,不是用来修改的,它也只不过提醒你用input iterator ,你用不用就是你的事了。

#include <iostream> 
#include <algorithm>
#include <vector>
using   namespace   std; 

int arr[]=
{
1,2,3,4,5
};

vector<int> vec(arr,arr+5);

void func(int &n)
{
++n;
}

void print(int n)
{
cout<<n<<" ";
}

int main() 

for_each(vec.begin(),vec.end(),func);
for_each(vec.begin(),vec.end(),print);
cout<<endl;

    return 0; 
}


它不做任何检查,只要你敢用.

#7


引用 4 楼 huer0625 的回复:
lz, 你提的问题很好。sgi之所这样实现有前提的。先看sgi中for_each的模板声明:

C/C++ code
  for_each(_InputIterator __first, _InputIterator __last, _Function __f)

你应该注意到,for_each要求输入的迭代器是InputIterator类型的迭代器。而InputIterator迭代器的……


这点也是我疑惑的问题,既然是声明为InputIterator类型的迭代器,为什么可以修改删除元素?
《C++标准模板库》中描述for_each算法时,描述为"它可以不同的方式存取、处理、修改每一个元素"。

另外你说的两种写法完全等价,我不认同,起码效率上就不一样。
而且行为上也可能造成差异,你看我1楼的例子,测试代码完全一样,就是因为for_each的两种不同实现,就是不同的结果。

#8


声明为InputIterator类型

表示这个算法能接受的最低阶的迭代器类型。没没有其他含义。

for_each是一个不应该引起质变的算法。

#9


该回复于2011-04-13 11:18:48被版主删除

#10


for_each 是一个函数,它没办法限制迭代器的类型,重载不现实,因为迭代器类型千千万万 .

特化不可能,因为只有类可以特化. 所以,它只能这样办,告诉用户请传入一个只读迭代器,告诉用户别修改数

据.

#11


"它可以不同的方式存取、处理、修改每一个元素"。
而不是删除元素,因为那是在操作容器!

#12


早上起来又思考了一下,其实除了效率之外,for_each根本无法保证对迭代器操作的后果

以lz的例子来说,list<>中remove一个元素是摘除操作,也就是不影响++之后的迭代器的有效性

但是,如果我写一个新的模板,其中对于remove的实现是在摘除之后对剩余元素进行重新排序,那么,使用后缀++也不能避免_f中使用remove后下个元素的失效问题

毕竟,stl讲究的是泛用性和效率,如果希望库或者语言本身来替代程序员做很多安全性方面的检查,C/C++并非一个很好的选择

#13


f must not modify the order of the sequence.

人家在算法前面就提醒过使用者了,不应该修改元素顺序。所以这个属于防君子不防小人的。

#14


C++标准程序库  作者说了,STL更注重的是效率,不是安全。。。

#15


引用 8 楼 pengzhixi 的回复:
声明为InputIterator类型

表示这个算法能接受的最低阶的迭代器类型。没没有其他含义。

for_each是一个不应该引起质变的算法。


谢谢提醒,才搞明白申明inputIterator是表示算法能接受的最低阶的迭代器类型,这也说明for_each可作用于所有标准容器上,不表示算法对元素只读。

#16


引用 11 楼 taodm 的回复:
"它可以不同的方式存取、处理、修改每一个元素"。
而不是删除元素,因为那是在操作容器!


谢谢大侠,原来我搞混了这两个概念,对,操作元素和操作容器是有区别的。
《C++标准模板库》把for_each归为变动性算法,是指存取、处理、修改元素,而不是容器。

#17


正常来说任何对容器造成重新排序的操作都是被严格禁止的

#18


引用 12 楼 pamtry 的回复:
早上起来又思考了一下,其实除了效率之外,for_each根本无法保证对迭代器操作的后果

以lz的例子来说,list<>中remove一个元素是摘除操作,也就是不影响++之后的迭代器的有效性

但是,如果我写一个新的模板,其中对于remove的实现是在摘除之后对剩余元素进行重新排序,那么,使用后缀++也不能避免_f中使用remove后下个元素的失效问题

毕竟,stl讲究的是泛用性和效……


这个后来我也想到了,如果对op函数对容器的改变,造成了所有迭代器都无效,那么先++迭代器也没用了。
看来,stl的确是要求程序员自己保证迭代器的有效性了!

#19


谢谢回复的各位,多谢了。

#20


引用 7 楼 typecsdn 的回复:
引用 4 楼 huer0625 的回复:
lz, 你提的问题很好。sgi之所这样实现有前提的。先看sgi中for_each的模板声明:

C/C++ code
for_each(_InputIterator __first, _InputIterator __last, _Function __f)

你应该注意到,for_each要求输入的迭代器是InputIterator类型的迭……

lz,针对你的疑问,我重新审核前面的解释,发现之前的解释存在问题。看了你这个回复,跟同事探讨后。发现我自己也混淆了两个概念。for_each中要求输入的迭代器是input iteraor, 应该理解成for_each要求的迭代器类型最低标准是InputerIterator, 因为这个算法要求进行++操作和*(引用,即读)操作。第二:我混淆了针对容器操和元素的操作的概念。准确的说是标准库里面的算法不执行针对容器的操作,但是允许修改容器里面的元素。当然如果你要修改你的元素,那么你传递给for_each的迭代器同时必须是OutputIterator类型。
总结:for_each支持针对容器元素的修改,前提是你传递给它的迭代器也支持写操作;但是直接使用for_each对容器进行删除操作是不允许的。
此外,lz那两种写法,在迭代器合法的前提下,是等价的,而且也不存在性能上的差别。如果在迭代器出现非法的情况,那两种写法存在差别。先解释那种写法不存在性能的差别,再解释如果迭代器非法时,那两种的写法的差别。
第一种写法:
  for ( ; __first != __last; ++__first)
__f(*__first);

第二种写法:
  for ( ; __first != __last;)
__f(*__first++);

上面第一种写法的流程如下:
先判断__first != __last
接下来对__first进行解引用 __f(*__first);
然后对__first 直接自加1: ++__first
之后再进行__first != __last的判断。。。。
(这里罗嗦两句,++__first, 在底层实现的时候是直接递增1,并不对__first的值进行保存;
而__first++, 在底层实现的时候首先把__first值先保存起来,然后才对__first的值进行自加1操作。
这两个的区别在于如果你不需要使用到__first的值的时候,使用++__first会比__first++会省掉保存那个步骤的开销。这也是大家经常建议使用++i代替i++的理由,大这些都是有前提的。)
上面第二种写法的流程如下:
先判断__first != __last
接下来对__first++操作,正如刚才的解释,__first++再对自己进行自加1操作时候,会把自己的值先报存起来;再对__first++操作后,进行解引用操作,这时候引用的对象是__first自加1时所保存的值所指向的对象: __f(*__first++);
然后再进行__first != __last的判断。。。。
综上:可以得出这样的一个结论:当迭代器始终是合法的时候,这两种写法是等价的,性能上也是等价的。
下面解释当迭代器如果遇到非法的时候两者不同的反应:
在你所举的例子中:当元素5被删除的时候,其对应的迭代器也失效:
对于第一种写法:
当*__first = 5时,执行__f(*__first)后,__first已经失效,所以之后针对__first进行++__first操作就属于非法的操作了。
对于第二种写法:
当*__first = 5时,再执行函数__f(*__first++)之前,根据刚才的解释__first已经指向元素6,当函数后,指向5的迭代器失效,但是这时候__first已经指向元素6了。所以程序可以被继续执行下去。
在你的例子,程序之所以能够执行下去还有一个重要的原因。那就是因为,在删除list元素的时候,只有指向这个元素本身的迭代器失效,这个是由list(链表)的特性决定的。如果换一种数据类型或者容器,如果针对某个元素进行删除会导致整个容器所有迭代器都失效,第二种写法,也可能会core掉。
综上: 如果某种操作会导致迭代器部分失效,则上述两种写法可能存在差别。
ps:
  本人更倾向第一种写法,因为这种写法能够把这种可能出现的误用尽快的暴露出来。

#21