Jak umieścić dwie instrukcje inkrementacji w pętli C ++ „for”?

93

Chciałbym inkrementować dwie zmienne w stanie for-loop zamiast jednej.

Więc coś takiego:

for (int i = 0; i != 5; ++i and ++j) 
    do_something(i, j);

Jaka jest składnia tego?

Peter Smit
źródło

Odpowiedzi:

155

Popularnym idiomem jest użycie operatora przecinka, który ocenia oba operandy i zwraca drugi operand. A zatem:

for(int i = 0; i != 5; ++i,++j) 
    do_something(i,j);

Ale czy to naprawdę operator przecinka?

Po napisaniu tego komentator zasugerował, że jest to w rzeczywistości jakiś specjalny cukier składniowy w instrukcji for, a nie w ogóle operator przecinka. Sprawdziłem to w GCC w następujący sposób:

int i=0;
int a=5;
int x=0;

for(i; i<5; x=i++,a++){
    printf("i=%d a=%d x=%d\n",i,a,x);
}

Spodziewałem się, że x podniesie oryginalną wartość a, więc powinno było wyświetlić 5,6,7 .. dla x. Dostałem to

i=0 a=5 x=0
i=1 a=6 x=0
i=2 a=7 x=1
i=3 a=8 x=2
i=4 a=9 x=3

Jeśli jednak ująłem wyrażenie w nawiasach, aby zmusić parser do rzeczywistego zobaczenia operatora przecinka, otrzymuję to

int main(){
    int i=0;
    int a=5;
    int x=0;

    for(i=0; i<5; x=(i++,a++)){
        printf("i=%d a=%d x=%d\n",i,a,x);
    }
}

i=0 a=5 x=0
i=1 a=6 x=5
i=2 a=7 x=6
i=3 a=8 x=7
i=4 a=9 x=8

Początkowo myślałem, że to pokazało, że nie zachowuje się on wcale jak operator przecinka, ale jak się okazuje, jest to po prostu kwestia pierwszeństwa - operator przecinka ma najniższy możliwy priorytet , więc wyrażenie x = i ++, a ++ jest efektywne przeanalizowane jako (x = i ++), a ++

Dzięki za wszystkie komentarze, to była ciekawa nauka, a C od wielu lat używam!

Paul Dixon
źródło
1
Kilkakrotnie czytałem, że przecinek w pierwszej lub trzeciej części pętli for nie jest operatorem przecinka, a jedynie separatorem przecinka. (Jednak nie mogę znaleźć oficjalnego źródła tego, ponieważ jestem szczególnie kiepski w analizowaniu standardu języka C ++.)
Daniel Daranas
Na początku myślałem, że się mylisz, ale napisałem trochę kodu testowego i masz rację - nie zachowuje się jak operator przecinka. Poprawi moją odpowiedź!
Paul Dixon
19
W tym kontekście jest to operator przecinka. Powodem, dla którego nie otrzymujesz tego, czego oczekujesz, jest to, że operator polecenia ma niższy priorytet niż operator przypisania, więc bez parantez analizuje go jako (x = i ++), j ++.
kawiarnia
6
Jest to operator przecinka. Przypisanie wiąże się silniej niż operator przecinka, więc x = i ++, a ++ jest analizowane (x = i ++), a ++ a nie x = (i ++, a ++). Ta cecha jest nadużywana przez niektóre biblioteki, tak że v = 1,2,3; robi rzeczy intuicyjne, ale tylko dlatego, że v = 1 zwraca obiekt proxy, dla którego operator przeciążonego przecinka dodaje.
AProgrammer
3
Ok. Z open-std.org/jtc1/sc22/wg21/docs/papers/2009/n2857.pdf sekcja 6.5.3 ostatnia część to „wyrażenie”. (Chociaż 1.6 # 2 definiuje „listę-wyrażeń” jako „listę wyrażeń oddzielonych przecinkami”, ta konstrukcja nie pojawia się w 6.5.3.). Oznacza to, że kiedy piszemy „++ i, ++ j”, musi to być wyrażenie samo w sobie, a zatem „,” musi być operatorem przecinka (5.18). (To nie jest „lista inicjatorów” ani „lista argumentów funkcji”, które są przykładami, w których „przecinkowi nadano specjalne znaczenie”, jak mówi 5.18 # 2). Uważam to jednak za trochę zagmatwane.
Daniel Daranas,
56

Spróbuj tego

for(int i = 0; i != 5; ++i, ++j)
    do_something(i,j);
yeyeyerman
źródło
18
+1 Możesz również zadeklarować jw pierwszej części. for (int i = 0, j = 0; i! = 5; ++ i, ++ j) {...}
Daniel Daranas,
1
+1 Na marginesie, ta sama składnia działa w C # (wyszukałem tutaj hasło „C # dla liczników pętli incrament 2”, więc pomyślałem, że wspomnę o tym).
CodingWithSpike
@CodingWithSpike: Cóż, w C # przecinek jest specjalny, w rzeczywistości nie jest legalne, aby pojawiło się tam wyrażenie operatora przecinka. Przykład to legalne użycie operatora przecinka w C ++, ale odrzucone przez C #:for( ; ; ((++i), (++j)) )
Ben Voigt
@BenVoigt nie ma nic wspólnego z przecinkiem. To też nie jest legalne C #: for(int i = 0; i != 5; (++i)) {dodatkowy nawias oszukuje kompilator do myślenia, że ​​nie jest to już operacja „inkrementacji”.
CodingWithSpike
@CodingWithSpike: To prawda, ale nawiasy zmieniają również sposób, w jaki C # widzi przecinek i zapobiega specjalnemu znaczeniu wewnątrz polecenia for.
Ben Voigt,
6

Staraj się tego nie robić!

Z http://www.research.att.com/~bs/JSF-AV-rules.pdf :

Reguła AV 199
Wyrażenie inkrementujące w pętli for nie wykona żadnej akcji poza zmianą pojedynczego parametru pętli na następną wartość pętli.

Uzasadnienie: czytelność.

squelart
źródło
4
To prawda, ale żeby być uczciwym, jestem prawie pewien, że standard reguł został napisany dla oprogramowania osadzonego w myśliwcu, a nie dla programu C (++) odmiany ogrodowej. Biorąc to pod uwagę, prawdopodobnie jest to dobry nawyk czytelności, a kto wie, może będziesz projektować oprogramowanie F-35 i będzie to jeden nawyk mniej do złamania.
galois
3
for (int i = 0; i != 5; ++i, ++j) 
    do_something(i, j);
malajski
źródło
2

Przyszedłem tutaj, aby przypomnieć sobie, jak zakodować drugi indeks w klauzuli inkrementacji pętli FOR, co wiedziałem, że można to zrobić głównie obserwując go w próbce, którą włączyłem do innego projektu, napisanego w C ++.

Dziś pracuję w języku C #, ale miałem pewność, że pod tym względem będzie przestrzegać tych samych reguł, ponieważ instrukcja FOR jest jedną z najstarszych struktur kontrolnych w całym programowaniu. Na szczęście spędziłem ostatnio kilka dni na dokładnym dokumentowaniu zachowania pętli FOR w jednym z moich starszych programów w języku C i szybko zdałem sobie sprawę, że te badania obejmowały lekcje dotyczące dzisiejszego problemu C #, w szczególności zachowania drugiej zmiennej indeksu .

Dla nieostrożnych poniżej znajduje się podsumowanie moich obserwacji. Wszystko, co widziałem dzisiaj, dzięki uważnej obserwacji zmiennych w oknie Locals, potwierdziło moje oczekiwanie, że instrukcja C # FOR zachowuje się dokładnie tak, jak instrukcja C lub C ++ FOR.

  1. Przy pierwszym wykonaniu pętli FOR klauzula inkrementacji (trzecia z trzech) jest pomijana. W Visual C i C ++ przyrost jest generowany jako trzy instrukcje maszynowe w środku bloku, który implementuje pętlę, tak więc początkowy przebieg uruchamia kod inicjujący tylko raz, a następnie przeskakuje przez blok inkrementacji, aby wykonać test zakończenia. Implementuje to funkcję, że pętla FOR wykonuje zero lub więcej razy, w zależności od stanu jej zmiennych indeksowych i limitów.
  2. Jeśli wykonywana jest treść pętli, jej ostatnią instrukcją jest skok do pierwszej z trzech instrukcji inkrementacji, które zostały pominięte przez pierwszą iterację. Po wykonaniu tych czynności kontrola w naturalny sposób przechodzi do kodu testu limitu, który implementuje klauzulę środkową. Wynik tego testu określa, czy zostanie wykonana treść pętli FOR, czy też sterowanie przenosi do następnej instrukcji poza skokiem na dole jej zakresu.
  3. Ponieważ sterowanie przenosi od dołu bloku pętli FOR do bloku inkrementacji, zmienna indeksowa jest zwiększana przed wykonaniem testu. To zachowanie nie tylko wyjaśnia, dlaczego musisz zakodować swoje klauzule limit w sposób, którego się nauczyłeś, ale ma wpływ na każdy dodatkowy przyrost, który dodajesz za pomocą operatora przecinka, ponieważ staje się częścią trzeciej klauzuli. W związku z tym nie jest zmieniana w pierwszej iteracji, ale jest w ostatniej iteracji, która nigdy nie wykonuje ciała.

Jeśli którakolwiek ze zmiennych indeksujących pozostanie w zasięgu po zakończeniu pętli, ich wartość będzie o jeden wyższa niż próg zatrzymujący pętlę, w przypadku zmiennej indeksującej true. Podobnie, jeśli, na przykład, druga zmienna jest inicjalizowana do zera przed wprowadzeniem pętli, jej wartością na końcu będzie liczba iteracji, przy założeniu, że jest to przyrost (++), a nie dekrementacja i że nic w ciało pętli zmienia swoją wartość.

David A. Gray
źródło
1

Zgadzam się z squelartem. Zwiększanie dwóch zmiennych jest podatne na błędy, zwłaszcza jeśli testujesz tylko jedną z nich.

Oto czytelny sposób, aby to zrobić:

int j = 0;
for(int i = 0; i < 5; ++i) {
    do_something(i, j);
    ++j;
}

Forpętle są przeznaczone dla przypadków, w których pętla działa na jednej rosnącej / malejącej zmiennej. Dla każdej innej zmiennej zmień ją w pętli.

Jeśli musisz jbyć przywiązany i, dlaczego nie pozostawić oryginalnej zmiennej bez zmian i dodać i?

for(int i = 0; i < 5; ++i) {
    do_something(i,a+i);
}

Jeśli twoja logika jest bardziej złożona (na przykład, musisz faktycznie monitorować więcej niż jedną zmienną), użyłbym whilepętli.

Ran Halprin
źródło
2
W pierwszym przykładzie j jest zwiększane raz więcej niż i! A co z iteratorem, w którym należy wykonać jakąś akcję dla pierwszych x kroków? (A kolekcja jest zawsze dostatecznie długa) Możesz następnie wznosić iterator w każdej iteracji, ale imho jest znacznie czystszy.
Peter Smit
0
int main(){
    int i=0;
    int a=0;
    for(i;i<5;i++,a++){
        printf("%d %d\n",a,i);
    } 
}
Arkaitz Jimenez
źródło
1
Jaki jest sens braku tworzenia ii alokalnego w pętli?
sbi
2
Brak, tylko pokazując, jak wykonać oba przyrosty w for, to tylko przykład sytnax
Arkaitz Jimenez
0

Użyj matematyki. Jeśli te dwie operacje matematycznie zależą od iteracji pętli, dlaczego nie wykonać matematyki?

int i, j;//That have some meaningful values in them?
for( int counter = 0; counter < count_max; ++counter )
    do_something (counter+i, counter+j);

Lub, bardziej szczegółowo odwołując się do przykładu PO:

for(int i = 0; i != 5; ++i)
    do_something(i, j+i);

Zwłaszcza jeśli przechodzisz do funkcji według wartości, powinieneś otrzymać coś, co robi dokładnie to, czego chcesz.

xaviersjs
źródło