36

コンテナーを反復処理する 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
    });
  }

std::for_eachforループと次の点の長所/短所は何ですか?

パフォーマンス?(違いはないと思います)

b) 可読性と保守性?

ここに の多くの欠点が見られfor_eachます。ループがそうする間、それはcスタイルの配列を受け入れません。ラムダ仮パラメータの宣言は非常に冗長であるため、autoそこでは使用できません。から抜け出すことはできませんfor_each

C++11 より前の時代forには、イテレータの型を指定する必要があること (これ以上は成り立たない) と、ループ条件のタイプを間違える可能性があること (私はこの 10 年間でそのような間違いをしたことがありません) に対する反対意見がありました。

結論として、私の考えfor_eachは一般的な意見と矛盾しています。ここで何が欠けていますか?

4

5 に答える 5

33

これまでの回答ではまだカバーされていない違いがいくつかあると思います。

  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. もう 1 つの違いは、ループからの脱却 (ループ内またはループ内) に関するものbreakです。私の知る限り、ループでは例外をスローすることによってのみこれを行うことができます。例えばreturnforfor_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); }
      };
    }
    

    このタイプの反復子は、たとえばforandで正しく機能しますfor_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);
    

    ただし、反復ごとに 2 ~ 3 回の比較と分岐が必要です。より効率的な方法は、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
    

    ほとんどの反復で 1 つの比較のみが必要であり、分岐はありません (分岐はパフォーマンスに悪影響を与える可能性があることに注意してください)。名前空間でこれを定義する必要はありませんがstd(これは違法である可能性があります)、適切なusingディレクティブによって正しいバージョンが使用されるようにすることができます。これは、特定のユーザー定義型using std::swap;に特化する場合と同じです。swap()

于 2012-08-15T08:30:22.497 に答える
7

パフォーマンスに関しては、forループはstd::end繰り返し呼び出しますが、そうでstd::for_eachはありません。これにより、使用するコンテナーに応じて、パフォーマンスの違いが生じる場合と生じない場合があります。

于 2012-08-14T16:20:29.087 に答える
4
  • std::for_eachバージョンは各要素を 1 回だけ訪問します。コードを読んでいる人std::for_eachは、ラムダでイテレータを台無しにするためにできることは何もないので、 が表示されるとすぐにそれを知ることができます。従来の for ループでは、通常とは異なる制御フロー ( continuebreakreturn) とイテレータ (たとえば、この場合は で次の要素をスキップする)についてループの本体を調べる必要があります++it

  • ラムダソリューションのアルゴリズムを簡単に変更できます。たとえば、n 番目の要素ごとにアクセスするアルゴリズムを作成できます。多くの場合、実際には for ループは必要ありませんが、 のような別のアルゴリズムが必要ですcopy_if。アルゴリズムとラムダを使用すると、多くの場合、変更しやすくなり、もう少し簡潔になります。

  • 反対に、プログラマーは従来の for ループに慣れているため、アルゴリズム + ラムダは読みにくいと感じるかもしれません。

于 2012-08-14T18:05:35.707 に答える
0

それはそう; Lambda 式を使用する場合は、パラメーターの型と名前を宣言する必要があるため、何も得られません。

しかし、これを使って 1 つの (名前付きの) 関数または関数オブジェクトを呼び出したいと思うとすぐに素晴らしいものになります。( を介して関数のようなものを組み合わせることができることを思い出してくださいstd::bind。)

Scott Meyers の本 ( Effective STLだったと思います) は、そのようなプログラミング スタイルを非常にわかりやすく説明しています。

于 2012-08-14T17:06:55.127 に答える
0

まず、for_each は for ループを使用して実装されているため、これら 2 つの違いはほとんどわかりません。ただし、for_each は戻り値を持つ関数であることに注意してください。

2 つ目は、いずれにせよこの日はすぐに来るので、この場合は範囲​​ループ構文を使用します。

于 2012-08-14T16:17:19.043 に答える