【问题标题】:for loop vs std::for_each with lambda [duplicate]for循环与带有lambda的std :: for_each [重复]
【发布时间】:2012-08-10 23:38:35
【问题描述】:

让我们考虑一个用 C++11 编写的模板函数,它迭代一个容器。 请排除范围循环语法,因为我正在使用的编译器尚不支持它。

template <typename Container>
void DoSomething(const Container& i_container)
  {
  // Option #1
  for (auto it = std::begin(i_container); it != std::end(i_container); ++it)
    {
    // do something with *it
    }

  // Option #2
  std::for_each(std::begin(i_container), std::end(i_container), 
    [] (typename Container::const_reference element)
    {
    // do something with element
    });
  }

for 循环与std::for_each 在以下方面的优缺点是什么:

a) 性能? (我不希望有任何区别)

b) 可读性和可维护性?

在这里我看到for_each 的许多缺点。它不会接受 c 风格的数组,而循环会。 lambda 形式参数的声明非常冗长,无法在此处使用auto。无法突破for_each

在 C++11 之前,反对 for 的论点是需要指定迭代器的类型(不再保留),并且很容易错误输入循环条件(我从来没有这样做过10 年的错误)。

作为结论,我对for_each 的看法与普遍看法相矛盾。我在这里错过了什么?

【问题讨论】:

  • std::for_each 确实适用于 c 样式数组。
  • @juanchopanza:typename Container::const_reference 不会。 OP 应该改用std::iterator_traits&lt;&gt;::const_reference
  • @Andrey:你不能在 lambda 参数中使用auto,但你当然可以在函数体中使用typedef,以使 lambda 更易于阅读。
  • for ( Container::const_reference r : container ) {
  • 不会像 decltype(*begin(i_container)) 对 lambda 案例有帮助吗?

标签: c++ lambda c++11


【解决方案1】:

我认为到目前为止,答案还没有涵盖其他一些差异。

  1. a for_each 可以接受任何适当的可调用对象,允许为不同的 for 循环“回收”循环体。例如(伪代码)

    for( range_1 ) { lengthy_loop_body }    // many lines of code
    for( range_2 ) { lengthy_loop_body }    // the same many lines of code again
    

    变成

    auto loop_body = some_lambda;           // many lines of code here only
    std::for_each( range_1 , loop_body );   // a single line of code
    std::for_each( range_2 , loop_body );   // another single line of code
    

    从而避免重复并简化代码维护。 (当然,在有趣的风格组合中,我们也可以使用类似的方法来处理for 循环。)

  2. 另一个区别在于跳出循环(for 循环中的breakreturn)。据我所知,在for_each 循环中,这只能通过抛出异常来完成。例如

    for( range )
    {
      some code;
      if(condition_1) return x; // or break
      more code;
      if(condition_2) continue;
      yet more code;
    }
    

    变成

    try {
      std::for_each( range , [] (const_reference x)
                    {
                      some code;
                      if(condition_1) throw x;
                      more code;
                      if(condition_2) return;
                      yet more code;
                    } );
    } catch(const_reference r) { return r; }
    

    对于具有循环体和函数体(循环周围)范围的对象调用析构函数具有相同的效果。

  3. for_each 的主要好处是,恕我直言,当普通迭代效率不高时,可以为某些容器类型重载它。例如,考虑一个包含数据块链表的容器,每个块包含一个连续的元素数组,类似于(省略不相关的代码)

    namespace my {
      template<typename data_type, unsigned block_size>
      struct Container
      {
        struct block
        {
          const block*NEXT;
          data_type DATA[block_size];
          block() : NEXT(0) {}
        } *HEAD;
      };
    }
    

    那么对于这种类型的适当前向迭代器将需要在每次递增时检查块的结尾,并且比较运算符需要比较块指针和每个块内的索引(省略不相关的代码):

    namespace my {
      template<typename data_type, unsigned block_size>
      struct Container
      {
        struct iterator
        {
          const block*B;
          unsigned I;
          iterator() = default;
          iterator&operator=(iterator const&) = default;
          iterator(const block*b, unsigned i) : B(b), I(i) {}
          iterator& operator++()
          {
            if(++I==block_size) { B=B->NEXT; I=0; }    // one comparison and branch
            return*this;
          }
          bool operator==(const iterator&i) const
          { return B==i.B && I==i.I; }                 // one or two comparisons
          bool operator!=(const iterator&i) const
          { return B!=i.B || I!=i.I; }                 // one or two comparisons
          const data_type& operator*() const
          { return B->DATA[I]; }
        };
        iterator begin() const
        { return iterator(HEAD,0); }
        iterator end() const
        { return iterator(0,0); }
      };
    }
    

    这种类型的迭代器可以与forfor_each 一起正常工作,例如

    my::Container<int,5> C;
    for(auto i=C.begin();
        i!=C.end();              // one or two comparisons here
        ++i)                     // one comparison here and a branch
      f(*i);
    

    但每次迭代需要两到三个比较以及一个分支。更有效的方法是重载for_each()函数,分别循环块指针和索引:

    namespace my {
      template<typename data_type, int block_size, typename FuncOfDataType>
      FuncOfDataType&&
      for_each(typename my::Container<data_type,block_size>::iterator i,
               typename my::Container<data_type,block_size>::iterator const&e,
               FuncOfDataType f)
      {
        for(; i.B != e.B; i.B++,i.I=0)
          for(; i.I != block_size; i.I++)
            f(*i);
        for(; i.I != e.I; i.I++)
          f(*i);
        return std::move(f);
      }
    }
    using my::for_each;     // ensures that the appropriate
    using std::for_each;    // version of for_each() is used
    

    大多数迭代只需要一次比较并且没有分支(请注意,分支可能会对性能产生严重影响)。请注意,我们不需要在命名空间std 中定义它(这可能是非法的),但可以确保适当的using 指令使用正确的版本。当特化 swap() 用于某些用户定义的类型时,这等效于 using std::swap;

【讨论】:

  • 这很可爱 :) 所以break 变成了throwcontinue 变成了return
  • @Fiktik 我补充了一点,恕我直言,这是最重要的
  • 在命名空间std中添加函数重载是否合法?我忘记了这个标准是怎么说的
  • @haohaolee 好点。实际上没有必要。我修改了答案。
  • 可以给namespace std添加模板特化
【解决方案2】:

关于性能,您的for 循环反复调用std::end,而std::for_each 不会。这可能会也可能不会导致性能差异,具体取决于所使用的容器。

【讨论】:

  • 好点。但这对于任何标准容器来说都是一个问题吗?
  • @Andrey:可能不是(IIRC 恒定复杂性end 函数是所有 STL 容器的要求,尽管我不完全确定较新的容器)。但是,它们通常不是无操作的,因此可以通过非常简单的循环体来衡量差异。
  • @Andrey 但是,您的代码不必这样做。您可以在循环外声明和初始化迭代器。
  • 我不认为这个答案是正确的,但也许引用标准会使其更有效。
  • @Klaim,你为什么这么认为?您是否认为for 不会重复计算中间表达式,或者for_each 会出于某种原因调用end(尽管它不知道应该调用它的容器)?
【解决方案3】:
  • std::for_each 版本将只访问每个元素一次。阅读代码的人一看到std::for_each 就会知道,因为在 lambda 中没有什么可以做的事情来弄乱迭代器。在传统的 for 循环中,您必须研究循环体中的异常控制流(continuebreakreturn)并使用迭代器(例如,在这种情况下,使用 @ 跳过下一个元素987654327@)。

  • 您可以简单地更改 lambda 解决方案中的算法。例如,您可以创建一个访问每个第 n 个元素的算法。在许多情况下,你并不真正想要一个 for 循环,而是一个不同的算法,如 copy_if。使用算法+lambda,通常更易于更改并且更简洁。

  • 另一方面,程序员更习惯于传统的 for 循环,因此他们可能会发现算法+lambda 更难阅读。

【讨论】:

  • 你可以简单地改变 lambda 解决方案中的算法你能提供一个支持这个说法的例子吗?一种在 for_each 中可以轻松更改,但在 for 循环中却不是微不足道的更改?
  • @Andrey:我确实给出了几个正确的答案。我相信 Herb Sutter 在这次演讲中给出了更多的例子:vimeo.com/23975522
  • “算法”是指循环部分,而不是循环体。例如,将 for_each 替换为 parallel_for_each。
【解决方案4】:

首先,我看不出这两者之间有多大区别,因为 for_each 是使用 for 循环实现的。但请注意,for_each 是一个有返回值的函数。

其次,我将在这种情况下使用范围循环语法,因为无论如何这一天很快就会到来。

【讨论】:

  • C 样式数组的问题在于 lambda 的声明。该数组没有typename Container::const_reference
  • 对不起,看了一眼,我明白你的意思了......
  • 我想你可以用decltype(*std::begin(i_container)) 代替typename Container::const_reference
【解决方案5】:

确实;在使用 Lambda 表达式的情况下,您必须声明参数类型和名称,因此没有任何收获。

但只要你想用 this 调用一个(命名的)函数或函数对象,它就会很棒。 (请记住,您可以通过std::bind 组合类似函数的东西。)

Scott Meyers 的书(我相信是Effective STL)非常清楚地描述了这种编程风格。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-12-20
    • 1970-01-01
    • 2013-01-25
    • 1970-01-01
    • 1970-01-01
    • 2011-06-12
    • 2014-03-23
    • 1970-01-01
    相关资源
    最近更新 更多