Czy pętla „for” oparta na zakresach unieważnia wiele prostych algorytmów?

81

Rozwiązanie algorytmu:

std::generate(numbers.begin(), numbers.end(), rand);

Rozwiązanie for-loop oparte na zakresie:

for (int& x : numbers) x = rand();

Dlaczego miałbym chcieć używać bardziej szczegółowych std::generatepętli for opartych na zakresie w C ++ 11?

fredoverflow
źródło
14
Kompatybilność? Oh Nieważne algorytmy z iteratorów często nie są composable tak ... :(
R. Martinho Fernandes
2
... które nie są begin()i end()?
the_mandrill,
6
@jrok Spodziewam się, że wiele osób ma już rangefunkcję w swoim zestawie narzędzi. (tj. for(auto& x : range(first, last)))
R. Martinho Fernandes
14
boost::generate(numbers, rand); // ♪
Xeo,
5
@JamesBrock Dyskutowaliśmy o tym często na czacie C ++ (powinno być gdzieś w transkrypcjach: P). Głównym problemem jest to, że algorytmy często zwracają jeden iterator i pobierają dwa iteratory.
R. Martinho Fernandes

Odpowiedzi:

79

Pierwsza wersja

std::generate(numbers.begin(), numbers.end(), rand);

mówi nam, że chcesz wygenerować sekwencję wartości.

W drugiej wersji czytelnik będzie musiał sam to rozgryźć.

Oszczędność na pisaniu jest zwykle nieoptymalna, ponieważ najczęściej tracona jest na czasie czytania. Większość kodu jest czytana znacznie częściej niż jest wpisywana.

Bo Persson
źródło
13
Oszczędzasz na pisaniu? Oh, już rozumiem. Dlaczego, och, dlaczego mamy ten sam termin na „sprawdzanie poczytalności w czasie kompilacji” i „naciskanie klawiszy na klawiaturze”? :)
fredoverflow
25
Oszczędność na wpisywaniu jest zwykle nieoptymalna ” Nonsens; wszystko zależy od używanej biblioteki. std :: generowanie jest długie, ponieważ musisz podać numbersdwa razy bez powodu. Stąd: boost::range::generate(numbers, rand);. Nie ma powodu, dla którego nie możesz mieć jednocześnie krótszego i bardziej czytelnego kodu w dobrze zbudowanej bibliotece.
Nicol Bolas,
9
Wszystko zależy od czytelnika. Dla wersji pętli jest to zrozumiałe w większości środowisk programistycznych: dodaj wartość rand do każdego elementu kolekcji. Std :: generation wymaga znajomości ostatniego C ++ lub zgadywanie, że generowanie oznacza w rzeczywistości „modyfikację elementów”, a nie „zwracanie wygenerowanych wartości”.
hyde
2
Jeśli chcesz zmodyfikować tylko część kontenera, możesz std::generate(number.begin(), numbers.begin()+3, rand), prawda? Więc numberwydaje mi się, że określenie dwa razy może być czasami przydatne.
Marson Mao,
7
@MarsonMao: jeśli masz tylko dwa argumenty std::generate(), możesz zamiast tego zrobić std::generate(slice(number.begin(), 3), rand)lub nawet lepiej, używając hipotetycznej składni krojenia zakresu, takiej jak ta, std::generate(number[0:3], rand)która usuwa powtórzenie, numberjednocześnie umożliwiając elastyczną specyfikację części zakresu. Odwrotna sytuacja, zaczynając od trzech argumentów, std::generate()jest bardziej uciążliwa.
Lie Ryan,
42

To, czy pętla for jest oparta na zakresie, czy nie, w ogóle nie robi różnicy, tylko upraszcza kod wewnątrz nawiasów. Algorytmy są wyraźniejsze, ponieważ pokazują zamiar .

David Rodríguez - dribeas
źródło
30

Osobiście moje pierwsze czytanie:

std::generate(numbers.begin(), numbers.end(), rand);

to „przypisujemy wszystko w zakresie. Zakres to numbers. Przypisane wartości są losowe”.

Moja pierwsza lektura:

for (int& x : numbers) x = rand();

to „robimy coś dla wszystkiego w zakresie. Zakres wynosi numbers. To, co robimy, to przypisywanie losowej wartości”.

Te są cholernie podobne, ale nie identyczne. Jednym z możliwych powodów, dla których mógłbym chcieć sprowokować pierwsze czytanie, jest to, że uważam, że najważniejszym faktem dotyczącym tego kodu jest to, że przypisuje on zakres. Więc jest twoje "dlaczego miałbym chcieć ...". Używam, generateponieważ w C ++ std::generateoznacza „przypisanie zakresu”. Tak przy okazji std::copy, różnica między nimi polega na tym, z czego przypisujesz.

Istnieją jednak czynniki mylące. Pętle for oparte na zakresie mają z natury bardziej bezpośredni sposób wyrażania, że ​​jest to zakres numbers, niż robią to algorytmy oparte na iteratorach. Dlatego ludzie pracują na bibliotekach algorytmów opartych na zakresach: boost::range::generate(numbers, rand);wygląda lepiej niż std::generatewersja.

W przeciwieństwie do tego, int&w twoim zakresie pętli for jest zmarszczka. A co jeśli typ wartości zakresu nie jest int, to robimy tutaj coś irytująco subtelnego, co zależy od tego, czy można go zamienić na int&, podczas gdy generatekod zależy tylko od powrotu z randprzypisania do elementu. Nawet jeśli typ wartości to int, nadal mogę przestać myśleć o tym, czy tak jest, czy nie. Stąd auto, co odkłada myślenie o typach, dopóki nie zobaczę, co zostanie przypisane - auto &xmówię „weź odwołanie do elementu zakresu, dowolnego typu, który może mieć”. Powrót w C ++ 03, algorytmy (bo są szablony funkcyjne) były sposób ukryć dokładnych typów, teraz są sposób.

Myślę, że zawsze było tak, że najprostsze algorytmy miały tylko marginalną korzyść w porównaniu z równoważnymi pętlami. Oparte na zakresie pętle for poprawiają pętle (głównie poprzez usunięcie większości kotłów, chociaż jest ich trochę więcej). Tak więc marginesy są coraz ciaśniejsze i być może zmienisz zdanie w niektórych szczególnych przypadkach. Ale nadal istnieje różnica stylu.

Steve Jessop
źródło
Czy kiedykolwiek widziałeś typ zdefiniowany przez użytkownika z operator int&()? :)
fredoverflow
@FredOverflow zastąpić int&z SomeClass&a teraz trzeba się martwić o operatorów konwersji i konstruktorów pojedynczy parametr nie oznakowanych explicit.
TemplateRex,
@FredOverflow: nie myśl tak. Dlatego, jeśli to się kiedykolwiek wydarzy, nie będę się tego spodziewał i nieważne, jak bardzo jestem teraz paranoikiem, ugryzie mnie, jeśli nie pomyślę o tym wtedy ;-) Obiekt proxy mógłby pracować przez przeciążenie operator int&()i operator int const &() const, ale z drugiej strony może działać przez przeciążenie operator int() consti operator=(int).
Steve Jessop
1
@rhalbersma: Myślę, że nie musisz martwić się o konstruktory, ponieważ ref niebędący stałym nie wiąże się z tymczasowym. To tylko operatory konwersji do typów referencyjnych.
Steve Jessop,
23

Moim zdaniem, efektywny element STL 43: „Preferuj wywołania algorytmów od ręcznie pisanych pętli”. to nadal dobra rada.

Zwykle piszę funkcje opakowujące, aby pozbyć się begin()/ end()hell. Jeśli to zrobisz, Twój przykład będzie wyglądał następująco:

my_util::generate(numbers, rand);

Uważam, że pokonuje zakres oparty na pętli, zarówno pod względem komunikowania intencji, jak i czytelności.


Powiedziawszy to, muszę przyznać, że w C ++ 98 niektóre wywołania algorytmu STL dawały kod, którego nie można było wypowiedzieć, a następująca po tym fraza „Preferuj wywołania algorytmu zamiast ręcznie pisanych pętli” nie wydawała się dobrym pomysłem. Na szczęście lambdy to zmieniły.

Rozważmy następujący przykład z Herba Suttera: Lambdas, Lambdas Everywhere .

Zadanie: znajdź pierwszy element w v, czyli > xi < y.

Bez lambd:

auto i = find_if( v.begin(), v.end(),
bind( logical_and<bool>(),
bind(greater<int>(), _1, x),
bind(less<int>(), _1, y) ) );

Z lambdą

auto i=find_if( v.begin(), v.end(), [=](int i) { return i > x && i < y; } );
Ali
źródło
1
Trochę ortogonalnie do pytania. Tylko pierwsze zdanie dotyczy tego pytania.
David Rodríguez - dribeas
@ DavidRodríguez-dribeas Tak. Druga połowa wyjaśnia, dlaczego moim zdaniem pozycja 43 jest nadal dobrą radą.
Ali
Z Boost.Lambda jest jeszcze lepiej niż z funkcjami lambda w C ++: auto i = find_if (v.begin (), v.end (), _1> x && _1 <y);
sdkljhdf hda
1
+1 za opakowania. Robi to samo. Powinien być w standardzie od dnia 1 (a może 2 ...)
Macke
22

W moim zdaniem, instrukcja pętli, chociaż może zmniejszyć szczegółowość, brakuje readabitly:

for (int& x : numbers) x = rand();

Nie użyłbym tej pętli do zainicjowania 1 zakresu określonego liczbami , bo kiedy na to patrzę, wydaje mi się, że iteruje po zakresie liczb, ale w rzeczywistości nie (w istocie), tj. Zamiast czytając z zakresu, pisze do zakresu.

Intencja jest znacznie wyraźniejsza, gdy używasz std::generate.

1. inicjalizacja w tym kontekście oznacza nadanie znaczącej wartości elementom kontenera.

Nawaz
źródło
5
Czy to nie tylko dlatego, że nie jesteś przyzwyczajony do pętli for opartych na zakresie? Wydaje mi się dość jasne, że to stwierdzenie przypisuje każdemu elementowi zakresu. Jest jasne, że generowanie robi to samo, co iff, które znasz std::generate, co można założyć w przypadku programisty C ++ (jeśli nie są zaznajomieni, sprawdzą to, ten sam wynik).
Steve Jessop
4
@SteveJessop: Ta odpowiedź nie różni się od pozostałych dwóch. Wymaga to nieco więcej wysiłku ze strony czytelnika i jest nieco bardziej podatne na błędy (a co, jeśli zapomnisz jednego &znaku?). Zaletą algorytmów jest to, że pokazują zamiar, podczas gdy w przypadku pętli musisz to wywnioskować. Jeśli w implementacji pętli występuje błąd, nie jest jasne, czy jest to błąd, czy był zamierzony.
David Rodríguez - dribeas
1
@ DavidRodríguez-dribeas: ta odpowiedź różni się znacznie od pozostałych dwóch, IMO znacznie. Próbuje zgłębić powód , dla którego autor uważa jeden fragment kodu za bardziej jasny / zrozumiały niż inny. Inni stwierdzają to bez analizy. Dlatego uważam to za wystarczająco interesujące, aby na nie odpowiedzieć :-)
Steve Jessop
1
@SteveJessop: Musisz zajrzeć do treści pętli, aby dojść do wniosku, że faktycznie generujesz liczby, ale w przypadku std::generatezwykłego spojrzenia można powiedzieć, że coś jest generowane przez tę funkcję; na co to coś odpowiada trzeci argument funkcji. Myślę, że to jest znacznie lepsze.
Nawaz
1
@SteveJessop: Oznacza to, że należysz do mniejszości. Napisałbym kod, który byłby czytelniejszy dla większości: P. Jeszcze ostatnia: nigdzie nie powiedziałem, że inni będą czytać pętlę w taki sam sposób, jak ja. Powiedziałem (raczej chodziło mi o ), że jest to jeden ze sposobów odczytania pętli, która jest dla mnie myląca, a ponieważ jest tam treść pętli, różni programiści będą ją czytać inaczej, aby dowiedzieć się, co się tam dzieje; mogą sprzeciwić się użyciu takiej pętli z różnych powodów, z których wszystkie mogą być poprawne zgodnie z ich postrzeganiem.
Nawaz
9

Są rzeczy, których nie można zrobić (po prostu) z pętlami opartymi na zakresie, ponieważ algorytmy przyjmują iteratory jako dane wejściowe. Na przykład z std::generate:

Wypełnij kontener do limit(wykluczone, limitto poprawny iterator włączony numbers) zmiennymi z jednej dystrybucji, a resztę zmiennymi z innej dystrybucji.

std::generate(numbers.begin(), limit, rand1);
std::generate(limit, numbers.end(), rand2);

Algorytmy oparte na iteratorze zapewniają lepszą kontrolę nad zakresem, na którym pracujesz.

Khaur
źródło
8
Chociaż powód czytelność jest ogromny jeden wolą algorytmów, jest to jedyna odpowiedź, która pokazuje piecyk oparty na pętli jest tylko podzbiór co algorytmy są, a tym samym nie może potępiać coś ...
K-ballo
6

W konkretnym przypadku std::generatezgadzam się z poprzednimi odpowiedziami dotyczącymi czytelności / intencji. std :: generowanie wydaje mi się bardziej przejrzystą wersją. Ale przyznaję, że to w pewnym sensie kwestia gustu.

To powiedziawszy, mam jeszcze jeden powód, aby nie wyrzucać algorytmu std :: - istnieją pewne algorytmy, które są wyspecjalizowane dla niektórych typów danych.

Najprostszym przykładem byłoby std::fill. Wersja ogólna jest implementowana jako pętla for w podanym zakresie i ta wersja będzie używana podczas tworzenia wystąpienia szablonu. Ale nie zawsze. Np. Jeśli podasz mu zakres, który jest std::vector<int>- często będzie faktycznie wywoływał memsetpod maską, dając znacznie szybszy i lepszy kod.

Więc próbuję zagrać tutaj kartę efektywności.

Twoja odręczna pętla może być tak szybka jak wersja std :: algorytm, ale prawie nie może być szybsza. Co więcej, algorytm std :: może być wyspecjalizowany dla określonych kontenerów i typów i odbywa się to w ramach czystego interfejsu STL.

Siergiej Nosow
źródło
3

Moja odpowiedź brzmiałaby może i nie. Jeśli mówimy o C ++ 11, to może (bardziej jak nie). Na przykład std::for_eachjest naprawdę denerwujące w użyciu nawet z lambdami:

std::for_each(c.begin(), c.end(), [&](ExactTypeOfContainedValue& x)
{
    // do stuff with x
});

Ale używanie oparte na zakresie dla jest o wiele lepsze:

for (auto& x : c)
{
    // do stuff with x
}

Z drugiej strony, jeśli mówimy o C ++ 1y, to argumentowałbym, że nie, algorytmy nie zostaną przestarzałe przez zakres oparty na. W komisji normalizacyjnej C ++ istnieje grupa analityczna, która pracuje nad propozycją dodania zakresów do C ++, a także trwają prace nad polimorficznymi lambdami. Zakresy wyeliminowałyby potrzebę używania pary iteratorów, a polimorficzna lambda pozwoliłaby ci nie określać dokładnego typu argumentu lambda. Oznacza to, że std::for_eachmożna go użyć w ten sposób (nie traktuj tego jako twardego faktu, tak po prostu wyglądają sny dzisiaj):

std::for_each(c.range(), [](x)
{
    // do stuff with x
});
sdkljhdf hda
źródło
Zatem w tym drugim przypadku zaletą algorytmu byłoby to, że pisząc []z lambdą określasz przechwytywanie zerowe? Oznacza to, że w porównaniu z samym pisaniem treści pętli wyizolowałeś fragment kodu z kontekstu wyszukiwania zmiennych, w którym występuje on leksykalnie. Izolacja jest zwykle pomocna dla czytelnika, a mniej do myślenia podczas czytania.
Steve Jessop
1
Nie chodzi o zdobycie. Chodzi o to, że w przypadku polimorficznej lambdy nie musisz wyraźnie określać, jaki jest typ x.
sdkljhdf hda
1
W takim przypadku wydaje mi się, że w tym hipotetycznym C ++ 1y for_eachjest nadal bezcelowe, nawet jeśli jest używane z lambdą. foreach + przechwytywanie lambda jest obecnie rozwlekłym sposobem pisania pętli for opartej na zakresie i staje się nieco mniej szczegółowym sposobem, ale nadal bardziej niż pętla. for_eachOczywiście nie uważam, że powinieneś się bronić , ale jeszcze przed zobaczeniem twojej odpowiedzi pomyślałem, że gdyby pytający chciał pokonać algorytmy, mógłby for_eachwybrać jako najdelikatniejszy z możliwych celów ;-)
Steve Jessop
Nie będzie się bronić for_each, ale ma jedną niewielką przewagę nad opartą na zasięgu - możesz uczynić go równoległym łatwiej, po prostu poprzedzając go przedrostkiem parallel_, aby go zamienić parallel_for_each(jeśli używasz PPL i zakładając, że jest to bezpieczne wątkowo) . :-D
sdkljhdf hda
@lego Twoja „mała” przewaga jest rzeczywiście „dużą” zaletą, jeśli uogólniając ją do faktu, że std::algorithmimplementacja s jest ukryta za ich interfejsem i może być dowolnie złożona (lub dowolnie zoptymalizowana).
Christian Rau,
1

Należy zauważyć, że algorytm wyraża to, co się robi, a nie jak.

Pętla oparta na zakresie obejmuje sposób wykonywania czynności: zacznij od pierwszego, zastosuj i przejdź do następnego elementu aż do końca. Nawet prosty algorytm mógłby zrobić coś inaczej (przynajmniej niektóre przeciążenia dla określonych kontenerów, nawet nie myśląc o okropnym wektorze), a przynajmniej sposób, w jaki to się robi, nie jest biznesem pisarza.

Dla mnie to duża różnica, hermetyzuj tyle, ile możesz, a to uzasadnia zdanie, kiedy możesz, użyj algorytmów.

Cedric
źródło
1

Oparta na zakresie pętla for jest właśnie tym. Dopóki oczywiście nie zmieni się standard.

Algorytm to funkcja. Funkcja, która stawia pewne wymagania swoim parametrom. Wymagania są sformułowane w standardzie, aby umożliwić na przykład implementację, która wykorzystuje wszystkie dostępne wątki wykonawcze i przyspiesza automatycznie.

Tomek
źródło