Jest niezdefiniowany, ponieważ modyfikuje x
dwukrotnie między punktami sekwencji. Standard mówi, że jest niezdefiniowany, dlatego jest niezdefiniowany.
Tyle wiem.
Ale dlaczego?
Rozumiem, że zabranianie tego pozwala kompilatorom na lepszą optymalizację. Mogło to mieć sens, gdy wynaleziono C, ale teraz wydaje się słabym argumentem.
Gdybyśmy dzisiaj wymyślili na nowo C, czy zrobilibyśmy to w ten sposób, czy można to zrobić lepiej?
A może jest głębszy problem, który utrudnia zdefiniowanie spójnych reguł dla takich wyrażeń, więc najlepiej ich zabronić?
Przypuśćmy, że dzisiaj wymyślimy C na nowo. Chciałbym zasugerować proste reguły dla wyrażeń takich jak x=x++
, które wydają mi się działać lepiej niż istniejące reguły.
Chciałbym poznać Twoją opinię na temat sugerowanych reguł w porównaniu do istniejących lub innych sugestii.
Sugerowane zasady:
- Między punktami sekwencji kolejność oceny jest nieokreślona.
- Efekty uboczne występują natychmiast.
Nie wiąże się to z niezdefiniowanym zachowaniem. Wyrażenia oceniają tę lub inną wartość, ale na pewno nie sformatują dysku twardego (o dziwo, nigdy nie widziałem implementacji, w której x=x++
formatuje dysk twardy).
Przykładowe wyrażenia
x=x++
- Dobrze zdefiniowany, nie zmienia sięx
.
Najpierwx
jest zwiększany (natychmiast pox++
ocenie), a następnie zapisywana jest jego stara wartośćx
.x++ + ++x
- Przyrostyx
dwukrotnie, ocenia na2*x+2
.
Chociaż każda ze stron może być oceniona jako pierwsza, wynikiem jest albox + (x+2)
(lewa strona pierwsza), albo(x+1) + (x+1)
(prawa strona pierwsza).x = x + (x=3)
- Nieokreślony,x
ustaw na jedenx+3
lub6
.
Jeśli prawa strona jest oceniana jako pierwsza, to jestx+3
. Możliwe jest również, żex=3
najpierw jest oceniane, więc tak jest3+3
. W obu przypadkachx=3
przypisanie następuje natychmiast pox=3
dokonaniu oceny, więc zapisana wartość jest zastępowana przez inne przypisanie.x+=(x=3)
- Dobrze zdefiniowane, ustawionex
na 6.
Można argumentować, że jest to tylko skrót dla powyższego wyrażenia.
Ale powiedziałbym, że+=
należy to wykonać pox=3
, a nie w dwóch częściach (przeczytajx
, oceńx=3
, dodaj i zapisz nową wartość).
Jaka jest zaleta?
Niektóre komentarze podniosły ten dobry punkt.
Z pewnością nie sądzę, aby wyrażenia takie jak x=x++
powinny być używane w normalnym kodzie.
Faktycznie, jestem o wiele bardziej rygorystyczne niż - Myślę, że tylko dobre wykorzystanie dla x++
jako x++;
samotnie.
Myślę jednak, że reguły językowe muszą być tak proste, jak to możliwe. W przeciwnym razie programiści po prostu ich nie rozumieją. reguła zabraniająca dwukrotnej zmiany zmiennej między punktami sekwencji jest z pewnością regułą, której większość programistów nie rozumie.
Bardzo podstawowa zasada jest następująca:
jeśli A jest ważne, a B jest ważne i są one połączone w prawidłowy sposób, wynik jest ważny.
x
jest prawidłową wartością L, x++
jest prawidłowym wyrażeniem i =
jest prawidłowym sposobem łączenia wartości L i wyrażenia, więc dlaczego x=x++
nie jest legalne?
Standard C stanowi tutaj wyjątek, a ten wyjątek komplikuje reguły. Możesz przeszukać stackoverflow.com i zobaczyć, jak bardzo ten wyjątek wprowadza ludzi w błąd.
Więc mówię - pozbyć się tego zamieszania.
=== Podsumowanie odpowiedzi ===
Dlaczego to robisz
Próbowałem wyjaśnić w powyższej sekcji - chcę, aby reguły C były proste.Potencjał do optymalizacji:
Odsuwa to trochę kompilatora, ale nie widziałem niczego, co przekonałoby mnie, że może być znaczące.
Nadal można przeprowadzić większość optymalizacji. Na przykłada=3;b=5;
można zmienić kolejność, nawet jeśli standard określa kolejność. Wyrażenia takie jaka=b[i++]
wciąż można zoptymalizować podobnie.Nie możesz zmienić istniejącego standardu.
Przyznaję, że nie mogę. Nigdy nie myślałem, że mogę iść naprzód i zmieniać standardy i kompilatory. Chciałem tylko pomyśleć, czy można było zrobić inaczej.
źródło
x
do samego siebie nie ma sensu , a jeśli chcesz zwiększyćx
, możesz po prostu powiedziećx++;
- nie trzeba tego przypisania. Powiedziałbym, że nie należy tego definiować tylko dlatego, że trudno byłoby sobie przypomnieć, co się stanie.Odpowiedzi:
Może powinieneś najpierw odpowiedzieć na pytanie, dlaczego należy to zdefiniować? Czy jest jakaś przewaga w stylu programowania, czytelności, łatwości konserwacji lub wydajności, umożliwiając takie wyrażenia z dodatkowymi efektami ubocznymi? Jest
bardziej czytelny niż
Biorąc pod uwagę, że taka zmiana jest niezwykle fundamentalna i stanowi przełamanie istniejącej bazy kodu.
źródło
Argument, że takie niezdefiniowane zachowanie umożliwia lepszą optymalizację, nie jest dziś słaby. W rzeczywistości jest dziś znacznie silniejszy niż wtedy, gdy C był nowy.
Gdy C było nowe, maszyny, które mogłyby to wykorzystać do lepszej optymalizacji, były głównie modelami teoretycznymi. Ludzie mówili o możliwości budowania procesorów, w których kompilator instruuje procesor o tym, jakie instrukcje mogą / powinny być wykonywane równolegle z innymi instrukcjami. Wskazywali oni na fakt, że dopuszczenie tego do nieokreślonego zachowania oznaczało, że na takim procesorze, jeśli kiedykolwiek istniałby naprawdę, można było zaplanować wykonanie części „inkrementacji” równolegle z resztą strumienia instrukcji. Chociaż mieli rację co do teorii, w tamtym czasie niewiele było sprzętu, który mógłby naprawdę skorzystać z tej możliwości.
To już nie tylko teoria. Teraz istnieje sprzęt w produkcji i szeroko stosowany (np. Itanium, DSL VLIW), który naprawdę może z tego skorzystać. Oni naprawdę zrobić pozwolić kompilator do generowania strumienia instrukcji, która określa, że instrukcje X, Y i Z mogą być wykonywane równolegle. To nie jest już model teoretyczny - to prawdziwy sprzęt w prawdziwym użyciu, wykonujący prawdziwą pracę.
IMO sprawia, że zdefiniowane zachowanie jest bliskie najgorszemu „rozwiązaniu” problemu. Oczywiście nie powinieneś używać takich wyrażeń. W przypadku znacznej większości kodu idealnym rozwiązaniem byłoby po prostu całkowite odrzucenie takich wyrażeń przez kompilator. W tym czasie kompilatory C nie przeprowadzały analizy przepływu koniecznej do niezawodnego wykrycia tego. Nawet w czasach oryginalnego standardu C wciąż nie było to wcale powszechne.
Nie jestem też pewien, czy byłoby to dzisiaj akceptowalne dla społeczności - podczas gdy wiele kompilatorów może przeprowadzić tego rodzaju analizę przepływu, zazwyczaj robią to tylko wtedy, gdy żądasz optymalizacji. Wątpię, aby większość programistów chciała spowolnić kompilacje „debugowania” tylko po to, aby móc odrzucić kod, którego (normalnie) nigdy nie napisaliby.
To, co zrobił C, jest pół-rozsądnym drugim najlepszym wyborem: powiedz ludziom, aby tego nie robili, pozwalając (ale nie wymagając) kompilatorowi na odrzucenie kodu. Pozwala to uniknąć (jeszcze bardziej) spowolnienia kompilacji dla osób, które nigdy go nie użyłyby, ale nadal pozwala komuś napisać kompilator, który odrzuci taki kod, jeśli zechce (i / lub będzie miał flagi, które odrzuci go, z których ludzie mogą korzystać lub nie według własnego uznania).
Przynajmniej IMO sprawi, że to zdefiniowane zachowanie byłoby (przynajmniej blisko) najgorszą możliwą decyzją do podjęcia. Na sprzęcie w stylu VLIW wybrałbyś generowanie wolniejszego kodu dla racjonalnego wykorzystania operatorów inkrementacji, tylko ze względu na kiepski kod, który ich nadużywa, lub zawsze wymaga obszernej analizy przepływu, aby udowodnić, że nie masz do czynienia z kiepski kod, dzięki czemu możesz tworzyć wolny (serializowany) kod tylko wtedy, gdy jest to naprawdę konieczne.
Konkluzja: jeśli chcesz wyleczyć ten problem, powinieneś myśleć w przeciwnym kierunku. Zamiast definiować, co robi taki kod, powinieneś zdefiniować język, aby takie wyrażenia po prostu w ogóle nie były dozwolone (i żyć z faktem, że większość programistów prawdopodobnie zdecyduje się na szybszą kompilację zamiast egzekwowania tego wymagania).
źródło
a=b[i++];
(na przykład) jest w porządku, a optymalizacja to dobra rzecz. Nie widzę jednak sensu ranienia takiego rozsądnego kodu, więc coś takiego++i++
ma określone znaczenie.++i++
jest właśnie to, że generalnie trudno jest odróżnić je od prawidłowych wyrażeń o skutkach ubocznych (takich jaka=b[i++]
). To może wydawać się dla nas dość proste, ale jeśli dobrze pamiętam Dragon Book, to w rzeczywistości jest to trudny problem NP. To dlaczego takie zachowanie jest UB, zamiast zabronione.Eric Lippert, główny projektant w zespole kompilatorów C #, opublikował na swoim blogu artykuł na temat wielu rozważań, które należy podjąć, aby uczynić funkcję niezdefiniowaną na poziomie specyfikacji języka. Oczywiście C # jest innym językiem, z różnymi czynnikami wpływającymi na jego projekt językowy, ale jego uwagi są istotne.
W szczególności zwraca uwagę na kwestię posiadania kompilatorów dla języka, który ma istniejące implementacje, a także przedstawicieli w komitecie. Nie jestem pewien, czy tak jest w tym przypadku, ale zwykle dotyczy większości dyskusji na temat specyfikacji C i C ++.
Warto również zauważyć, jak powiedziałeś, potencjał wydajności optymalizacji kompilatora. Chociaż prawdą jest, że wydajność procesorów w tych dniach jest o wiele rzędów wielkości większa niż w czasach, gdy C był młody, duża część programowania w C wykonywana w tych dniach jest wykonywana specjalnie ze względu na potencjalny wzrost wydajności i potencjalną (hipotetyczną przyszłość ) Optymalizacje instrukcji procesora i optymalizacje przetwarzania wielordzeniowego byłyby głupie, aby wykluczyć ze względu na zbyt restrykcyjny zestaw zasad postępowania z efektami ubocznymi i punktami sekwencji.
źródło
Najpierw spójrzmy na definicję niezdefiniowanego zachowania:
Innymi słowy, „niezdefiniowane zachowanie” oznacza po prostu, że kompilator może dowolnie obsługiwać sytuację w dowolny sposób, a każde takie działanie jest uważane za „prawidłowe”.
Podstawą omawianego problemu jest następująca klauzula:
Podkreślenie dodane.
Biorąc pod uwagę wyrażenie jak
Podwyrażenia
a++
,--b
,c
, i++d
może być oceniana w dowolnej kolejności . Ponadto skutki ubocznea++
,--b
i++d
mogą być stosowane w dowolnym momencie przed następnym punkcie sekwencji (IOW, nawet jeślia++
jest oceniane przed--b
, to nie gwarantuje, żea
zostanie zaktualizowany przed--b
jest analizowany). Jak inni powiedzieli, uzasadnieniem tego zachowania jest zapewnienie implementacji swobody w celu optymalnego uporządkowania operacji.Z tego powodu jednak wyrażenia takie jak
itp., przyniesie różne wyniki dla różnych implementacji (lub dla tej samej implementacji z różnymi ustawieniami optymalizacji lub w oparciu o otaczający kod itp.).
Zachowanie pozostaje niezdefiniowane, więc kompilator nie jest zobowiązany do „robienia właściwych rzeczy”, cokolwiek by to nie było. Powyższe przypadki są wystarczająco łatwe do złapania, ale istnieje nietrywialna liczba przypadków, które byłyby trudne do niemożliwości do złapania w czasie kompilacji.
Oczywiście można zaprojektować taki język, aby kolejność oceny i kolejność stosowania efektów ubocznych były ściśle określone, a zarówno Java, jak i C # robią to, w dużej mierze, aby uniknąć problemów, do których prowadzą definicje C i C ++.
Dlaczego więc nie wprowadzono tej zmiany do C po 3 standardowych wersjach? Po pierwsze, istnieje 40 lat starszego kodu C i nie ma gwarancji, że taka zmiana nie złamie tego kodu. Nakłada to nieco obciążenia na autorów kompilatorów, ponieważ taka zmiana natychmiast sprawiłaby, że wszystkie istniejące kompilatory nie byłyby zgodne; wszyscy musieliby dokonać znaczących przeróbek. Nawet na szybkich, nowoczesnych procesorach nadal można osiągnąć rzeczywisty wzrost wydajności poprzez zmianę kolejności oceny.
źródło
Najpierw musisz zrozumieć, że nie tylko x = x ++ jest niezdefiniowane. Nikt nie dba o x = x ++, ponieważ bez względu na to, co byś zdefiniował, nie ma sensu. Nieokreślone jest bardziej jak „a = b ++ gdzie aib są takie same” - tj
Istnieje kilka różnych sposobów implementacji tej funkcji, w zależności od tego, co jest najbardziej wydajne dla architektury procesora (i dla otaczających instrukcji, w przypadku gdy jest to funkcja bardziej złożona niż w przykładzie). Na przykład dwa oczywiste:
lub
Zauważ, że pierwszy wymieniony powyżej, ten, który wykorzystuje więcej instrukcji i więcej rejestrów, jest tym, którego należy użyć we wszystkich przypadkach, w których nie można udowodnić, że aib są różne.
źródło
b
wcześnieja
.Dziedzictwo
Założenie, że C można dziś wymyślić na nowo, nie może zostać przyjęte. Jest tak wiele linii kodów C, które zostały wyprodukowane i są codziennie używane, że zmiana zasad gry w środku gry jest po prostu niewłaściwa.
Oczywiście możesz wymyślić nowy język, powiedzmy C + = , według własnych zasad. Ale to nie będzie C.
źródło
Zadeklarowanie, że coś jest zdefiniowane, nie zmieni istniejących kompilatorów, aby były zgodne z twoją definicją. Jest to szczególnie prawdziwe w przypadku założenia, na którym można było polegać w sposób jawny lub dorozumiany w wielu miejscach.
Główny problem związany z założeniem nie dotyczy
x = x++;
(kompilatory mogą łatwo to sprawdzić i powinny ostrzec), ale ma*p1 = (*p2)++
i jest równoważny (p1[i] = p2[j]++;
gdy p1 i p2 są parametrami funkcji), gdzie kompilator nie może łatwo stwierdzić, czyp1 == p2
(w C99restrict
dodano w celu rozłożenia możliwości przyjęcia p1! = p2 między punktami sekwencji, dlatego uznano, że możliwości optymalizacji były ważne).źródło
p1[i]=p2[j]++
. Jeśli kompilator nie zakłada aliasingu, nie ma problemu. Jeśli nie, musi przejść obok książki -p2[j]
najpierw zwiększ , ap1[i]
później przechowaj . Z wyjątkiem utraconych możliwości optymalizacji, które nie wydają się znaczące, nie widzę problemu.x = x++;
nie zostało napisane, alet = x; x++; x = t;
albox=x; x++;
albo cokolwiek chcesz jako semantyczny (ale co z diagnostyką?). W przypadku nowego języka po prostu porzuć działania niepożądane.x++
jako punkt sekwencyjny, jakby to było wywołanie funkcji, załatwi sprawęinc_and_return_old(&x)
.W niektórych przypadkach ten rodzaj kodu został zdefiniowany w nowym standardzie C ++ 11.
źródło
x = ++x
jest teraz dobrze zdefiniowany (ale niex = x++
)