C ++で「if」条件が含まれる「for」ループを回避するにはどうすればよいですか?


111

私が作成するほとんどすべてのコードを使用して、コレクション内の最終的な単純な "if"条件で終わるコレクションのセット削減問題に対処しています。以下に簡単な例を示します。

for(int i=0; i<myCollection.size(); i++)
{
     if (myCollection[i] == SOMETHING)
     {
           DoStuff();
     }
}

関数型言語では、コレクションを別のコレクションに(簡単に)削減して問題を解決し、削減したセットですべての操作を実行できます。疑似コード:

newCollection <- myCollection where <x=true
map DoStuff newCollection

そして、C#のような他のCバリアントでは、次のようなwhere句で減らすことができます

foreach (var x in myCollection.Where(c=> c == SOMETHING)) 
{
   DoStuff();
}

以上(少なくとも私の目には)

myCollection.Where(c=>c == Something).ToList().ForEach(d=> DoStuff(d));

確かに、私は多くのパラダイムミキシングと主観/意見ベースのスタイルを行っていますが、この推奨される手法をC ++で使用できるようにするための根本的な何かが欠けていると感じざるを得ません。誰かが私を啓発できますか?


7
C ++の標準ライブラリ機能の外で、試すことができますstd::copy_ifが、選択は怠惰ではありません
Milleniumbug

14
range-v3に興味があるかもしれません。また、TSとしてC ++で提供され、うまくいけば将来のリリースで標準化される予定です。
NathanOliver

12
あなたが言及しているif内部はfor、他の例と機能的に同等であるだけでなく、多くの場合はおそらくより高速であることを指摘する必要があると私は感じています。また、関数型のスタイルが好きだと主張する人にとって、あなたが宣伝していることは、関数型プログラミングの純粋に愛されているコンセプトにDoStuff反しているようです。
Pharap

60
すべてのロジックを1つの行にまとめてリンクすると、なぜか見栄えがよくなったり、読みやすくなったりする理由を理解できません。最上部にあるC ++スニペットは、すべての可能性の中で、断然最も読みやすいものです。また、効率は変更されないので、削除するコードの行数で支払われない限り、なぜそれを記述しない方がよいか理解できません。
コーディグレイ

10
@CodyGray同意:それは単なる構文上の砂糖です。それは非常に異なっているので、質問のタイトルは、誤解を招くさ回避の分岐と隠れて抽象化の下でそれを。
edmz

回答:


99

私見それの中にifでforループを使用する方がより簡単で読みやすいです。ただし、これが煩わしい場合はfor_each_if、以下のようなを使用できます。

template<typename Iter, typename Pred, typename Op> 
void for_each_if(Iter first, Iter last, Pred p, Op op) {
  while(first != last) {
    if (p(*first)) op(*first);
    ++first;
  }
}

使用事例:

std::vector<int> v {10, 2, 10, 3};
for_each_if(v.begin(), v.end(), [](int i){ return i > 5; }, [](int &i){ ++i; });

ライブデモ


10
それは非常に賢いです。また、これが単純ではないことにも同意します。他の人が使用するC ++をプログラミングするときにif条件を使用するだけでしょう。しかし、それはまさに私自身の個人的な使用に必要なものです!:)
Darkenor

14
@Defaultコンテナではなくイテレータのペアを渡すことは、柔軟性と慣用的なC ++の両方です。
マークB

8
@Slava、一般的な範囲では、アルゴリズムの数は減りません。たとえば、あなたはまだ必要find_iffind彼らはイテレータの範囲またはペア上で動作するかどうか。(for_eachおよびなどのいくつかの例外がありますfor_each_n)。くしゃみごとに新しいアルゴを作成しないようにする方法は、既存のアルゴでさまざまな操作を使用することです。たとえばfor_each_if、条件をに渡される呼び出し可能オブジェクトに埋め込むのではなくfor_each、たとえばfor_each(first, last, [&](auto& x) { if (cond(x)) f(x); });
Jonathan Wakely

9
最初の文に同意する必要があります。標準のfor-ifソリューションの方がはるかに読みやすく、扱いやすいです。ラムダ構文と、単純なループを処理するためだけに別の場所で定義されたテンプレートの使用は、他の開発者を苛立たせたり混乱させる可能性があると思います。局所性とパフォーマンスを犠牲にしています...何ですか?何かを一行で書けるの?
user1354557 2016

45
@Darkenor、一般的に「非常に賢い」プログラミング、将来の自分を含む他のすべての人のがらくたを困らせるので、避けるべきです。
ライアン

48

Boostは、範囲ベースで使用できる範囲を提供します。範囲は、利点は、彼らが基礎となるデータ構造をコピーしないことを、彼らは単に(ある、「ビュー」を提供していbegin()end()範囲のためにとoperator++()operator==()イテレータのため)。これはあなたの興味かもしれません:http : //www.boost.org/libs/range/doc/html/range/reference/adaptors/reference/filtered.html

#include <boost/range/adaptor/filtered.hpp>
#include <iostream>
#include <vector>

struct is_even
{
    bool operator()( int x ) const { return x % 2 == 0; }
};

int main(int argc, const char* argv[])
{
    using namespace boost::adaptors;

    std::vector<int> myCollection{1,2,3,4,5,6,7,8,9};

    for( int i: myCollection | filtered( is_even() ) )
    {
        std::cout << i;
    }
}

1
is_even=> conditioninput=> myCollectionなどの代わりにOPの例を使用することをお勧めします
デフォルトの

これはかなり優れた答えであり、間違いなく私がやろうとしていることです。だれかが遅延/据え置き実行を使用する標準準拠の方法を思いつくことができない限り、私は受け入れることを控えます。賛成。
Darkenor

5
@Darkenor:Boostが問題である場合(たとえば、会社のポリシーとマネージャーの知恵により、Boostの使用が禁止されている場合)は、簡単な定義を考え出すことができfiltered()ます。一部のアドホックコードよりもサポートされているlib。
lorro

完全に同意します。質問がブーストライブラリではなくC ++自体に向けられたため、最初に標準に準拠した方法が採用されたため、私はそれを受け入れました。しかし、これは本当に素晴らしいです。また-はい、私は不条理な理由でブーストを禁止した多くの場所で悲しいことに働きました...
Darkenor

@LeeClagett:?。
lorro

44

新しいアルゴリズムを作成する代わりに、受け入れられた回答のように、条件を適用する関数で既存のアルゴリズムを使用できます。

std::for_each(first, last, [](auto&& x){ if (cond(x)) { ... } });

または、本当に新しいアルゴリズムが必要な場合はfor_each、反復ロジックを複製する代わりに、少なくともそこで再利用します。

template<typename Iter, typename Pred, typename Op> 
  void
  for_each_if(Iter first, Iter last, Pred p, Op op) {
    std::for_each(first, last, [&](auto& x) { if (p(x)) op(x); });
  }

標準ライブラリを使用する方がはるかに良くて明確です。
匿名

4
そのためstd::for-each(first, last, [&](auto& x) {if (p(x)) op(x); });より完全に簡単ですかfor (Iter x = first; x != last; x++) if (p(x)) op(x);}
user253751

2
@immibisが標準ライブラリを再利用すると、イテレータの有効性チェックや、(C ++ 17では)引数を1つ追加するだけで、並列化がはるかに簡単になるなど、他の利点があり std::for_each(std::execution::par, first, last, ...);ます。
ジョナサンウェイクリー2016

1
#pragma omp parallel for
Mark K Cowan

2
@mark申し訳ありませんが、ソースコードまたはビルドチェーンのランダムな奇妙な動作により、厄介なほどに脆弱な並列非標準コンパイラー拡張が、診断なしでパフォーマンスの向上をゼロにしました。
Yakk-Adam Nevraumont 2016

21

回避するという考え

for(...)
    if(...)

アンチパターンとしての構成は広すぎます。

ループ内から特定の式に一致する複数の項目を処理することは完全に問題なく、コードはそれよりはるかに明確にすることはできません。処理が大きくなりすぎて画面に収まらない場合は、それがサブルーチンを使用する理由ですが、条件はループ内に配置するのが最適です。

for(...)
    if(...)
        do_process(...);

非常に好ましい

for(...)
    maybe_process(...);

1つの要素のみが一致する場合はアンチパターンになります。これは、最初に要素を検索し、ループの外で処理を実行する方が明確になるためです。

for(int i = 0; i < size; ++i)
    if(i == 5)

これは極端で明白な例です。より微妙な、したがってより一般的なのは、次のようなファクトリパターンです。

for(creator &c : creators)
    if(c.name == requested_name)
    {
        unique_ptr<object> obj = c.create_object();
        obj.owner = this;
        return std::move(obj);
    }

本体コードが1回だけ実行されるかどうかは明らかではないため、これは読みにくいです。この場合、ルックアップを分離する方が良いでしょう。

creator &lookup(string const &requested_name)
{
    for(creator &c : creators)
        if(c.name == requested_name)
            return c;
}

creator &c = lookup(requested_name);
unique_ptr obj = c.create_object();

まだあるiffor、それはそれが何をするか明確になる文脈から、検索変化(例えばにしない限り、このコードを変更する必要はありませんmap)、そしてすぐに明らかであるcreate_object()ことがあるので、一度だけ呼び出されますループ内ではありません。


私はこれが、ある意味で提起された質問に答えることを拒否したとしても、思慮深くバランスの取れた概要として好きです。私はそれを見つけるfor( range ){ if( condition ){ action } }スタイルは、それが簡単に一度のものに1つのチャンクを読み取ることが可能とのみ、基本的な言語構造の知識を使用しています。
PJTraill 2016

@PJTraill、質問の言い回しは、貨物栽培されており、どういうわけか絶対的なものになったfor-ifアンチパターンに対するレイモンドチェンの怒りを思い出させました。これfor(...) if(...) { ... }がしばしば最良の選択であることに完全に同意します(そのため、アクションをサブルーチンに分割するという推奨を修飾しました)。
Simon Richter

1
明確にしてくれたリンクに感謝します。「for-if」という名前は誤解を招くものであり、「for-all-if-one」や「lookup-avoidance」のような名前にする必要があります。それは道のことを思い出す抽象反転がで記述された2005年にウィキペディア「1がときのように、複合体の上に簡単な構造を作成します(もの)」 -私はそれを書き直しまで!実際にfor(…)if(…)…、それが唯一の場所検索が行われた場合のlookup-process-exitフォームを修正することを急いではいません。
PJTraill

17

これは、比較的最小限のfilter関数です。

述語が必要です。イテラブルを取る関数オブジェクトを返します。

で使用できるイテラブルを返します for(:)ループます。

template<class It>
struct range_t {
  It b, e;
  It begin() const { return b; }
  It end() const { return e; }
  bool empty() const { return begin()==end(); }
};
template<class It>
range_t<It> range( It b, It e ) { return {std::move(b), std::move(e)}; }

template<class It, class F>
struct filter_helper:range_t<It> {
  F f;
  void advance() {
    while(true) {
      (range_t<It>&)*this = range( std::next(this->begin()), this->end() );
      if (this->empty())
        return;
      if (f(*this->begin()))
        return;
    }
  }
  filter_helper(range_t<It> r, F fin):
    range_t<It>(r), f(std::move(fin))
  {
      while(true)
      {
          if (this->empty()) return;
          if (f(*this->begin())) return;
          (range_t<It>&)*this = range( std::next(this->begin()), this->end() );
      }
  }
};

template<class It, class F>
struct filter_psuedo_iterator {
  using iterator_category=std::input_iterator_tag;
  filter_helper<It, F>* helper = nullptr;
  bool m_is_end = true;
  bool is_end() const {
    return m_is_end || !helper || helper->empty();
  }

  void operator++() {
    helper->advance();
  }
  typename std::iterator_traits<It>::reference
  operator*() const {
    return *(helper->begin());
  }
  It base() const {
      if (!helper) return {};
      if (is_end()) return helper->end();
      return helper->begin();
  }
  friend bool operator==(filter_psuedo_iterator const& lhs, filter_psuedo_iterator const& rhs) {
    if (lhs.is_end() && rhs.is_end()) return true;
    if (lhs.is_end() || rhs.is_end()) return false;
    return lhs.helper->begin() == rhs.helper->begin();
  }
  friend bool operator!=(filter_psuedo_iterator const& lhs, filter_psuedo_iterator const& rhs) {
    return !(lhs==rhs);
  }
};
template<class It, class F>
struct filter_range:
  private filter_helper<It, F>,
  range_t<filter_psuedo_iterator<It, F>>
{
  using helper=filter_helper<It, F>;
  using range=range_t<filter_psuedo_iterator<It, F>>;

  using range::begin; using range::end; using range::empty;

  filter_range( range_t<It> r, F f ):
    helper{{r}, std::forward<F>(f)},
    range{ {this, false}, {this, true} }
  {}
};

template<class F>
auto filter( F&& f ) {
    return [f=std::forward<F>(f)](auto&& r)
    {
        using std::begin; using std::end;
        using iterator = decltype(begin(r));
        return filter_range<iterator, std::decay_t<decltype(f)>>{
            range(begin(r), end(r)), f
        };
    };
};

近道をしました。実際のライブラリーは、for(:)私が作成した、適格な擬似ファサードではなく、実際のイテレーターを作成する必要があります。

使用時点では、次のようになります。

int main()
{
  std::vector<int> test = {1,2,3,4,5};
  for( auto i: filter([](auto x){return x%2;})( test ) )
    std::cout << i << '\n';
}

これはかなりいいですし、印刷します

1
3
5

実例

この種のことを行うRangesv3と呼ばれるC ++への追加が提案されています。 boost利用可能なフィルター範囲/イテレーターもあります。boostには、上記の記述をはるかに短くするヘルパーもあります。


15

言及するのに十分に使用されているが、まだ言及されていないスタイルの1つは、次のとおりです。

for(int i=0; i<myCollection.size(); i++) {
  if (myCollection[i] != SOMETHING)
    continue;

  DoStuff();
}

利点:

  • DoStuff();条件の複雑さが増したときにインデントレベルを変更しません。論理的にDoStuff();は、forループのあります。
  • すぐにそれがクリアになり、その超えるループが繰り返さSOMETHINGコレクションのS、クロージング後には何もないことを確認するために、リーダーを必要としない}ifブロック。
  • ライブラリ、ヘルパーマクロ、関数は必要ありません。

短所:

  • continue、他のフロー制御文のように、そんなに何人かの人々がに反対していることをハード・ツー・フォローコードにつながる方法で悪用されます任意のそれらの使用:ことを回避するにはことをいくつかのフォローというコーディングの有効なスタイルがありcontinue、回避break以外は、で、関数の最後以外でswitch回避returnします。

3
私はfor、多くの行にまたがるループでは、2行の「そうでない場合は続行する」がはるかに明確で論理的で読みやすいと主張します。すぐに言って、forステートメントの後の「これをスキップする」はよく読み、あなたが言ったように、ループの残りの機能面をインデントしません。場合はcontinue、さらにダウンしている(一部の操作は常に前に実行されますつまり場合は、しかし、いくつかの明確さが犠牲になるifの文)。
匿名の

11
for(auto const &x: myCollection) if(x == something) doStuff();

for私にはC ++固有の理解のように見えます。あなたへ?


autoキーワードがc ++ 11より前に存在したとは思わないので、非常に古典的なc ++であるとは言えません。私がコメントでここで質問をする場合、「auto const」はコンパイラーにすべての要素を必要に応じて再配置できることを伝えますか?もしそうなら、コンパイラが分岐を避けるように計画する方が簡単かもしれません。
mathreadler 2016

1
@mathreadler「古典的なc ++」について心配するのをやめる人は早いほど良い。C ++ 11は言語の大進化イベントであり、5歳です。これは、私たちが努力する最低限のことです。とにかく、OPはそれとC ++ 14にタグを付けました(さらに良い!)。いいえ、auto const反復順序にはまったく関係ありません。ranged-basedを検索するとfor、基本的に暗黙的な逆参照begin()を使用して標準ループから標準ループが実行されていることがわかりますend()。反復されるコンテナの順序の保証(ある場合)を破る可能性はありません。それは地球の表面から笑われたでしょう
underscore_d

1
@mathreadler、実際にはそうでしたが、まったく異なる意味を持っていました。存在しなかったのはrange-for ...とその他の明確なC ++ 11機能です。ここで私が意味したのは、range-fors、std::futures、std::functions、それらの匿名のクロージャでさえ、構文がC ++に非常によく似ているということです。すべての言語には独自の専門用語があり、新しい機能を組み込むとき、それらは古いよく知られた構文を模倣しようとします。
bipll

@ underscore_d、as-ifルールに従えば、コンパイラーは変換を実行できますね。
bipll

1
うーん、それはどういう意味ですか?
bipll

7

DoStuff()が将来的に何らかの形でiに依存するようになる場合、この保証されたブランチフリーのビットマスキングバリアントを提案します。

unsigned int times = 0;
const int kSize = sizeof(unsigned int)*8;
for(int i = 0; i < myCollection.size()/kSize; i++){
  unsigned int mask = 0;
  for (int j = 0; j<kSize; j++){
    mask |= (myCollection[i*kSize+j]==SOMETHING) << j;
  }
  times+=popcount(mask);
}

for(int i=0;i<times;i++)
   DoStuff();

ここで、popcountは、母集団のカウントを行う任意の関数です(ビットのカウント数= 1)。iとその近傍に、より高度な制約を課す自由度があります。それが必要なければ、内側のループを取り除いて外側のループを作り直すことができます

for(int i = 0; i < myCollection.size(); i++)
  times += (myCollection[i]==SOMETHING);

続いて

for(int i=0;i<times;i++)
   DoStuff();

6

また、コレクションの並べ替えを気にしない場合は、std :: partitionが安価です。

#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>

void DoStuff(int i)
{
    std::cout << i << '\n';
}

int main()
{
    using namespace std::placeholders;

    std::vector<int> v {1, 2, 5, 0, 9, 5, 5};
    const int SOMETHING = 5;

    std::for_each(v.begin(),
                  std::partition(v.begin(), v.end(),
                                 std::bind(std::equal_to<int> {}, _1, SOMETHING)), // some condition
                  DoStuff); // action
}

しかしstd::partition、コンテナを再注文します。
celtschk

5

上記のソリューションの複雑さに私は畏敬の念を抱いています。シンプルな方法を提案しました#define foreach(a,b,c,d) for(a; b; c)if(d)が、いくつかの明らかな欠点があります。たとえば、ループではセミコロンの代わりにカンマを使用する必要があり、aまたはでカンマ演算子を使用できませんc

#include <list>
#include <iostream>

using namespace std; 

#define foreach(a,b,c,d) for(a; b; c)if(d)

int main(){
  list<int> a;

  for(int i=0; i<10; i++)
    a.push_back(i);

  for(auto i=a.begin(); i!=a.end(); i++)
    if((*i)&1)
      cout << *i << ' ';
  cout << endl;

  foreach(auto i=a.begin(), i!=a.end(), i++, (*i)&1)
    cout << *i << ' ';
  cout << endl;

  return 0;
}

3
一部の回答は、最初に再利用可能なジェネリックメソッド(一度だけ実行する)を示し、次にそれを使用するため、非常に複雑です。アプリケーション全体でif条件のループが1つある場合は効果的ではありませんが、1000回発生する場合は非常に効果的です。
gnasher729 2016

1
ほとんどの提案と同様に、これにより範囲と選択条件を特定することが難しくなり、簡単ではなくなります。また、マクロを使用すると、たとえ驚きがない場合でも、式が評価される時期(および頻度)に関する不確実性が高まります。
PJTraill

2

i:sが重要な場合の別の解決策。これは、doStuff()を呼び出すインデックスを埋めるリストを作成します。繰り返しますが、主なポイントは、分岐を回避し、パイプライン化可能な演算コストと交換することです。

int buffer[someSafeSize];
int cnt = 0; // counter to keep track where we are in list.
for( int i = 0; i < container.size(); i++ ){
   int lDecision = (container[i] == SOMETHING);
   buffer[cnt] = lDecision*i + (1-lDecision)*buffer[cnt];
   cnt += lDecision;
}

for( int i=0; i<cnt; i++ )
   doStuff(buffer[i]); // now we could pass the index or a pointer as an argument.

「魔法の」行は、値を保持して定位置に留まるか、位置をカウントして値を追加するかを算術的に計算するバッファ読み込み行です。したがって、潜在的な分岐をいくつかのロジックと算術、そしておそらくいくつかのキャッシュヒットと交換します。これが役立つ典型的なシナリオは、doStuff()が少量のパイプライン化可能な計算を行い、呼び出し間の分岐がこれらのパイプラインに割り込む可能性がある場合です。

次に、バッファをループして、cntに到達するまでdoStuff()を実行します。今回は、現在のiをバッファに格納するため、必要に応じてdoStuff()の呼び出しで使用できます。


1

コードパターンは、範囲のサブセットに関数を適用する、つまり、範囲全体にフィルターを適用した結果に関数を適用すると説明できます。

これは、Eric Neiblerのranges-v3ライブラリを使用すると、最も簡単な方法で実現できます。インデックスを操作したいので、少し面倒ですが:

using namespace ranges;
auto mycollection_has_something = 
    [&](std::size_t i) { return myCollection[i] == SOMETHING };
auto filtered_view = 
    views::iota(std::size_t{0}, myCollection.size()) | 
    views::filter(mycollection_has_something);
for (auto i : filtered_view) { DoStuff(); }

しかし、インデックスを省略しても構わないとしたら、次のようになります。

auto is_something = [&SOMETHING](const decltype(SOMETHING)& x) { return x == SOMETHING };
auto filtered_collection = myCollection | views::filter(is_something);
for (const auto& x : filtered_collection) { DoStuff(); }

これはより良い私見です。

PS-範囲ライブラリは、主にC ++ 20のC ++標準に組み込まれています。


0

私はマイク・アクトンに言及します、彼は間違いなく言うでしょう:

これを行う必要がある場合は、データに問題があります。データを並べ替える!

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.