Zacznę od rozważań nad potencjalnymi rozwiązaniami, a potem przejdę do wyników empirycznych (które przygotował nism0).
Więc... które miejsca są UB/niewiadome/zależne od kompilatora?
Po pierwsze, nie wiadomo które a zostanie podstawione pierwsze w równaniu z drugiego polecenia (przez podstawienie mam na myśli skopiowanie wartości z pamięci do jakiegoś podręcznego rejestru). Opcje są dwie:
Opcja 1. Najpierw podstawienie pierwszego a, potem wyliczenie pre-inkrementacji i podstawienie drugiego a (post-inkrementację na chwilę pominę):
a = a + ++a;
Krok 1. Podstawienie pierwszego a.
a = 5 + ++a; (a==5)
Krok 2. Pre-inkrementacja a.
a = 5 + a; (a==6)
Krok 3. Podstawienie drugiego a.
a = 5 + 6; (a==6)
Krok 4. Wyliczenie dodawania.
a = 11;
Opcja 2. Najpierw wykonana zostanie pre-inkrementacja, łącznie z zapisem wyniku do pamięci, a dopiero później nastąpi podstawienie pierwszego a.
a = a + ++a;
Krok 1. Pre-inkrementacja a.
a = a + a; (a==6)
Krok 2 i 3. Podstawienie pierwszego i drugiego a.
a = 6 + 6; (a==6)
Krok 4. Wyliczenie dodawania.
a = 12;
Czyli już z samej pre-inkrementacji i podstawiania dostajemy dwa różne wyniki (11 i 12).
Drugi UB związany jest z post-inkrementacją i potencjalnie trywialną linijką a = a++. Jak się okazuje, są tutaj również dwie opcje, które rozważę posługując się kodem pomocniczym w postaci int a = 5; a = a++;.
Terminologia:
a_mem - a w pamięci (np. jako lokalna zmienna na stosie)
a_copy - kopia a w jakimś podręcznym rejestrze
Opcja 1. Wynikowy kod ma następującą formę (w pseudo-assembly):
Warunki początkowe: (a_mem == 5, a_copy == brak)
Krop 1. Podstawienie a do równania.
a = 5++; (a_mem == 5, a_copy == 5)
Krok 2. Post-inkrementacja na zmiennej w pamięci.
a = 5; (a_mem == 6, a_copy == 5)
Krok 3. Przypisanie, czyli a_copy leci do a_mem.
(a_mem == 5, a_copy == brak)
W powyższym wypadku wynik post-inkrementacji a zaginął w akcji. Tj. niby zostało zapisane do pamięci, ale po chwili operacja przypisania (=) wrzuciła finalny wynik obliczeń (czyli 5) do zmiennej a w pamięci nadpisując jednocześnie wynik post-inkrementacji. (prawdę mówiąc zawsze uważałem, że operacja post-inkrementacji jest deferowana na sam koniec wszystkich obliczeń, więc uznałbym to zachowanie za bug kompilatora)
Opcja 2. Post-inkrementacja dzieje się po przypisaniu.
Warunki początkowe: (a_mem == 5, a_copy == brak)
Krop 1. Podstawienie a do równania.
a = 5++; (a_mem == 5, a_copy == 5)
Krok 3. Przypisanie, czyli a_copy leci do a_mem.
(zostaje a++) (a_mem == 5, a_copy == brak)
Krok 2. Post-inkrementacja na zmiennej w pamięci.
(a_mem == 6, a_copy == brak)
Czyli post-inkrementacja zostaje faktycznie zdeferowana na koniec obliczeń.
Podsumowując, ostateczny wynik a = a++ + ++a to:
Opcja 1 i 1: 5+6 i wynik post-inkrementacji MIA, razem 11
Opcja 2 i 1: 6+6 i wynik post-inkrementacji MIA, razem 12
Opcja 1 i 2: 5+6 i post-inkrementacja zdeferowana, razem 12
Opcja 2 i 2: 6+6 i post-inkrementacja zdeferowana, razem 13
Jak wspomniałem na początku, nism0 porobił trochę testów (empirycznych), z czego wyszła następująca tabelka (update: jak słusznie zauważył nonek, kolumny 1 i 2 oraz 4 z 5 w tabeli były zamienione miejscami względem wyników; teraz już jest OK, thx nonek ;>) (update 2: jeszcze jedna seria literówek poprawiona (dot. C#), thx qyon):
Kod1 | Kod 2 | Kod 3 | Kod 4 | Kod 5 | Kod 6 |
---|---|---|---|---|---|
int a = 5; a = a++ + a++; | int a = 5; a = a++ + ++a | int a = 5; a = ++a + a++; | int a = 5; a = ++a + ++a; | int a = 5; a = a++; | int a = 5; a = a + ++a; |
Kompilator/Język | Wersja | wynik 1 | wynik 2 | wynik 3 | wynik 4 | wynik 5 | wynik 6 |
---|---|---|---|---|---|---|---|
gcc | 2.95 | 12 | 13 | 14 | 13 | 6 | 12 |
gcc | 4.1 | 12 | 13 | 14 | 13 | 6 | 12 |
gcc | 4.2 | 12 | 13 | 14 | 13 | 6 | 12 |
gcc | 4.2.1 Apple | 12 | 13 | 14 | 13 | 6 | 12 |
gcc | 4.3 | 12 | 13 | 14 | 13 | 6 | 12 |
gcc | 4.3.3 | 12 | 13 | 13 | 14 | 6 | 12 |
gcc | 4.4.4 | 12 | 13 | 13 | 14 | 6 | 12 |
gcc | 4.6.0 (exp.) | 12 | 13 | 14 | 13 | 6 | 12 |
gcc | 4.5.1 MinGW64 | 12 | 13 | 13 | 14 | 6 | 12 |
tcc | 0.9.25 | ?? | ?? | ?? | ?? | 5 | 12 |
bcc | 0.16.17 | ?? | ?? | ?? | ?? | 5 | 12 |
Microsoft C/C++ | 16.00.30319.01 (80x86) | 12 | 13 | 13 | 14 | 6 | 12 |
Embarcadero C++ | 6.31 for Win32 | 12 | 13 | 13 | 14 | 6 | 12 |
Intel C++ | 12.0.1.127 | 12 | 13 | 13 | 13 | 6 | 12 |
Keil C | 9.02 | 11 | 12 | 12 | 13 | 6 | 12 |
SDCC | 3.0.1 #6092 | 11 | 12 | 13 | 14 | 5 | 12 |
clang | 2.8 | 11 | 12 | 12 | 13 | 5 | 11 |
clang | 1.6 Apple | 11 | 12 | 12 | 13 | 5 | 11 |
PHP | 5.2.10 | 11 | 12 | 12 | 13 | 5 | 12 |
java | 1.6.0_06 | 11 | 12 | 12 | 13 | 5 | 11 |
javac | 1.4.2_12 | 11 | 12 | 12 | 13 | 5 | 11 |
java | 1.6.0_21 | 11 | 12 | 12 | 13 | 5 | 11 |
javac | 1.6.0_22 | 11 | 12 | 12 | 13 | 5 | 11 |
C# | 2.0 | 11 | 12 | 12 | 13 | 5 | 11 |
C# | 4.0 | 11 | 12 | 12 | 13 | 5 | 11 |
C# | Mono 2.6.4 | 11 | 12 | 12 | 13 | 5 | 11 |
Borland Turbo C++ for DOS | 2.01 | 12 | 13 | 13 | 14 | 6 | 12 |
HiSoft C for ZX Spectrum | 1.3 | 11 | 12 | 12 | 13 | 5 | 12 |
Podziękowania za dodatkowe wyniki dla: Icewall, Krzysztof Kotowicz (za PHP 5.2.10), mlen (za 2x clang, 2x gcc), none'a (za 2xJava), Keraj (za 2x Java), MDobak (za SDCC i Keil C), garbaty_lamer (za 3xC#, Turbo C++, HiSoft C), Xgrzyb90 (za gcc 4.4.4), no_name (za gcc 4.3.3), dikamilo (za mingw64 4.5.1)
Update: kapitalny screen z HiSoft C for ZX Spectrum który garbaty lamer wrzucił w komentarzach:
Wyniki z innych kompilatorów jak zwykle mile widziane (kod do testów, autorstwa nism0, umieściłem poniżej). Wyniki z innych języków programowania posiadających pre- i post-inkrementacje również mogą być ciekawe.
I tyle na dzisiaj. Szczęśliwego nowego roku ;>
Update:
P.S. Zachęcam do rzucenia okiem na komentarze, szczególnie na komentarz Rolek'a (Rolka? ;>), który zaproponował test (kod jest w jego komentarzu) z przeciążeniem operatorów (wyniki by Rolek (MSVC++) & me (g++)):
Kod1 | Kod 2 | Kod 3 | Kod 4 | Kod 5 | Kod 6 |
---|---|---|---|---|---|
int a = 5; a = a++ + a++; | int a = 5; a = a++ + ++a | int a = 5; a = ++a + a++; | int a = 5; a = ++a + ++a; | int a = 5; a = a++; | int a = 5; a = a + ++a; |
Kompilator/Język | Wersja | wynik 1 | wynik 2 | wynik 3 | wynik 4 | wynik 5 | wynik 6 |
---|---|---|---|---|---|---|---|
Microsoft C/C/++ (bez przeciążenia) | 16.00.30319.01 | 12 | 13 | 13 | 14 | 6 | 12 |
Microsoft C/C/++ (z przeciążeniem) | 16.00.30319.01 | 11 | 13 | 12 | 14 | 5 | 12 |
g++ (bez przeciążenia) | 4.5.0 MinGW | 12 | 13 | 13 | 14 | 6 | 12 |
g++ (z przeciążeniem) | 4.5.0 MinGW | 11 | 13 | 12 | 14 | 5 | 12 |
Poza tym, krlm rzucił dobry link o sequence points: http://en.wikipedia.org/wiki/Sequence_point.
By the way...
If you'd like to learn SSH in depth, in the second half of January'25 we're running a 6h course - you can find the details at hexarcana.ch/workshops/ssh-course
Komentarz garbatego_lamera dot C# jest również ciekawy i warty uwagi:
Nudne to wklejanie takich samych wyników. To, co w niektórych językach jest undefined, w innych jest perfectly defined. Cytat z §7.3 specyfikacji:
Operands in an expression are evaluated from left to right. For example, in F(i) + G(i++) * H(i), method F is called using the old value of i, then method G is called with the old value of i, and, finally, method H is called with the new value of i. This is separate from and unrelated to operator precedence.
End of update.
Oryginalny kod do testów:
#include <stdio.h>
int main(void)
{
int a = 5, b = 5, c = 5, d = 5, e = 5, f = 5;
// test pierwszy
a = a++ + a++;
printf("%i \n",a);
// test drugi
b = b++ + ++b;
printf("%i \n",b);
// test trzeci
c = ++c + c++;
printf("%i \n",c);
// test czwarty
d = ++d + ++d;
printf("%i \n",d);
// test piaty
e = e++;
printf("%i \n",e);
// test szosty
f = f + ++f;
printf("%i \n",f);
// koniec testow
return 0;
}
Appendix 4:
Komentarz by Cem Paya (ad Java0:
--start--
Similar to C#, this is also not a riddle for Java because Java defines
evaluation to be strictly left-to-right.
See section 15.7 here for some examples with side-effects as in your case:
http://java.sun.com/docs/books/jls/second_edition/html/expressions.doc.html
In C++ where it is undefined, the result can also depend on the
optimization level used during compilation, which can change the
number of times a value is referenced. eg the compiler expects "a" to
not change its value during the evaluation and may optimize other
occurences to the same one it fetched.
==end==
Comments:
11,12,12,13,5,12
clang version 2.8 (branches/release_28)
11
12
12
13
5
11
Apple clang version 1.6 (tags/Apple/clang-70)
11
12
12
13
5
11
gcc version 4.2.1 (Apple Inc. build 5664)
12
13
13
14
6
12
gcc version 4.6.0 20101106 (experimental) (GCC)
12
13
13
14
6
12
12
13
13
14
6
12
12
13
13
14
6
12
http://nism0.lunarii.org/dump/testy.html
przepraszam :D
11
12
12
13
5
11
12
11
13
12
5
11
javac 1.6.0_22
12
11
13
12
5
11
Wątpię, by to się w międzyczasie zmieniało.
((a++) + a) * (a++) * (a++) / (a++)
Pozdrawiam
11
12
12
13
5
11
wiec miedzy wersjami mi sie nic nie zmienia ;). Ale nie jestem specjalista java wiec moze mam blad w kodzie.
11
12
12
13
5
11
[code]
#include <cstdio>
class Int
{
int m_val;
public:
Int(const int val = 0) : m_val(val) {}
Int(const Int& o) : m_val(o.m_val) {}
operator int() const { return m_val; }
Int& operator = (const Int& o) { m_val = o.m_val; return *this; }
Int& operator ++ () { ++m_val; return *this; }
Int operator ++ (int) { Int t = *this; ++*this; return t; }
Int operator + (const Int& r) const { return m_val + r.m_val; }
};
template<typename T> void foo(void)
{
T a = 5, b = 5, c = 5, d = 5, e = 5, f = 5;
// test pierwszy
a = a++ + a++;
printf("%i ",(int)a);
// test drugi
b = b++ + ++b;
printf("%i ",(int)b);
// test trzeci
c = ++c + c++;
printf("%i ",(int)c);
// test czwarty
d = ++d + ++d;
printf("%i ",(int)d);
// test piaty
e = e++;
printf("%i ",(int)e);
// test szosty
f = f + ++f;
printf("%i ",(int)f);
}
int main()
{
printf("
int: "); foo<int>();
printf("
Int: "); foo<Int>();
return 0;
}
[/code]
Wyniki:
int: 12 13 13 14 6 12
Int: 11 13 12 14 5 12
IDE: MsVC++2010EE
Kompilator: Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.30319.01 for 80x86
Może ktoś sprawdzi jakie wyniki daje np. g++ albo coś innego?
11
12
12
13
5
11
C# 2.0 (testowane przy pomocy LinqPad 2.31)
11
12
12
13
5
11
C#/Mono 2.6.4 (Linux/x86)
11
12
12
13
5
11
Nudne to wklejanie takich samych wyników. To, co w niektórych językach jest undefined, w innych jest perfectly defined. Cytat z §7.3 specyfikacji:
Operands in an expression are evaluated from left to right. For example, in F(i) + G(i++) * H(i), method F is called using the old value of i, then method G is called with the old value of i, and, finally, method H is called with the new value of i. This is separate from and unrelated to operator precedence.
HNY!
a = a++ + ++a;
a == 6+6+1 == 13
ponieważ:
C ma cos takiego jak wagi operatorow.
Wedlug nic pierw bedzie POSTINKREMENTACJA (a++),
dopiero pozniej bedzie PREINKREMENTACJA (++a),
następnie dodawanie.
jednakze POSTINKREMENTACJA jest liczona dopiero po ; dlatego też mamy
6 (++5) + 6 (drugi operator już preinkrementowany, więc jest wstawiana preinkrementowana wartosc z czlonu pierwszego) +1 (efekt postinkrementacji).
6+6+1=13. ;)
Mam nadzieje że nie zagmatwałem za bardzo.
Ktos na lekcjach z C i Operatorów nie uważał.
http://staff.elka.pw.edu.pl/~jarabas/dyd/prm/operatory.pdf
http://nadzieja.el-kfa.net/strony/operators.html
http://pl.wikibooks.org/wiki/C/Operatory#Priorytety_i_kolejno.C5.9B.C4.87_oblicze.C5.84
Wyniki dla gcc version 4.1.2 20080704 (Red Hat 4.1.2-48:
Wynik2 | Wynik1 | Wynik 3 | Wynik 4 | Wynik 5 | Wynik 6
12 | 13 | 13 | 14 | 6 | 12
Na dalsze kolumny nie patrzałem, więc nie wiem czy jest błąd, ale napewno w kolumny 1 i 2 są zamienione miejscami
Reading specs from /usr/lib/gcc/i486-slackware-linux/4.4.4/specs
Target: i486-slackware-linux
Configured with: ../gcc-4.4.4/configure --prefix=/usr --libdir=/usr/lib --enable-shared --enable-bootstrap --enable-languages=ada,c,c++,fortran,java,objc --enable-threads=posix --enable-checking=release --with-system-zlib --with-python-dir=/lib/python2.6/site-packages --disable-libunwind-exceptions --enable-__cxa_atexit --enable-libssp --with-gnu-ld --verbose --with-arch=i486 --target=i486-slackware-linux --build=i486-slackware-linux --host=i486-slackware-linux
Thread model: posix
gcc version 4.4.4 (GCC)
tomasz@darkstar:~/tmp$ gcc -o test test.c
tomasz@darkstar:~/tmp$ chmod +x test
tomasz@darkstar:~/tmp$ ./test
12
13
13
14
6
12
SDCC 3.0.1 #6092
11
12
13
14
5
12
Keil C 9.02
11
12
12
13
6
12
Tak się zastanawiam (i tutaj pytanie/prośba) czy mógłbym uzupełnić tabelkę o wasze wyniki (mam na myśli wyniki kompilatorów C/C++) ?
PS: Sam się zastanawiam czy by nie zrobić kilku testów dla innych języków.
Szczególnego ? Raczej nic (chociaż zależy dla kogo), ale zawsze się może przydać taki drobny spis. BTW nie mam zamiaru tego kontynuować w nieskończoność, raczej się skupiam na C/C++. Najbardziej tutaj zainteresowały mnie wyniki podane przez @MDobak, bo są to nietypowe kompilatory.
A używaj tych wyników do czego chcesz :).
@krlm
Sens jest prosty - wskazać, że coś takiego istnieje. Poza tym przyznaję, że ciekawi mnie jak który kompilator podchodzi do kompilacji, a z wyliczania UB można coś takiego próbować wnioskować.
Ad "co z innymi architekturami": wszelkie wyniki z innych architektur również mile widziane ;)
Ad link - thx, ciekawy ;>
Ad "lepsze spożytkowanie swoich mocy" - hehe przypuszczam również, że można lepiej spożytkować swoje moce, niż użycie ich by wytykać innym subiektywnie gorsze spożytkowanie mocy ;)
@Radom
Fajne wyrażenie :)
W drafcie n1336 standardu C99 pojawiają się dwa inne przykłady na UB z sequencami:
i = ++i + 1;
a[i++] = i;
Ten pierwszy jest imo dość ciekawy.
Pojawia się także:
EXAMPLE In the function call
(*pf[f1()]) (f2(), f3() + f4())
the functions f1, f2, f3, and f4 may be called in any order. All side effects have to be completed before
the function pointed to by pf[f1()] is called.
@nonek
Thx, fixed.
@Nikow
Trochę źle rozumiesz wagi operatorów (mają one znaczenie przy wyborze które z danych dwóch operacji wykonać najpierw, a nie przy wyborze co ma się wykonać pierwsze globalnie dla całego wyrażenia).
Rzuć okiem na link krlm'a.
Borland (obecnie Embarcadero) Turbo C++ 2.01 for DOS: 12, 13, 13, 14, 6, 12
HiSoft C 1.3 for ZX Spectrum (8 bit FTW!): 11, 12, 12, 13, 5, 12
(http://img94.imageshack.us/img94/7498/hisoftgynvael.png)
pozostałe emulatory (Amstrad, Amiga) i wirtualki (Solaris) niestety zdechły...
Woah, ale klasyki wyciągnąłeś ;)
Update wrzucony. Screen pozwoliłem sobie przerzucić na mój serwer i dodać do artykułu ;>
O ile tabela jest ciekawa, to prezentowane w niej wyniki nie powinny być w żadnym wypadku użyte do przewidywania zachowania konkretnego kompilatora. Jestem przekonany (choć nie mam jeszcze dowodów), że wyniki mogą różnić się w zależności od użytych opcji optymalizacji, lub nawet od konkretnego kodu. Specyfikacja definiuje to niezdefiniowane zachowanie, sprawdziłem dla ANSI C, dla C++ sprzed standaryzacji oraz ANSI/ISO C++. Nie wiem czy ze względu na copyright mogę podać treść, na wszelki wypadek nie podam, odsyłam do
- B. Kerninghan i D. Ritchie, "Język ANSI C", p. A7 "Wyrażenia",
- B. Stroustrup "Język C++" (o języku sprzed standaryzacji), p. 14.5 "Wyrażenia",
- dokument ISO/IEC 14882:2003 - rozdział 5 "Expressions", akapit 4.
A oto kilka ciekawostek, na podstawie przykładów podanych w w/w publikacjach:
a = 0;
a = tablica[a++]; // zachowanie jest niezdefiniowane
b = 13, b++, b++; // zachowanie zdefiniowane, b otrzymuje wartość 15
c = 0;
c = ++c + 1; // zachowanie jest niezdefiniowane
"gynvael" L"coldwind"
Przypominam również programistom, że program którego zachowanie nie jest zdefinowane jest nieprzenośny.
11
12
13
14
5
12
12 (ilość palcy u rąk - 10).
gcc version 4.5.2 20101216 (gdc hg, using dmd 2.052) (GCC)
naz@quad ~/D $ ./test
12
13
13
14
6
11
12
12
13
5
11
12
13
13
14
5
12
------
g++ (GCC) 3.4.2 (mingw-special)
12
13
13
14
5
12
Output for 5.1.0 - 7.0.0rc4, hhvm-3.6.1 - 3.9.1
11
12
12
13
5
12
-----------------------
Output for 4.3.0 - 5.0.5
11
12
12
13
5
11
Sprawdzone zostały praktycznie wszystkie wersje PHP od 4.3.0 jakie zostały wydane.
Trochę odgrzewam kotleta, ale wspomniałeś ten post w swojej książce w rozdziale 4., i postanowiłem sprawdzić :)
Wyniki dla Apple LLVM version 10.0.0 (clang-1000.11.45.5):
11
12
12
13
5
11
var a = 5; a = a++ + a++
11
var a = 5; a = a++ + ++a
12
var a = 5; a = ++a + a++
12
var a = 5; a = ++a + ++a
13
Add a comment: