私が書いたほぼすべてのコードで、コレクション内のセット削減の問題に対処することが多く、最終的にはコレクション内の単純な「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++でこの好ましい手法を使用できるようにするための本当に基本的なものが欠けていると感じざるを得ません。誰かが私を啓発できますか?
私見では、内部に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; });
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;
}
}
受け入れられた答えのように、新しいアルゴリズムを作成する代わりに、条件を適用する関数を持つ既存のアルゴリズムを使用できます。
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); });
}
回避のアイデア
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);
}
本体コードが一度だけ実行されることは明らかではないため、これは読みにくいです。この場合、ルックアップを分離することをお勧めします。
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();
if
内にはまだfor
がありますが、コンテキストから、それが何をするかが明らかになり、ルックアップが変更されない限り(たとえばmap
に)このコードを変更する必要はなく、create_object()
ループ内にないため、一度だけ呼び出されます。
以下は、比較的最小限のfilter
関数です。
述語が必要です。 iterableをとる関数オブジェクトを返します。
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(:)
- qualifying pseudo-fascadeではなく、実際のイテレータを作成する必要があります。
使用時点では、次のようになります。
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には、上記の記述をはるかに短くするヘルパーもあります。
言及するのに十分に使用されているが、まだ言及されていないスタイルの1つは次のとおりです。
for(int i=0; i<myCollection.size(); i++) {
if (myCollection[i] != SOMETHING)
continue;
DoStuff();
}
利点:
DoStuff();
のインデントレベルを変更しません。論理的には、DoStuff();
はfor
ループの最上位にある必要があります。SOMETHING
ブロックの}
を閉じた後に読者が何も存在しないことを確認することなく、ループがコレクションのif
を反復処理することを即座に明らかにします。短所:
continue
は、他のフロー制御ステートメントと同様に、追跡が困難なコードにつながる方法で悪用されるため、一部の人々はanyを使用することに反対します。有効なスタイルがありますcontinue
を回避する、break
を回避する、switch
を回避する、関数の最後以外のreturn
を回避するコーディングの例.for(auto const &x: myCollection) if(x == something) doStuff();
C++固有のfor
理解のように見えます。あなたへ?
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();
また、コレクションの並べ替えを気にしない場合、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
}
上記のソリューションの複雑さにcomplexity敬の念を抱いています。単純な#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;
}
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()を実行します。今回は、必要に応じてdoStuff()の呼び出しで使用できるように、現在のiをバッファーに格納します。