Szybkość preinkrementacji, a postinkrementacji

0

Spotkałem się ze stwierdzeniem, że ++i jest szybsze od i++.
Co wy o tym myślicie?

0

Generalnie? Że komuś odbiło. Różnica występuje tylko wtedy, kiedy z wynikiem coś chcesz zrobić - ++i zwraca wynik operacji, i++ zwraca wartość przed operacją, czyli jedną wartość bierze na wynik, drugą modyfikuje. Póki nigdzie nie przypisujesz wyniku inkrementacji to różnica nie istnieje, zaś kiedy musisz przypisać to wtedy różnicy nie ma z innego powodu - i tak musisz użyć konkretnej formy...

0

Moim zdaniem to szybkością się to nie różni. Choć zawsze można to sprawdzić w jakiejś pętli. Nawet jeśli jest jakaś różnica, to nie wydaje mi się, żeby była aż tak duża, aby dało się ją zauważyć/zmierzyć. Może to też zależeć od środowiska, kompilatora, układu planet... Tak czy inaczej, wątpię czy jest czym się przejmować :)

0

@Świętowit:
@pcmcymc:
Może chodzi o to, że teoretycznie postinkrementacja musi:

  1. Zapamiętać aktualną wartość inkrementowanej zmiennej w osobnym miejscu.
  2. Dokonać inkrementacji.
  3. Zwrócić zapamiętaną wartość (zwykle idzie ona w próżnię).

Tymczasem preinkrementacja po prostu:

  1. Dokonuje inkrementacji.
  2. Zwraca wynik.

To trochę mniej roboty.

Co nie znaczy, że w praktyce tak właśnie jest, gdy wynik idzie w próżnię. Pewnie to właśnie miałeś na myśli, @Świętowit, gdy napisałeś, że różnica jest tylko gdy NIE olewamy wyniku, tylko coś z nim zrobimy.

No bo od czego jest kompilator? Nie tylko od wypluwania kodu maszynowego. Również od wypluwania możliwie optymalnego kodu maszynowego. Myślę, że post/preinkrementacja to dobry kandydat do optymalizacji. Jeśli nie korzystamy z wyniki postinkrementacji, to kompilator może wygenerować dla niej kod tak samo optymalny, jak dla preinkrementacji.

Więc bym się tym wszystkim nie przejmował. To kwestia bardzo niskopoziomowa, optymalizacyjna. Nie ma sensu się nad tym zastanawiać, gdy nie trzeba.

A zresztą zawsze można zrobić benchmark i sprawdzić, który operator będzie szybszy na danym kompilatorze.

0
bswierczynski napisał(a)

A zresztą zawsze można zrobić benchmark i sprawdzić, który operator będzie szybszy na danym kompilatorze.

Przed chwilą sprawdziłem rezultat kompilacji gcc pętli for z ++i oraz i++. Wygenerowany kod był identyczny. Więc różnicę można między bajki włożyć, przynajmniej jeśli chodzi o ten kompilator.

0

Mnie uczyli, że preinkrementacja jest szybsza, bo chyba nie ma kopiowania (ztcp). Tyle, że już C++ trochę zapomniałem, a odnosiło się to tylko do typów złożonych (struktur) z przeciążonymi operatorami postinkrementacji czy preinkrementacji. Instrukcja "++i;" czy "i++;" jeśli i jest intem czy charem czy podobnym będzie zawsze tłumaczyć się do (instrukcji asemblerowej) "inc cośtam".

0

Czym jet ZTCP?

0

W wygenerowanym kodzie maszynowym roznica jest w kolejnosci operacji, a nie w ich ilosci. Stad nie ma roznicy w wydajnosci, jak pisal Swietowit. Mit wzial sie chyba z tego, ze inaczej sie wyobraza dzialanie tych operacji w jezykach wyzszego poziomu niz to sie dzieje naprawde.

0
johny_bravo napisał(a)

W wygenerowanym kodzie maszynowym roznica jest w kolejnosci operacji, a nie w ich ilosci.

Bzdura, różnica jest właśnie w ilości... Inkrementacja jest modyfikacją 'w miejscu', więc gdzieś poprzednią wartość dodatkowym kosztem, dodatkową instrukcją, zachować musisz. Jeżeli wynik jest zbędny to obie inkrementacje mają identyczną formę.

Różnica w kolejności tylko w pojedynczych przypadkach ma rację bytu.

0

Wrzuciłem przykłady (mam nadzieję, że wykładowca mnie nie zabije):
iterator: http://pastebin.4programmers.net/678
liczba wymierna: http://pastebin.4programmers.net/679

Zresztą wystarczy zajrzeć do Wikipedii: http://en.wikipedia.org/wiki/Operators_in_C_and_C%2B%2B żeby zobaczyć, że:

  • operator prefiksowy: modyfikuje siebie i potem zwraca referencję do siebie,
  • operator postfixowy: tworzy kopię siebie, inkrementuje się i potem zwraca kopię,

Typy wbudowane mają inną zasadę działania, dla takiego inta czy floata przecież nie ma żadnej klasy, która by go opisywała.

pcmycmc:
ZTCP = AFAIR
ZTCW = AFAIK
ATSD = BTW
MSPANC = mogłem się powstrzymać, ale nie chciałem

edit:
Sry, bswierczynski napisał już to co ja chciałem przekazać :)

0
donkey7 napisał(a)

Typy wbudowane mają inną zasadę działania, dla takiego inta czy floata przecież nie ma żadnej klasy, która by go opisywała.

Są ogólne zasady co do działania inkrementacji/dekrementacji, której wszystkie typy powinny się trzymać, takie jakie mają właśnie typy proste. Stąd kwestia l-value preinkrementacji i r-value postinkrementacji.

0

z tego co ja slyszalem, to preinkrementacja moze byc odrobinke xD szybsza...

chociaz sprawny kompilator odpowiednio zooptymalizuje postinkrementacje np. w petli...

btw. mimo wszystko chyba zwracanie na to uwage, to troszke nadgorliwosc...

0

Dobra wyjade z asm'em znowu, bo az sie prosi ;p

Mamy taki kod w C.

#include <stdio.h>

int main(void) {
1)	int a = 0;

2)	a++;
3)	++a;

4)	while(a++ < 6);

5)	while(++a < 6);
}

Po kompilacji z opcja -S i masm=intel w gcc zaznaczone kawalki wygladaja tak

mov	DWORD PTR [ebp-4], 0          ; te [ebp-4] to "zmienna a"
lea	eax, [ebp-4]                          ; adres zmiennej a do eax
inc	DWORD PTR [eax]                 ; zwiekszamy o 1, czyli postinkrementacja nasza
  1. preinkrementacja w tym przypadku wyglada identycznie
lea	eax, [ebp-4]
inc	DWORD PTR [eax]
  1. i 5)
L2:
	mov	edx, DWORD PTR [ebp-4]          ; zmienna jest zapisywana do edx, poniewaz mamy
                                                               ; postinkrementacje w petli, czyli odlozona na bok zostaje
	lea	eax, [ebp-4]                             ; reszta analogicznie jak do 2) i 3)
	inc	DWORD PTR [eax]
	cmp	edx, 5                                      ; mniejsza juz o to
	jg	L4
	jmp	L2
L4:                                        ; tu sie zaczyna 5)
	lea	eax, [ebp-4]                             ; tu mamy preinkrementacjie, wiec zmienna nie jest nigdzie
                                                               ; odkladana, czyli teoretycznie szybciej
	inc	DWORD PTR [eax]
	cmp	DWORD PTR [ebp-4], 5
	jg	L5
	jmp	L4

Jak widac roznice sa niewielkie, a w przypadku 2) i 3) nie ma zadnej, wiec takie zapisanie wartosci zmiennej naszej 'a' do edx w przypadku 4) to tak naprawde NIC na dzisiejszym sprzecie, wiec ja bym sie optymalizacji nie doszukiwal w przypadku xxxxinkrementacji.

//q: podkreslony compiler, zeby nie bylo niedoczytan

0

Hihi. Postinkrementacja dla typów prostych, jeśli wynik NIE idzie w próżnię, może być szybsza niż preinkrementacja, przynajmniej na nowoczesnych procesorach. Trochę dziwne, ale już tłumaczę.

Postinkrementacja (b = a++)

  1. skopiuj wartość z rejestru a do b
  2. zwiększ wartość rejestru a

Preimkrementacja (b = ++a)

  1. zwiększ wartość rejestru a
  2. skopiuj z a do b

W pierwszym przypadku jest RAR, w drugim jest RAW. Dla większości nowych procków z rozbudowanymi potokami ta druga sytuacja jest znacznie gorsza.

W przypadku, gdy wynik idzie w próżnie (nie ma przypisania do b w moim przykładzie), kompilatory powinny generować identyczny kod.

0

Boże, ale [CIACH!]... ten od asma też chyba do końca nie rozumie co testuje. Przyjdzie quetzalcoatl to powie co swoje i zamknie ten cyrk. Ciekawe który to już wątek o tym samym? Z każdym kolejnym coraz lepsze odkrycia odchodzą...

Najśmieszniejsze, że o działaniu pre/postinkrementacji w C++ najwięcej do powiedzenia mają użytkownicy Javy.

// wyrażaj się, z łaski swojej - Ł

0

Pozwolę sobie również zauważyć, że nawet jeśli byłaby jakaś realna i pewna różnica w prędkości, to nie należy się nią przejmować. Bo prawie zawsze nie będzie ona miała znaczenia. I gdy mówię "prawie zawsze", to mam na myśli "PRAWIE ZAWSZE". Czyli nie, nie w tym programie, który teraz piszesz i nie w następnym, ani w poprzednich 10.

Raz na ruski rok trafi się wąskie gardło, w którym faktycznie tego typu optymalizacje będą miały znaczenie.

Odnoszę wrażenie, że nad takimi rzeczami najwięcej zastanawiają się ci, co tak naprawdę nie bardzo wiedzą, jak optymalizuje się programy. To się nie sprowadza do paru prostych zasad typu "używaj ++i, a nie i++". Taka zmiana, nawet gdyby coś dawała (a nie daje), i tak nie zmieni złożoności obliczeniowej algorytmu.

Mało tego -- nawet jeśli chcemy poprawić nieco współczynnik przy danej złożoności, to prawie na pewno wybierzemy totalnie nieistotne miejsce, jeśli będziemy je lokalizowali na podstawie własnej intuicji, a nie porządnych pomiarów wydajności. Naprawdę, to nic, że zmienisz jedną niskopoziomową instrukcję w funkcji, która jest wywoływana 10, albo nawet 1000 razy i to jedynie przy starcie aplikacji. To NIC nie zmieni i czas. NIC. Nie "ten 0.001% to zawsze coś!", bo nikt tego nie zauważy. Lepiej zlokalizować wąskie gardło (testami!) i tam zmienić parę instrukcji, przyspieszając aplikację od razu o 10%.

Wydaje mi się, że takie niskopoziomowe optymalizacje mogą jeszcze mieć jakiś sens w językach skryptowych, jak np. w JavaScripcie czy PHP (ale to też dużo rzadziej, niż myślimy, a samo ++i/--i praktycznie nigdy nie robi różnicy). C++ jest jednak kompilowane, a kompilator ma SPORE pole do popisu jeśli chodzi o optymalizacje. Niski poziom warto pozostawić kompilatorowi, a samemu zająć się czymś pożytecznym, ew. wracając do niskiego poziomu gdy mamy pewność, że kompilator się nie spisał i że jest to faktycznym problemem.

Rozmawiać sobie można choćby z ciekawości o czymkolwiek, ale warto zdawać sobie sprawę, które dywagacje są stratą czasu, a które nie. Można mieć fałszywe przeświadczenie, że rozmawia się o optymalizacji i że to coś dobrego, ale zamiast rozmawiania o nic nie dającej optymalizacji można by się czegoś nauczyć o pożytecznej. Samo poznanie procedury "pisz zrozumiale, potem benchmarkuj, potem optymalizuj wąskie gardła" jest IMO znacznie ważniejsze niż 10 rad typu "co jest szybsze: if czy switch?". Czasem trzeba z góry planować architekturę systemu pod kątem optymalizacji, ale tego nie robi się na pałę. Poza tym w ortogonalnym systemie dodanie optymalizacji nie jest aż takie trudne.

0
donkey7 napisał(a)

W tym kodzie, o dziwo, postinkrementacja jest szybsza, 26s vs 20s. No ale znowu wynik jest inny

Wcale nie o dziwo. A nie mówiłem?
Ale IMHO kod jest do bani, nikt tak nie pisze, to nawet nie jest poprawne C. A różnica wydajności nieistotna, patrząc po tym, ileś tych inkrementacji tam nawstawiał.

Wniosek jest taki, że jeśli potrzebujesz wyniku pre-/postinkrementacji na typach prostych to staraj się zapisać algorytm tak, aby korzystać z tej drugiej. Choć kompilatory takie głupie nie są, i napisałem już wcześniej potrafią zapchać dziurę w RAW (w normalnych przypadkach, a nie takich jak kod donkey7), tak aby nie marnować potoku, więc znowu na jedno wyjdzie. Nigdy nie robiłem, ani nie musiałem robić takich optymalizacji - dywagacje są czysto teoretyczne.

[rant] BTW: W ogóle operatory pre- i postinkrementacji są niepotrzebne. [/rant]

@up: bardzo dobry post, podpisuję się w całości :)

//q: wyciety fragment odnoszacy sie do usunietego postu

0

Ja używam preinkrementacji tylko z jednego powodu: gdy zwykłe zmienne int zmienią się na iteratory, pre inkrementacja staje się szybsza.

0
rnd napisał(a)

Ja używam preinkrementacji tylko z jednego powodu: gdy zwykłe zmienne int zmienią się na iteratory, pre inkrementacja staje się szybsza.

Mógłbyś dać przykład?

0

Generalnie chodziło mi o sytuację gdy wprowadzamy zmianę w kodzie, np. tablicę zmieniamy na listę. Prosta iteracja po tablicy może wyglądać tak
for(int i = 0; i < n; i++)
i tutaj nie ma żadnego znaczenia czy jest ++i czy i++.
Natomiast gdy zmieni się typ kolekcji to iteracja może wyglądać tak:
for(list<int>iterator i = lst.begin(); i != lst.end(); i++)
I tutaj już ma znaczenie czy zastosujemy preinkrementacje czy postinkrementacje. Stosując domyślnie preinkrementację mniej kodu trzeba adaptować przy ewentualnych zmianach.

0

I tutaj już ma znaczenie czy zastosujemy preinkrementacje czy postinkrementacje. Stosując domyślnie preinkrementację mniej kodu trzeba adaptować przy ewentualnych zmianach.

IMHO to jest właśnie coś, czego nie lubię w C++ i językach mu podobnych: zmuszanie programisty do specyfikowania zbędnych dupereli technicznych, niezwiązanych z problemem. Mniej kodu trzeba by było adaptować, gdybyś mógł napisać wprost: "wykonaj dla każdego elementu kolekcji". A sposób zwiększania iteratora pozostawiłbym implementacji kompilatora / biblioteki (a może w ogóle nie musi być tam pod spodem żadnego iteratora).

Inna rzecz, czemu kompilator nie może dokonać podobnych optymalizacji dla iteratora? Wydaje mi się, że teoretycznie takie coś byłoby możliwe. W końcu widzi, że wynik nie jest potrzebny - jeśli kod inkrementacji udałoby się zrobić inline, to chyba dalszy przebieg optymalizacji powinien być oczywisty (usunięcie zbędnego kopiowania i wywołania konstruktora kopiującego, o ile da się udowodnić, że nie ma skutków ubocznych).
Jak tam w nowoczesnych kompilatorach: jest taka optymalizacja, czy nie?

0

Inna rzecz, czemu kompilator nie może dokonać podobnych optymalizacji dla iteratora? Wydaje mi się, że teoretycznie takie coś byłoby możliwe. W końcu widzi, że wynik nie jest potrzebny - jeśli kod inkrementacji udałoby się zrobić inline, to chyba dalszy przebieg optymalizacji powinien być oczywisty (usunięcie zbędnego kopiowania i wywołania konstruktora kopiującego, o ile da się udowodnić, że nie ma skutków ubocznych).

Operator postinkrementacji i preinkrementacji mogą być zupełnie inaczej zaimplementowane i mogę robić odmienne rzeczy (zły styl programowania, ale kompilator nie może robić takich założeń). Kompilator musiał by być na prawdę inteligentny, żeby wykryć że preinkrementacje i postinkrementacja bez użycia wyniku to jest to samo.
Sam jestem ciekaw czy już jest coś takiego możliwe.

0
rnd napisał(a)

Operator postinkrementacji i preinkrementacji mogą być zupełnie inaczej zaimplementowane i mogę robić odmienne rzeczy (zły styl programowania, ale kompilator nie może robić takich założeń). Kompilator musiał by być na prawdę inteligentny, żeby wykryć że preinkrementacje i postinkrementacja bez użycia wyniku to jest to samo.
Sam jestem ciekaw czy już jest coś takiego możliwe.

Nie musi wykrywać, że to to samo. Może przecież bez problemu wykryć, że wynik postinkrementacji nie jest używany, i na tej podstawie wyrzucić kopiowanie obiektu.

0

Nie musi wykrywać, że to to samo. Może przecież bez problemu wykryć, że wynik postinkrementacji nie jest używany, i na tej podstawie wyrzucić kopiowanie obiektu.

To co powiesz na takie coś:

Clazz Clazz::operator++(int) {
 Clazz c(*this);
 sendMailToFriends(); //albo cokolwiek co modyfikuje stan lub ma skutki uboczne
 ++c;
 return c;
}

Co musi zrobić kompilator:

  • Sprawdzić czy dowolna funkcja wywołana w ciele (w tym konstruktor kopiujący) nie ma skutków ubocznych
  • sprawdzić czy jest wywołany operator preinkrementacji
0

No, sytuacja, ze operator inkrementacji ma jakieś inne skutki uboczne to chyba nie jest normalna? ;)
Ale i tak - nadal przecież kopiowanie można wywalić, skoro wynik nie jest użyty.
Cały czas mówię o kodzie który został włączony jako inline. Więc wcale kompilator nie musi zamieniać operatora postinkrementacji na preinkrementację. Wystarczy, że z postinkrementacji wywali robienie kopii, a resztę kodu zostawi jak jest.

Poza tym mam wątpliwości co do linii "++c" - tam nie powinno być zwiększenie czegoś w obiekcie wskazywanym przez this, a nie operacja na kopii?

0

Poza tym mam wątpliwości co do linii "++c" - tam nie powinno być zwiększenie czegoś w obiekcie wskazywanym przez this, a nie operacja na kopii?

Tak masz rację: powinno być ++(*this) ;)
A co jeśli zamiast ++(*this) jest wywoływana funkcja inc() w operatorze preinkrementacji a operatorze postinkrementacji jest robiona inkrementacja bezpośrednio? Nie sądze żeby kompilator mógł zgadnąć że to jest to samo.
To mogło by być zoptymalizowane gdyby:

  1. Kompilator mógł wykrywać czy funkcja ma skutki uboczne (to pewnie nie taki problem)
  2. Użytkownik nie może utrudniać optymalizacji kompilatorowi (z tym pewnie gorzej)

Ale i tak - nadal przecież kopiowanie można wywalić, skoro wynik nie jest użyty.

O ile kopiowanie nie ma skutków ubocznych :D A tutaj akurat można wymyślić sensowny przykład: każdy konstruktor dodaje nowo stworzony obiekt na globalną listę obiektów (a destruktor usuwa ten obiekt).
Czegoś takiego kompilator na pewno nie będzie mógł zoptymalizować.

0

Hmm, no z tymi konstruktorami kopiującymi z efektami ubocznymi to chyba nie do końca tak. Bo np. RVO kompilatory robić mogą, nawet jeśli spowoduje to niechcący "ominięcie efektu ubocznego". Chyba, że RVO jest na jakiś specjalnych zasadach.

0

W sprawie ++i/i++ bswierczynski i rnd juz powiedzieli wszystko. Preinkrementacja ma duże prawdopodobieństwo byc szybsza, zwłaszcza dla typów złożonych - np. iteratorów. Oczywiście mam na myśli preinkrementację LOGICZNĄ, bo jak już mówimy o przeciążaniu operatorów, to chyba oczywiste, że ++i może sobie liczyć sinusa hiperbolicznego.

W praktyce zaś, zwłaszcza dla typów prostych - nie wiadomo, zależnie od widzimisię fazy optymalizacji. Ja osobiście silnie rozróżniam ++i od i++ i stosuję jedno albo drugie dokładnie w zależności od tego co chcę w danej linii kodu osiągnąć. W momencie "bez znaczenia", stosuję ++i na wypadek przyszlej przeróbki na iterator.

@Królik: co do foreach, w Javie tez go nie bylo na poczatku :) a tak na serio, to w C++, nawet obecnym, on istnieje. Sa template'y, które dokładnie tak się zachowują, jednak nie są najwygodniejsze. Jest zaś w BOOST makro FOREACH, które jest w 100% wygodne, użyteczne i zachowujące się dokładnie tak jak byś się tego spodziewał. See, IIR, boost::foreach

// OFTTOP
@RVO/cctor - tak, kompilator ma prawo pominąć cctor i używać obiektu z ramki caller'a. Podobna 'niejasność' istnieje np. w momencie inicjalizacji "type x = blah;" gdzie widać operator =, ale w praktyce odpalany jest konstruktor z argumentem blah. Pomijając na chwilę istnienie czegos takiego jak deklaracja-z-inicjalizacja, można to traktować dokladnie jak RVO, gdyz, z racji implicit conversion:
"type x = blah" === "type x = type(blah)" === "ctor+cctor"
efekt zas jest wiadomy, więc mozna declare+init traktowac jakby uber-RVO:)

Mówiąc skrótem i już nie żartem:

  • ctor/konstruktory, w tym kopiujące
  • operatory przypisania
  • operatory konwersji, w tym konstruktory 1-argumentowe, explicit i implicit
  • dtor/destruktory
    mają robić dokładnie to do czego te terminy zdefiniowano, robić tylko to i nic więcej.
    Wy/My musimy zaś pamiętać, że one nie są dla Was/Nas i naszego widzimisię, tylko - że są one, aby KOMPILATOR był w stanie prawidłowo wygenerować kod obsługi cyklu życia obiektu. Machnąłem to italikiem, gdyż nie nawidzę takich (edit:wodolejskich) stwierdzeń, niestety takie sformułowanie pasuje najlepiej. Nie wolno nam zakładać, że
  • tu sie odpali cctor
  • a tu bedzie RVO
    gdyż nikt nie ma nad tym bezposredniej kontroli, jedyne co dostajemy, to gwarancję że obiekt po-fakcie będzie zgodny z post-conditionami naszej baterii c/ctorow/op=:
  • CTOR ma zostawiac obiekt skonstruowany i gotowy do dalszej pracy, nawet jesli dalsza praca zaklada oznacza N-phase initialization
  • CCTOR ma tworzyc "z punktu widzenia uzytkownika, dokladna i niezalezna kopie" i zostawiac ja gotowa do pracy
  • OP= ma robic to co CCTOR z re-uzyciem *this, czyli starego obiektu
  • DTOR ma sprzatac wszystko, co obiekt posiada, ale nie zostaloby sprzatniete automatycznie przez kompilator lub inne obiekty.
    Jeżeli postanowiliśmy sobie, że ctor/cctor/op= robią kompletnie inne, różne i niespójne ze sobą rzeczy -- sorry, sami strzelilismy sobie w stopę lub sami swiadomie probujemy skorzystac ze specyfiki naszego kompilatora.
    \ OFTTOP

edit:
ps. królik, świętowit - przepraszam za hurtową rabunkową wycinkę ostatnich postów. Były sensowne, ale wywaliłem poprzedzający je n-ty z rzędu bzdurny donkey7'a i Wasze posty straciły logiczne powiązanie z resztą wątku

ps2: donkey7: mam na myśli Twoje zamotanie z multiple-reads/writes-in-a-single-expression. Jest to niezdefiniowane i kropka. Nie obejdziesz tego nijak, kombinowaniem możesz co najwyżej to rozciągnąć na inne aspekty.. efekty zapisow w stylu ++i + ++i + i++ czy funkcja(i++,++i, &i, &i) po prostu sa niezdefiniowane [zwlaszcza jezeli ta funkcja zapisuje cos pod wskazniki z arg3 i arg4].
W C++, w jednym wyrazeniu masz prawo do nastepujacych operacji na zmiennej "i":

  • albo czytania dowolna ilosc razy: x = i + i + i + 2*i
  • albo zapisywania dowolna ilosc razy: i = i = i = x -- acz sens jest watpliwy
  • czytania wiele i zapisywania jeden raz, pod warunkiem, ze zapis i wszystkie odczyty sa po rożnych stronach przypisań: i += ( i + i + 2*i)
    Inne rzeczy sa niedozwolone/niezdefiniowane. Odczyt i zapis nie mogą miescic się po tej samej stronie wyrażenia. Możesz nawet probowac wymusic priorytet za pomocą nawiasów: x = ((++i) + ++i) + i++ ale to i tak nie będzie zdefiniowane, tak jak i nie jest zdefiniowany wynik wyrazenia: x += (2 + (x=6))

1 użytkowników online, w tym zalogowanych: 0, gości: 1