W przypadku prawie całego kodu, który piszę, często mam do czynienia z problemami z redukcją zestawów na kolekcjach, które ostatecznie kończą się naiwnymi warunkami „jeśli” w nich. Oto prosty przykład:
for(int i=0; i<myCollection.size(); i++)
{
if (myCollection[i] == SOMETHING)
{
DoStuff();
}
}
Dzięki językom funkcjonalnym mogę rozwiązać problem, redukując zbiór do innego zbioru (łatwo), a następnie wykonując wszystkie operacje na moim zredukowanym zestawie. W pseudokodzie:
newCollection <- myCollection where <x=true
map DoStuff newCollection
A w innych wariantach języka C, takich jak C #, mógłbym zredukować za pomocą klauzuli where, takiej jak
foreach (var x in myCollection.Where(c=> c == SOMETHING))
{
DoStuff();
}
Albo lepiej (przynajmniej dla moich oczu)
myCollection.Where(c=>c == Something).ToList().ForEach(d=> DoStuff(d));
Wprawdzie robię dużo mieszania paradygmatów i stylu opartego na subiektywnych / opiniach, ale nie mogę się powstrzymać od poczucia, że brakuje mi czegoś naprawdę podstawowego, co pozwoliłoby mi użyć tej preferowanej techniki w C ++. Czy ktoś mógłby mnie oświecić?
std::copy_if
, ale wybory nie są leniweif
wnętrze, ofor
którym wspominasz, jest nie tylko funkcjonalnie równoważne z innymi przykładami, ale prawdopodobnie byłoby również szybsze w wielu przypadkach. Również dla kogoś, kto twierdzi, że lubi styl funkcjonalny, to, coDoStuff
promujesz, wydaje się być sprzeczne z ukochaną koncepcją czystości programowania funkcjonalnego, ponieważ wyraźnie ma efekty uboczne.Odpowiedzi:
IMHO jest prostsze i bardziej czytelne w użyciu pętli for z symbolem if w środku. Jeśli jednak jest to dla Ciebie irytujące, możesz użyć
for_each_if
poniższego:Przypadek użycia:
Live Demo
źródło
find_if
ifind
czy działają one na zakresach lub parach iteratorów. (Jest kilka wyjątków, takich jakfor_each
ifor_each_n
). Sposobem na uniknięcie pisania nowych algorytmów dla każdego kichnięcia jest użycie różnych operacji z istniejącymi algosami, np. Zamiastfor_each_if
osadzania warunku w elemencie wywoływanym przekazanymfor_each
np.for_each(first, last, [&](auto& x) { if (cond(x)) f(x); });
Zwiększenie zapewnia zakresy, których można używać w oparciu o zakres. Zakresy mają tę zaletę, że nie kopiować leżących u podstaw struktury danych, ale jedynie dostarczenie „Widok” (czyli
begin()
,end()
dla zakresu ioperator++()
,operator==()
na iteracyjnej). To może Cię zainteresować: http://www.boost.org/libs/range/doc/html/range/reference/adaptors/reference/filtered.htmlźródło
is_even
=>condition
,input
=>myCollection
Itp.filtered()
dla Ciebie uproszczoną definicję - to powiedziawszy, lepiej użyć obsługiwana biblioteka niż jakiś kod ad-hoc.Zamiast tworzyć nowy algorytm, jak to się dzieje w przypadku zaakceptowanej odpowiedzi, możesz użyć istniejącego z funkcją spełniającą warunek:
Lub jeśli naprawdę potrzebujesz nowego algorytmu, przynajmniej użyj go ponownie
for_each
zamiast powielać logikę iteracji:źródło
std::for-each(first, last, [&](auto& x) {if (p(x)) op(x); });
jest całkowicie prostsze niżfor (Iter x = first; x != last; x++) if (p(x)) op(x);}
?std::for_each(std::execution::par, first, last, ...);
Jak łatwo jest dodać te rzeczy do odręcznej pętli?Idea unikania
konstrukty jako anty-wzór jest zbyt szeroki.
Przetwarzanie wielu elementów, które pasują do określonego wyrażenia z wnętrza pętli, jest całkowicie w porządku, a kod nie może być znacznie bardziej przejrzysty. Jeśli przetwarzanie staje się zbyt duże, aby zmieścić się na ekranie, jest to dobry powód, aby użyć podprogramu, ale nadal warunek najlepiej umieścić wewnątrz pętli, tj.
jest znacznie lepsze niż
Staje się antywzorem, gdy tylko jeden element będzie pasował, ponieważ wtedy byłoby bardziej zrozumiałe, aby najpierw wyszukać element i wykonać przetwarzanie poza pętlą.
jest tego skrajnym i oczywistym przykładem. Bardziej subtelny, a przez to bardziej powszechny, jest wzór fabryczny
Jest to trudne do odczytania, ponieważ nie jest oczywiste, że kod treści zostanie wykonany tylko raz. W takim przypadku lepiej byłoby oddzielić wyszukiwanie:
Wciąż istnieje
if
wewnątrz afor
, ale z kontekstu staje się jasne, co robi, nie ma potrzeby zmieniać tego kodu, chyba że wyszukiwanie zmieni się (np. Na amap
) i od razu widać, żecreate_object()
jest wywoływane tylko raz, ponieważ jest nie wewnątrz pętli.źródło
for( range ){ if( condition ){ action } }
styl-ułatwia czytanie fragmentów po kawałku i wykorzystuje tylko znajomość podstawowych konstrukcji językowych.for(...) if(...) { ... }
często jest to najlepszy wybór (dlatego zaleciłem podzielenie akcji na podprogram).for(…)if(…)…
gdyby było to jedyne miejsce wyszukiwania.Oto szybka, stosunkowo minimalna
filter
funkcja.Potrzeba predykatu. Zwraca obiekt funkcji, który przyjmuje iterowalną.
Zwraca iterowalny element, którego można użyć w
for(:)
pętli.Poszedłem na skróty. Prawdziwa biblioteka powinna tworzyć prawdziwe iteratory, a nie
for(:)
kwalifikujące się pseudo-fasady, które zrobiłem.W miejscu użytkowania wygląda to tak:
co jest całkiem ładne i drukuje
Przykład na żywo .
Jest proponowany dodatek do C ++ o nazwie Rangesv3, który robi to i więcej.
boost
ma również dostępne zakresy filtrów / iteratory. boost ma również pomocników, dzięki którym pisanie powyższego jest znacznie krótsze.źródło
Jeden styl, który jest wystarczająco używany, aby go wspomnieć, ale nie został jeszcze wspomniany, to:
Zalety:
DoStuff();
gdy zwiększa się złożoność warunku. Logicznie rzecz biorąc,DoStuff();
powinien znajdować się na najwyższym poziomiefor
pętli i tak jest.SOMETHING
sekund od zbierania, nie wymagając czytnika w celu sprawdzenia, że nie ma po zamknięciu}
częściif
bloku.Niedogodności:
continue
, podobnie jak inne instrukcje sterujące przepływem, jest nadużywany w sposób, który prowadzi do trudnego do naśladowania kodu do tego stopnia, że niektórzy ludzie sprzeciwiają się jakiemukolwiek ich użyciu: istnieje prawidłowy styl kodowania, który niektórzy przestrzegają, który unikacontinue
i unikabreak
innego niż w aswitch
, to unikareturn
innego niż na końcu funkcji.źródło
for
pętli obejmującej wiele wierszy dwuwierszowe „jeśli nie, kontynuuj” jest znacznie jaśniejsze, logiczne i czytelne. Natychmiastowe powiedzenie „pomiń to, jeśli” pofor
instrukcji brzmi dobrze i, jak powiedziałeś, nie powoduje wcięcia pozostałych funkcjonalnych aspektów pętli. Jeśli jednakcontinue
jest dalej niżej, traci się pewną przejrzystość (tj. Jeśli przedif
instrukcją zawsze będzie wykonywana jakaś operacja ).Wygląda
for
dla mnie jak C ++ - specyficzne zrozumienie. Tobie?źródło
auto const
nie ma żadnego wpływu na kolejność iteracji. Jeśli spojrzysz na oparty na zakresiefor
, zobaczysz, że w zasadzie wykonuje standardową pętlę odbegin()
doend()
z niejawnym dereferencją. W żaden sposób nie może złamać gwarancji zamówienia (jeśli w ogóle) dla iterowanego kontenera; byłoby to wyśmiewane z powierzchni Ziemistd::future
s,std::function
s, nawet te anonimowe domknięcia są bardzo dobrze C ++ ish w składni; każdy język ma swój własny język, a wprowadzając nowe funkcje, stara się naśladować starą dobrze znaną składnię.Gdyby DoStuff () w przyszłości zależało od i, to zaproponowałbym ten gwarantowany wariant maskowania bitów bez gałęzi.
Gdzie popcount to dowolna funkcja obliczająca populację (liczba bitów = 1). Będzie pewna swoboda w nakładaniu bardziej zaawansowanych ograniczeń na i i ich sąsiadów. Jeśli nie jest to potrzebne, możemy zdjąć pętlę wewnętrzną i przerobić pętlę zewnętrzną
po którym następuje
źródło
Ponadto, jeśli nie obchodzi Cię zmiana kolejności kolekcji, std :: partition jest tania.
źródło
std::partition
zmienia kolejność kontenera.Jestem pod wrażeniem złożoności powyższych rozwiązań. Miałem zamiar zasugerować prostą,
#define foreach(a,b,c,d) for(a; b; c)if(d)
ale ma kilka oczywistych wad, na przykład musisz pamiętać o używaniu przecinków zamiast średników w pętli i nie możesz użyć operatora przecinka wa
lubc
.źródło
Inne rozwiązanie w przypadku, gdy i: s są ważne. Ten tworzy listę, która wypełnia indeksy, dla których należy wywołać funkcję doStuff (). Po raz kolejny głównym celem jest uniknięcie rozgałęzienia i zamiana na możliwe do rurociągu koszty arytmetyczne.
Linia „magiczna” to linia ładowania bufora, która oblicza arytmetycznie, czy zachować wartość i pozostać na miejscu lub policzyć pozycję i dodać wartość. Więc sprzedajemy potencjalną gałąź na jakąś logikę i arytmetykę, a może trochę trafień w pamięci podręcznej. Typowym scenariuszem, w którym byłoby to przydatne, jest sytuacja, w której doStuff () wykonuje niewielką liczbę potokowych obliczeń i każda gałąź między wywołaniami może przerwać te potoki.
Następnie po prostu przejrzyj bufor i uruchom doStuff (), aż osiągniemy cnt. Tym razem będziemy mieć bieżący i przechowywany w buforze, abyśmy mogli go użyć w wywołaniu doStuff (), jeśli zajdzie taka potrzeba.
źródło
Można opisać swój wzorzec kodu jako zastosowanie jakiejś funkcji do podzbioru zakresu lub innymi słowy: zastosowanie jej do wyniku zastosowania filtru do całego zakresu.
Jest to osiągalne w najprostszy sposób dzięki bibliotece range -v3 Erica Neiblera ; chociaż to trochę obrzydliwe, bo chcesz pracować z indeksami:
Ale jeśli chcesz zrezygnować z indeksów, otrzymasz:
co jest ładniejsze IMHO.
PS - Biblioteka zakresów przechodzi głównie do standardu C ++ w C ++ 20.
źródło
Wspomnę tylko o Mike'u Actonie, na pewno powiedziałby:
Jeśli musisz to zrobić, masz problem ze swoimi danymi. Sortuj swoje dane!
źródło