std :: for_each优于for循环的优点


166

std::for_eachover forloop 有什么优点吗?在我std::for_each看来,这似乎只会阻碍代码的可读性。那么为什么有些编码标准建议使用它呢?


10
std::for_each当与一起使用boost.lambdaboost.bind通常可以提高可读性时

问题和公认的答案来自2010年。有关最新答案(2018年以来
Segal-Halevi

Answers:


181

这种做法的好处与C ++ 11(以前称为C ++ 0x中),是这个无聊的争论将尘埃落定。

我的意思是,没有一个在他们的正确想法中想要遍历整个集合的人仍然会使用此方法

for(auto it = collection.begin(); it != collection.end() ; ++it)
{
   foo(*it);
}

或这个

for_each(collection.begin(), collection.end(), [](Element& e)
{
   foo(e);
});

基于范围的for循环语法可用时:

for(Element& e : collection)
{
   foo(e);
}

这种语法已经在Java和C#中使用了一段时间,实际上,在我看到的每一个最近的Java或C#代码中,foreach循环都比经典for循环更多。


12
实际上,带铲的foreach循环已经可以使用很长时间了,我仍然想用for_each和lambda函数进行迭代。
维克多·瑟

19
想要整个容器范围的假设不是问题的一部分,因此这只是部分答案。
Adrian McCarthy 2012年

6
请注意,遍历一个元素可能并不是您唯一想做的事情,因此使用for_each可能是一个好主意,以便您了解find / partition / copy_replace_if和其他元素,这实际上是很多for循环的原因做。
Macke 2012年

10
Range-for很好,除非您实际需要迭代器(否则无法使用它)。
戴蒙2014年

5
我什Element & e至 不会使用auto & e(或auto const &e)看起来更好。Element const e当我想要隐式转换时,例如当源是不同类型的集合,并且希望它们转换为时,我会使用(无参考)Element
纳瓦兹

53

原因如下:

  1. 似乎是因为您不习惯和/或没有使用正确的工具使其变得简单而导致可读性下降。(有关帮助程序,请参见boost :: range和boost :: bind / boost :: lambda。其中许多将进入C ++ 0x,并使for_each和相关函数更有用。)

  2. 它允许您在for_each之上编写可与任何迭代器一起使用的算法。

  3. 它减少了愚蠢的键入错误的机会。

  4. 它也打开你的心灵的STL的算法,休息一下就好了find_ifsortreplace等这些不会看起来那么奇怪了。这可能是一个巨大的胜利。

更新1:

最重要的是,它可以帮助您超越for_eachfor.loop之类的条件,并查看其他STL-alog,例如find / sort / partition / partition / copy_replace_if,并行执行..等等。

使用for_each兄弟姐妹的“其余”兄弟可以非常简洁地编写很多处理过程,但是如果您要做的是编写带有各种内部逻辑的for循环,那么您将永远不会学习如何使用它们,并且最终一遍又一遍地发明了轮子。

并且(即将可用的范围样式for_each):

for_each(monsters, boost::mem_fn(&Monster::think));

或使用C ++ x11 lambdas:

for_each(monsters, [](Monster& m) { m.think(); });

IMO比以下内容更具可读性:

for(Monsters::iterator i = monsters.begin(); i != monsters.end(); ++i) {
    i->think();
} 

另外这(或与lambdas,请参阅其他):

for_each(bananas, boost::bind(&Monkey::eat, my_monkey, _1));

比以下内容更简洁:

for(Bananas::iterator i = bananas.begin(); i != bananas.end(); ++i) {
    my_monkey->eat(*i);
} 

特别是如果您有几个要按顺序调用的函数...但是也许就是我。;)

更新2:我已经编写了自己的stl-算法单层包装器,该包装器可用于范围而不是一对迭代器。boost :: range_ex一旦发布,将包含该内容,也许它也将在C ++ 0x中存在?


+1,几个函数或嵌套类型:outer_class::inner_class::iterator或者他们是模板参数:typename std::vector<T>::iterator...在对结构本身能碰上本身是一个多线结构
大卫·罗德里格斯- dribeas

4
(顺便说一句:在for_each第二个例子是不正确的(应该是for_each( bananas.begin(), bananas.end(),...
大卫·罗德里格斯- dribeas

我编写了使用范围而不是两个迭代器的包装器。这些将在以后可用(请参阅range_ex),但是每个人都应该拥有它们。(由更新上。)
马科

1
没有并行处理支持。这里有1个原因。我们可以添加实现以使用cuda / gpu进行异构并行计算。
舒瓦

24

for_each更通用。您可以使用它来遍历任何类型的容器(通过传递begin / end迭代器)。您可以潜在地在使用的函数下换出容器,for_each而不必更新迭代代码。您需要考虑的是,除了std::vector普通的C数组之外,世界上还有其他容器才能看到的优势for_each

的主要缺点for_each是它需要一个函子,因此语法很笨拙。这在C ++ 11(以前为C ++ 0x)中通过引入lambda修复:

std::vector<int> container;
...
std::for_each(container.begin(), container.end(), [](int& i){
    i+= 10;
});

3年后,这对您来说似乎并不奇怪。


3
+1。当for_each接受一个容器/范围而不是两个迭代器时,它变得更强大了。
Macke 2010年

2
@Marcus:这将是介于换结构和本身的语法不会读“的for_each”: for ( int v : int_vector ) {(即使它今天可以模拟与BOOST_FOREACH)
大卫·罗德里格斯- dribeas

@David:我指的是基于范围的函数的一般添加(因此您可以将范围与所有这些for_each,copy,remove_if等函数一起使用),
Macke

1
为什么不可能写:std::for_each(container, [](int& i){ ... });。我的意思是为什么一个人被迫两次写容器?
乔治(Giorgio)2012年

1
@freitass:如我之前的评论中所述,一次编写容器可以默认为使用begin end迭代器,而无需显式调用它们。大多数在集合上提供高阶函数的语言(Ruby,Scala等)都编写了类似的内容,container.each { ... }而没有提及开始和结束迭代器。我发现必须一直指定结束迭代器有点多余。
Giorgio

17

就个人而言,任何时候我都需要竭尽全力地使用std::for_each(编写专用函子/ complex boost::lambdas),我就会发现BOOST_FOREACH并且基于C ++ 0x的范围更加清晰:

BOOST_FOREACH(Monster* m, monsters) {
     if (m->has_plan()) 
         m->act();
}

std::for_each(monsters.begin(), monsters.end(), 
  if_then(bind(&Monster::has_plan, _1), 
    bind(&Monster::act, _1)));

11

它非常主观,有人会说使用for_each 会使代码更具可读性,因为它允许使用相同的约定来对待不同的集合。 for_eachitslef实现为循环

template<class InputIterator, class Function>
  Function for_each(InputIterator first, InputIterator last, Function f)
  {
    for ( ; first!=last; ++first ) f(*first);
    return f;
  }

因此由您选择适合自己的内容。


11

像许多算法功能一样,最初的反应是认为使用foreach比循环更难读。这是许多火焰战争的主题。

一旦习惯了这种习语,您可能会发现它很有用。一个明显的优点是,它迫使编码器将循环的内部内容与实际的迭代功能分开。(好的,我认为这是一个优势。换句话说,您只是在切分代码而没有实际好处)。

另一个优点是,当我看到foreach时,我知道要么将处理每个项目,要么将引发异常。

一个用于循环允许用于终止循环的几个选项。您可以让循环运行整个过程,也可以使用break关键字显式跳出循环,或者使用return关键字退出整个函数中期循环。相反,foreach不允许使用这些选项,这使其更具可读性。您只需浏览一下函数名称,即可知道迭代的全部性质。

这是一个令人困惑的for循环的示例:

for(std::vector<widget>::iterator i = v.begin(); i != v.end(); ++i)
{
   /////////////////////////////////////////////////////////////////////
   // Imagine a page of code here by programmers who don't refactor
   ///////////////////////////////////////////////////////////////////////
   if(widget->Cost < calculatedAmountSofar)
   {
        break;
   }
   ////////////////////////////////////////////////////////////////////////
   // And then some more code added by a stressed out juniour developer
   // *#&$*)#$&#(#)$#(*$&#(&*^$#(*$#)($*#(&$^#($*&#)$(#&*$&#*$#*)$(#*
   /////////////////////////////////////////////////////////////////////////
   for(std::vector<widgetPart>::iterator ip = widget.GetParts().begin(); ip != widget.GetParts().end(); ++ip)
   {
      if(ip->IsBroken())
      {
         return false;
      }
   }
}

1
您提出了一个很好的观点,但您的动机示例并不完全公平。当您使用std::for_each()旧标准时(在撰写本文时),您必须使用一个命名函子,该函子可以像您所说的那样提高可读性,并禁止过早地打破循环。但是,等效for循环只不过是一个函数调用,因此也禁止过早中断。但是除此之外,我认为您在说std::for_each() 强制执行遍及整个范围方面非常有意思
wilhelmtell

10

您大体上是对的:大多数时候,std::for_each都是净亏损。我竟然比去for_eachgotogoto提供最通用的流控制-您可以使用它来实现您可以想象的几乎任何其他控制结构。这非常的多功能性,但是,意味着看到一个goto孤立告诉你几乎没有什么是它的预期在这种情况下做的。结果,goto除了万不得已之外,几乎没有人在他们的正确想法中使用过。

在标准算法中,for_each方法几乎是相同的-它可以用于实现几乎任何东西,这意味着for_each在这种情况下,视觉几乎无法告诉您任何用途。不幸的是,人们的态度for_each是关于goto(例如)1970年左右的态度的。一些人抓住了它只能作为不得已而为之的事实,但许多人仍然将其视为主要算法,并且很少使用其他任何东西。在绝大多数情况下,即使一目了然,也可以发现其中一种替代方案明显优越。

举例来说,我很确定自己已经迷失了多少次看到人们在编写代码以使用来打印出集合的内容for_each。根据我看过的帖子,这很可能是的最常见用法for_each。他们最终得到如下结果:

class XXX { 
// ...
public:
     std::ostream &print(std::ostream &os) { return os << "my data\n"; }
};

而他们的职位是问什么组合bind1stmem_fun等他们需要做出这样的:

std::vector<XXX> coll;

std::for_each(coll.begin(), coll.end(), XXX::print);

工作,并打印出的元素coll。如果它确实确实按照我在此处所写的那样工作,那将是中等水平,但事实并非如此-到您使它工作时,很难找到与代码相关的那几部分代码在将其结合在一起的碎片之间进行。

幸运的是,还有更好的方法。为XXX添加普通的流插入程序重载:

std::ostream &operator<<(std::ostream *os, XXX const &x) { 
   return x.print(os);
}

并使用std::copy

std::copy(coll.begin(), coll.end(), std::ostream_iterator<XXX>(std::cout, "\n"));

这不工作-并且需要在几乎所有没有工作弄清楚,它打印的内容collstd::cout


+1,不过有一个错误。在第一个示例中,它应该是,boost::mem_fn(&XXX::print)而不是XXX::print
missingfaktor,2010年

这就是为什么我说该示例不起作用,而他们发布的是寻求帮助以使其正常工作(哦,您还需要绑定std::cout它作为其工作的论据)。
杰里·科芬

1
尽管通常这是一个很好的答案,但问题不在于for_each的值或与其他std算法相比的值,而是与for循环相比其值。在没有其他标准算法适用的情况下,您可能会想到它。然后,您将使用for_each还是for循环?考虑一下,无论您想出什么,这都是您的答案。
Christian Rau

@ChristianRau:在完全按照要求回答问题与尝试提供有用的信息之间总是存在一条很好的界限。对他提出的问题的直接答案是“可能不是。谁知道?”,但那样太没用了,不值得打扰。同时,走得太远(例如,推荐Haskell而不是上述任何一种方法)也没有太大用处。
杰里·科芬,

3
@ChristianRau:您如何看待“ ...在大多数情况下,std :: for_each是净亏损”没有解决std :: for_each是否提供优势的问题?
杰里·科芬

8

编写函数以提高可读性的优势可能在for(...)和时不显现出来for_each(...

如果您使用functional.h中的所有算法,而不是使用for循环,则代码将更具可读性。

iterator longest_tree = std::max_element(forest.begin(), forest.end(), ...);
iterator first_leaf_tree = std::find_if(forest.begin(), forest.end(), ...);
std::transform(forest.begin(), forest.end(), firewood.begin(), ...);
std::for_each(forest.begin(), forest.end(), make_plywood);

多少超过可读;

Forest::iterator longest_tree = it.begin();
for (Forest::const_iterator it = forest.begin(); it != forest.end(); ++it{
   if (*it > *longest_tree) {
     longest_tree = it;
   }
}

Forest::iterator leaf_tree = it.begin();
for (Forest::const_iterator it = forest.begin(); it != forest.end(); ++it{
   if (it->type() == LEAF_TREE) {
     leaf_tree  = it;
     break;
   }
}

for (Forest::const_iterator it = forest.begin(), jt = firewood.begin(); 
     it != forest.end(); 
     it++, jt++) {
          *jt = boost::transformtowood(*it);
    }

for (Forest::const_iterator it = forest.begin(); it != forest.end(); ++it{
    std::makeplywood(*it);
}

这就是我认为的很好,将for循环概括为一个行函数=)


6

简便:for_each当您已经具有处理每个数组项的功能时很有用,因此您不必编写lambda。当然,这

for_each(a.begin(), a.end(), a_item_handler);

胜过

for(auto& item: a) {
    a_item_handler(a);
}

同样,远程for循环从头到尾仅遍历整个容器,同时for_each更加灵活。


4

所述for_each环路是指以隐藏来自用户代码的迭代(的环是如何实现的细节)并限定在操作清晰语义:每个元素将被正好一次迭代。

当前标准中可读性的问题在于,它需要一个仿函数作为最后一个参数,而不是代码块,因此在许多情况下,您必须为其编写特定的仿函数类型。由于函子对象无法就地定义(函数中定义的局部类不能用作模板参数),因此循环的代码变得可读性差,并且循环的实现必须脱离实际循环。

struct myfunctor {
   void operator()( int arg1 ) { code }
};
void apply( std::vector<int> const & v ) {
   // code
   std::for_each( v.begin(), v.end(), myfunctor() );
   // more code
}

请注意,如果要对每个对象执行特定操作,则可以使用std::mem_fn,或boost::bindstd::bind在下一个标准中)或boost::lambda(在下一个标准中的lambda)使其更简单:

void function( int value );
void apply( std::vector<X> const & v ) {
   // code
   std::for_each( v.begin(), v.end(), boost::bind( function, _1 ) );
   // code
}

如果您确实有要调用的功能/方法,那么它的可读性和手卷版本都不会比手卷版本紧凑。该实现可以提供for_each循环的其他实现(请考虑并行处理)。

即将发布的标准以不同的方式解决了一些缺点,它将允许本地定义的类作为模板的参数:

void apply( std::vector<int> const & v ) {
   // code
   struct myfunctor {
      void operator()( int ) { code }
   };
   std::for_each( v.begin(), v.end(), myfunctor() );
   // code
}

改善代码的本地性:浏览时,您会看到它在做什么。实际上,您甚至不需要使用类语法来定义函子,而只需在其中使用lambda即可:

void apply( std::vector<int> const & v ) {
   // code
   std::for_each( v.begin(), v.end(), 
      []( int ) { // code } );
   // code
}

即使对于的情况,for_each也会有一个特定的构造使它更自然:

void apply( std::vector<int> const & v ) {
   // code
   for ( int i : v ) {
      // code
   }
   // code
}

我倾向于将for_each构造与手摇循环混合使用。当我只需要调用一个现有函数或方法时(for_each( v.begin(), v.end(), boost::bind( &Type::update, _1 ) )),我就去for_each了从代码中删除很多样板迭代器内容的构造。当我需要更复杂的东西,而又不能在实际使用的上方几行处实现函子时,我将滚动自己的循环(将操作保留在适当的位置)。在代码的非关键部分,我可能会使用BOOST_FOREACH(同事让我参与其中)


3

除了可读性和性能之外,一致性经常被忽略。有多种方法可以通过以下方式在迭代器上实现for(或while)循环:

for (C::iterator iter = c.begin(); iter != c.end(); iter++) {
    do_something(*iter);
}

至:

C::iterator iter = c.begin();
C::iterator end = c.end();
while (iter != end) {
    do_something(*iter);
    ++iter;
}

在效率和潜在漏洞级别不同的情况下,有许多示例。

但是,使用for_each通过抽象化循环来增强一致性:

for_each(c.begin(), c.end(), do_something);

您现在唯一需要担心的是:您是否使用Boost或C ++ 0x功能将循环体实现为函数,函子或lambda?就个人而言,我宁愿担心这一点,而不是如何实现或读取随机的for / while循环。


3

我曾经不喜欢std::for_each并以为没有lambda,那是完全错误的。但是前段时间我确实改变了主意,现在我真的很喜欢它。而且我认为它甚至可以提高可读性,并使其更容易以TDD方式测试代码。

std::for_each算法可以被读取,就像所有元素都在range范围内一样可以提高可读性。假设您要执行的动作长20行,执行该动作的功能也大约20行长。这将使一个函数使用常规的for循环长40行,而使用则仅大约20行std::for_each,因此可能更容易理解。

的函std::for_each子更有可能更通用,因此可以重用,例如:

struct DeleteElement
{
    template <typename T>
    void operator()(const T *ptr)
    {
        delete ptr;
    }
};

而且在代码中,您将只有一个像std::for_each(v.begin(), v.end(), DeleteElement())这样的单行代码,这比显式循环好一些。

通常,所有这些函子都比在长函数中间使用显式的for循环更容易在单元测试中进行,而单单这对我来说已经是一个巨大的胜利。

std::for_each 通常也更可靠,因为您不太可能在范围上犯错误。

最后,std::for_each与某些类型的手工for循环相比,编译器可能会产生更好的代码,因为(for_each)对于编译器来说总是一样的,并且编译器作者可以投入所有知识,使其与他们一样好。能够。

同样适用于其他标准算法,例如find_iftransform等等。


2

for是用于可以迭代每个元素或每三个元素等等for_each的循环。仅用于迭代每个元素。从它的名字很明显。因此,您将更清楚地打算在代码中执行什么操作。


4
如果传递给它的迭代器各不超过3,则不是这样++。可能不寻常,但是for循环也是如此。
Potatoswatter 2010年

在这种情况下,最好transform不要混淆别人。
Kirill V. Lyadvinsky 2010年

2

如果您经常使用STL中的其他算法,则有以下几个优点for_each

  1. 与for循环相比,它通常更简单,更不容易出错,部分原因是您将习惯使用此接口,而部分原因是在许多情况下它实际上更加简洁。
  2. 尽管基于范围的for循环甚至可以更简单,但它的灵活性较差(正如Adrian McCarthy指出的那样,它在整个容器上进行迭代)。
  3. 与传统的for循环不同,它for_each迫使您编写适用于任何输入迭代器的代码。以这种方式受到限制实际上可能是一件好事,因为:

    1. 您实际上可能需要修改代码,以便以后在其他容器中工作。
    2. 开始时,它可能会教给您一些知识和/或使您的习惯变得更好。
    3. 即使您总是编写完全等效的for循环,其他修改相同代码的人也可能会在没有提示使用的情况下执行此操作for_each
  4. for_each有时使用显然可以使用更特定的STL函数来执行相同的操作。(就像杰里·科芬的例子一样;for_each最好的选择不一定是这种情况,但是for循环并不是唯一的选择。)


2

使用C ++ 11和两个简单​​的模板,您可以编写

        for ( auto x: range(v1+4,v1+6) ) {
                x*=2;
                cout<< x <<' ';
        }

作为替换for_each或循环。为什么选择它归结为简洁和安全起见,如果表达式不存在,就不会出错。

对我来说,for_each当循环主体已经是函子时,基于相同的理由总会更好,并且我将尽我所能获得的优势。

您仍然使用3表达式for,但是现在当您看到一个3表达式时,您知道那里需要理解的东西,不是样板。我讨厌样板。我很讨厌它的存在。这不是真正的代码,阅读本书没有什么可学的,只是需要检查的一件事。可以通过检查生锈的容易程度来衡量其精神努力。

模板是

template<typename iter>
struct range_ { 
                iter begin() {return __beg;}    iter end(){return __end;}
            range_(iter const&beg,iter const&end) : __beg(beg),__end(end) {}
            iter __beg, __end;
};

template<typename iter>
range_<iter> range(iter const &begin, iter const &end)
    { return range_<iter>(begin,end); }

1

通常,您必须遍历整个集合。因此,我建议您编写仅包含2个参数的for_each()变体。这将使您可以将Terry Mahaffey的示例重写为:

for_each(container, [](int& i) {
    i += 10;
});

我认为这确实比for循环更具可读性。但是,这需要C ++ 0x编译器扩展。


1

我发现for_each对可读性不利。这个概念很好,但是c ++使得很难编写可读性,至少对我而言。c ++ 0x lamda表达式将有所帮助。我真的很喜欢lamda。但是乍一看,我认为语法非常丑陋,我不是100%肯定会习惯。也许在5年后我会习惯它,而不是再三考虑,但也许不会。时间会证明:)

我更喜欢使用

vector<thing>::iterator istart = container.begin();
vector<thing>::iterator iend = container.end();
for(vector<thing>::iterator i = istart; i != iend; ++i) {
  // Do stuff
}

我发现一个显式的for循环清除器可以读取,并且显式地将命名变量用于start和end迭代器,从而减少了for循环中的混乱情况。

当然情况会有所不同,这正是我通常最能找到的。


0

您可以让迭代器成为对循环中每次迭代执行的函数的调用。

参见此处:http : //www.cplusplus.com/reference/algorithm/for_each/


2
仅链接的帖子无法提供很好的答案,无论如何,它在该链接的哪个位置显示类似于可调用迭代器的内容?我很确定这个概念没有任何意义。也许您只是概述了for_each做什么,在这种情况下,它没有回答有关其优势的问题。
underscore_d


0

for_each允许我们实现Fork-Join模式。除此之外,它还支持fluent-interface

叉连接模式

gpu::for_each通过在多个worker中调用lambda任务,我们可以添加实现以将cuda / gpu用于异构并行计算。

gpu::for_each(users.begin(),users.end(),update_summary);
// all summary is complete now
// go access the user-summary here.

并且gpu::for_each可以等待工人完成所有lambda任务的工作,然后再执行下一条语句。

流利的界面

它使我们能够以简洁的方式编写人类可读的代码。

accounts::erase(std::remove_if(accounts.begin(),accounts.end(),used_this_year));
std::for_each(accounts.begin(),accounts.end(),mark_dormant);
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.