Wolisz algorytmy od ręcznie pisanych pętli?

10

Które z poniższych stwierdzeń jest dla Ciebie bardziej czytelne? Ręcznie napisana pętla:

for (std::vector<Foo>::const_iterator it = vec.begin(); it != vec.end(); ++it)
{
    bar.process(*it);
}

Lub wywołanie algorytmu:

#include <algorithm>
#include <functional>

std::for_each(vec.begin(), vec.end(),
              std::bind1st(std::mem_fun_ref(&Bar::process), bar));

Zastanawiam się, czy std::for_eachnaprawdę warto, biorąc pod uwagę, że tak prosty przykład wymaga już tyle kodu.

Jakie są twoje przemyślenia na ten temat?

fredoverflow
źródło
2
W przypadku C ++ odpowiedź jest dość oczywista. Ale w innych językach oba są w przybliżeniu równe (np. Python: map(bar.process, vec)chociaż mapa efektów ubocznych jest odradzana, a lista wyrażeń / wyrażeń generatora jest zalecana zamiast mapy).
5
A potem jest też BOOST_FOREACH...
Benjamin Bannier
Tak więc pozycja w skutecznym STL cię nie przekonała? stackoverflow.com/questions/135129/…
Job

Odpowiedzi:

18

Jest powód, dla którego wprowadzono lambdas, i to dlatego, że nawet Standardowy Komitet rozpoznaje, że druga forma jest do bani. Skorzystaj z pierwszego formularza, aż uzyskasz obsługę C ++ 0x i lambda.

DeadMG
źródło
2
To samo rozumowanie można zastosować do uzasadnienia drugiej formy: Komitet Standardowy uznał, że był o tyle lepszy, że wprowadził lambda, aby uczynić go jeszcze lepszym. :-p (mimo to +1)
Konrad Rudolph
Nie sądzę, że drugi jest tak zły. Ale może być lepiej. Na przykład celowo sprawiłeś, że obiekt Bar jest trudniejszy w użyciu, nie dodając operatora (). Dzięki temu znacznie upraszcza punkt wywoławczy w pętli i porządkuje kod.
Martin York,
Uwaga: dodano obsługę lambda z powodu dwóch głównych skarg. 1) Standardowa płyta kotła potrzebna do przekazania parametrów funktorom. 2) Brak kodu w punkcie wywoławczym. Twój kod nie spełnia żadnego z tych kryteriów, ponieważ tylko wywołujesz metodę. Więc 1) brak dodatkowego kodu do przekazywania parametrów 2) Kod nie znajduje się już w punkcie połączenia, więc lambda nie poprawi obsługi kodu. Tak więc lambda są świetnym dodatkiem. To nie jest dla nich dobry argument.
Martin York,
9

Zawsze używaj wariantu, który najlepiej opisuje to , co zamierzasz zrobić. To jest

Dla każdego elementu xw vec, wykonaj bar.process(x).

Teraz przeanalizujmy przykłady:

std::for_each(vec.begin(), vec.end(),
              std::bind1st(std::mem_fun_ref(&Bar::process), bar));

My też for_eachtam mamy - yippeh . Mamy [begin; end)zasięg, na którym chcemy operować.

Zasadniczo algorytm był znacznie bardziej wyraźny i dlatego był lepszy niż jakakolwiek ręczna implementacja. Ale potem ... Segregatory? Memfun? Zasadniczo C ++ interna jak uzyskać funkcję członka? W moim zadaniu nie dbam o nie! Nie chcę też cierpieć z powodu tej pełnej, przerażającej składni.

Teraz inna możliwość:

for (std::vector<Foo>::const_iterator it = vec.begin(); it != vec.end(); ++it)
{
    bar.process(*it);
}

To prawda, że ​​jest to powszechny wzorzec rozpoznawania, ale ... tworzenie iteratorów, zapętlanie, inkrementacja, dereferencje. To także są rzeczy , których nie obchodzi mnie , aby wykonać swoje zadanie.

Trzeba przyznać, że wygląda lepiej niż waay pierwszego rozwiązania (przynajmniej, pętla ciało jest elastyczne i dość wyraźny), ale nadal, to naprawdę nie jest , że świetnie. Użyjemy tego, jeśli nie mielibyśmy lepszych możliwości, ale może mamy ...

Lepszy sposób?

Teraz z powrotem do for_each. Czy nie byłoby wspaniale powiedzieć for_each i być elastycznym w operacjach, które należy wykonać? Na szczęście, odkąd C ++ 0x lambdas, jesteśmy

for_each(v.begin(), v.end(), [&](const Foo& x) { bar.process(x); })

Teraz, gdy znaleźliśmy abstrakcyjne, ogólne rozwiązanie wielu powiązanych sytuacji, warto zauważyć, że w tym konkretnym przypadku istnieje absolutnie ulubiony numer 1 :

foreach(const Foo& x, vec) bar.process(x);

Naprawdę nie może być dużo jaśniejszego niż to. Na szczęście, C ++ 0x get podobna składnia wbudowanej !

Dario
źródło
2
Powiedział, że „podobna składnia” jest for (const Foo& x : vec) bar.process(x);lub używa, const auto&jeśli chcesz.
Jon Purdy,
const auto&jest możliwe? Nie wiedziałem o tym - świetne informacje!
Dario
8

Ponieważ to jest tak nieczytelne?

for (unsigned int i=0;i<vec.size();i++) {
{
    bar.process(vec[i]);
}
Martin Beckett
źródło
4
Przepraszam, ale z jakiegoś powodu jest napisane „ocenzurowane”, gdzie moim zdaniem powinien być twój kod.
Mateen Ulhaq
6
Twój kod ostrzega kompilator, ponieważ porównywanie intz size_tjest niebezpieczne. Ponadto indeksowanie nie działa na przykład z każdym kontenerem std::set.
fredoverflow
2
Ten kod będzie działał tylko w przypadku kontenerów z operatorem indeksowania, których jest niewiele. Algorytm bezładnie wiąże algorytm - co by było, gdyby mapa <> lepiej pasowała? Oryginał dla pętli i for_each pozostanie niezmieniony.
JBRWilkinson
2
Ile razy projektujesz kod dla wektora, a następnie decydujesz się zamienić go na mapę? Jakby zaprojektowanie tego było priorytetem dla języków.
Martin Beckett,
2
Odradzanie iteracji po kolekcjach według indeksu jest odradzane, ponieważ niektóre kolekcje mają słabą wydajność podczas indeksowania, podczas gdy wszystkie kolekcje zachowują się ładnie podczas korzystania z iteratora.
slaphappy,
3

ogólnie rzecz biorąc, pierwsza forma jest czytelna dla praktycznie każdego, kto wie, czym jest pętla for, bez względu na to, jakie ma doświadczenie.

także ogólnie rzecz biorąc, drugi nie jest wcale tak czytelny: dość łatwe ustalenie, co robi for_each, ale jeśli nigdy nie widziałeś, std::bind1st(std::mem_fun_refnie mogę sobie wyobrazić, że trudno to zrozumieć.

stijn
źródło
2

Właściwie nawet przy C ++ 0x nie jestem pewien, for_eachczy dostanę dużo miłości.

for(Foo& foo: vec) { bar.process(foo); }

Jest o wiele bardziej czytelny.

Jedną z rzeczy, których nie lubię w algorytmach (w C ++) jest to, że ich rozumowanie na iteratorach jest bardzo szczegółowe.

Matthieu M.
źródło
2

Gdyby napisać takt jako funktor, byłoby to o wiele prostsze:

// custom functor
class Bar
{    public: void operator()(Foo& value) { /* STUFF */ }
};


std::for_each(vec.begin(), vec.end(), Bar());

Tutaj kod jest dość czytelny.

Martin York
źródło
2
Jest dość czytelny poza tym, że właśnie napisałeś funktor.
Winston Ewert
Zobacz tutaj, dlaczego uważam, że ten kod jest gorszy.
sbi
1

Wolę ten drugi, ponieważ jest schludny i czysty. W rzeczywistości jest częścią wielu innych języków, ale w C ++ jest częścią biblioteki. To naprawdę nie ma znaczenia.

sarat
źródło
0

Twierdziłbym, że teraz ten problem nie jest specyficzny dla C ++.

Chodzi o to, że przekazanie funktora do innej funkcji nie ma na celu zastąpienia pętli .
Ma uprościć pewne wzorce, które stają się bardzo naturalne w programowaniu funkcjonalnym, takim jak gość i obserwator , żeby wymienić tylko dwa, które przychodzą mi do głowy natychmiast.
W językach, w których brakuje funkcji pierwszego rzędu (Java jest prawdopodobnie najlepszym przykładem), takie podejścia zawsze wymagają implementacji określonego interfejsu, który jest dość werbalny i zbędny.

Częstym zastosowaniem, które często widzę w innych językach, byłoby:

someCollection.forEach(someUnaryFunctor);

Zaletą tego jest to, że nie musisz wiedzieć jak someCollection jest faktycznie wdrażany ani co someUnaryFunctorrobi. Wszystko, co musisz wiedzieć, to że forEachmetoda zastosuje iterację wszystkich elementów kolekcji, przekazując je do podanej funkcji.

Osobiście, jeśli jesteś w stanie, aby mieć wszystkie informacje o strukturze danych, którą chcesz iterować, oraz wszystkie informacje o tym, co chcesz zrobić na każdym etapie iteracji, podejście funkcjonalne nadmiernie komplikuje rzeczy, szczególnie w języku, gdzie jest to pozornie dość nużące.

Należy również pamiętać, że funkcjonalne podejście jest wolniejsze, ponieważ masz wiele połączeń, które kosztują za pewną cenę, których nie masz w pętli for.

back2dos
źródło
1
„Należy również pamiętać, że funkcjonalne podejście jest wolniejsze, ponieważ masz wiele połączeń, które są płatne, a których nie masz w pętli for”. ? To oświadczenie nie jest obsługiwane. Podejście funkcjonalne niekoniecznie jest wolniejsze, zwłaszcza że pod maską mogą znajdować się optymalizacje. I nawet jeśli całkowicie rozumiesz swoją pętlę, prawdopodobnie będzie ona wyglądać bardziej czysto w bardziej abstrakcyjnej formie.
Andres F.,
@Andres F: Więc to wygląda na czystsze? std::for_each(vec.begin(), vec.end(), std::bind1st(std::mem_fun_ref(&Bar::process), bar));I jest wolniejszy w czasie wykonywania lub kompilacji (jeśli masz szczęście). Nie rozumiem, dlaczego zastąpienie struktur kontroli przepływu wywołaniami funkcji poprawia kod. Każda przedstawiona tutaj alternatywa jest bardziej czytelna.
back2dos
0

Myślę, że problem polega na tym, że for_eachtak naprawdę nie jest algorytmem, to po prostu inny (i zwykle gorszy sposób) napisanie ręcznie napisanej pętli. z tej perspektywy powinien to być najmniej używany standardowy algorytm i tak, prawdopodobnie równie dobrze możesz użyć pętli for. jednak rada w tytule nadal obowiązuje, ponieważ istnieje kilka standardowych algorytmów dostosowanych do bardziej specyficznych zastosowań.

Tam, gdzie jest algorytm, który ściślej robi to, co chcesz, to tak, powinieneś preferować algorytmy zamiast ręcznie napisanych pętli. oczywistymi przykładami są tutaj sortowanie za pomocą sortlub stable_sortwyszukiwanie za pomocą lower_bound, upper_boundlub equal_range, ale większość z nich ma pewną sytuację, w której lepiej jest używać ich w porównaniu z ręcznie kodowaną pętlą.

jk.
źródło
0

W przypadku tego małego fragmentu kodu oba są dla mnie jednakowo czytelne. Ogólnie uważam, że ręczne pętle są podatne na błędy i wolę używać algorytmów, ale tak naprawdę zależy to od konkretnej sytuacji.

Nemanja Trifunovic
źródło