Czytałem o naruszeniach kolejności oceny , a oni dają przykład, który mnie zastanawia.
1) Jeśli efekt uboczny na obiekcie skalarnym nie jest sekwencjonowany względem innego efektu ubocznego na tym samym obiekcie skalarnym, zachowanie jest niezdefiniowane.
// snip f(i = -1, i = -1); // undefined behavior
W tym kontekście i
jest obiektem skalarnym , co najwyraźniej oznacza
Typy arytmetyczne (3.9.1), typy wyliczeń, typy wskaźników, wskaźnik do typów elementów (3.9.2), std :: nullptr_t oraz wersje tych typów kwalifikowane przez CV (3.9.3) są wspólnie nazywane typami skalarnymi.
Nie rozumiem, w jaki sposób stwierdzenie jest w tym przypadku niejednoznaczne. Wydaje mi się, że niezależnie od tego, czy pierwszy lub drugi argument jest oceniany jako pierwszy, i
kończy się jako -1
i oba argumenty również -1
.
Czy ktoś może wyjaśnić?
AKTUALIZACJA
Naprawdę doceniam całą dyskusję. Jak dotąd bardzo podoba mi się odpowiedź @ harmic, ponieważ ujawnia pułapki i zawiłości w definiowaniu tego stwierdzenia, pomimo tego, jak prosto na pierwszy rzut oka wygląda. @ acheong87 wskazuje pewne problemy, które pojawiają się podczas korzystania z referencji, ale myślę, że jest to ortogonalne względem aspektu skutków ubocznych tego pytania.
PODSUMOWANIE
Ponieważ na to pytanie zwróciło się wiele uwagi, podsumuję główne punkty / odpowiedzi. Po pierwsze, pozwólcie mi na małą dygresję, aby wskazać, że „dlaczego” może mieć ściśle powiązane, ale subtelnie różne znaczenia, mianowicie „z jakiej przyczyny ”, „z jakiego powodu ” i „w jakim celu ”. Pogrupuję odpowiedzi, według których z tych znaczeń „dlaczego” się odnieśli.
z jakiej przyczyny
Główna odpowiedź tutaj pochodzi od Paula Drapera , a Martin J udzielił podobnej, ale nie tak obszernej odpowiedzi. Odpowiedź Paula Drapera sprowadza się do
Jest to niezdefiniowane zachowanie, ponieważ nie jest zdefiniowane, jakie jest zachowanie.
Odpowiedź jest ogólnie bardzo dobra, jeśli chodzi o wyjaśnienie tego, co mówi standard C ++. Zajmuje się również niektórymi powiązanymi przypadkami UB, takimi jak f(++i, ++i);
i f(i=1, i=-1);
. W pierwszym z pokrewnych przypadków nie jest jasne, czy pierwszym argumentem powinien być i+1
drugi i+2
i odwrotnie; po drugie, nie jest jasne, czy i
po wywołaniu funkcji powinna wynosić 1 czy -1. Oba te przypadki są UB, ponieważ podlegają następującej zasadzie:
Jeśli efekt uboczny na obiekcie skalarnym nie ma wpływu na inny efekt uboczny na tym samym obiekcie skalarnym, zachowanie jest niezdefiniowane.
Dlatego też f(i=-1, i=-1)
jest UB, ponieważ podlega tej samej zasadzie, mimo że intencja programisty jest (IMHO) oczywista i jednoznaczna.
Paul Draper również wyraźnie stwierdza w swoim wniosku, że
Czy mogło to być określone zachowanie? Tak. Czy to zostało zdefiniowane? Nie.
co prowadzi nas do pytania „z jakiego powodu / celu f(i=-1, i=-1)
pozostawiono zachowanie nieokreślone?”
z jakiego powodu / celu
Chociaż w standardzie C ++ występują pewne niedopatrzenia (być może nieostrożne), wiele pominięć jest dobrze uzasadnionych i służy konkretnemu celowi. Chociaż zdaję sobie sprawę z tego, że często celem jest albo „ułatwienie pracy kompilatora-pisarza”, albo „szybszy kod”, interesowało mnie przede wszystkim to, czy istnieje uzasadniony powód pozostawienia f(i=-1, i=-1)
UB.
harmic i supercat podają główne odpowiedzi, które stanowią przyczynę UB. Harmic wskazuje, że optymalizujący kompilator, który może rozbić pozornie atomowe operacje przypisywania na wiele instrukcji maszynowych i że może dalej przeplatać te instrukcje dla uzyskania optymalnej prędkości. Może to prowadzić do bardzo zaskakujących rezultatów: i
w jego scenariuszu kończy się na -2! W ten sposób harmonia pokazuje, jak przypisanie tej samej wartości zmiennej więcej niż raz może mieć złe skutki, jeśli operacje nie są konsekwentne.
supercat przedstawia pokrótce pułapki związane z próbami f(i=-1, i=-1)
zrobienia tego, na co wygląda. Wskazuje, że w niektórych architekturach istnieją twarde ograniczenia dotyczące wielokrotnego zapisu na ten sam adres pamięci. Kompilator może mieć trudności z uchwyceniem tego, jeśli mamy do czynienia z czymś mniej trywialnym niż f(i=-1, i=-1)
.
davidf dostarcza również przykład instrukcji przeplatania bardzo podobnych do instrukcji harmic.
Chociaż każdy z przykładów Harmika, Superkata i Davida jest nieco wymyślony, razem wzięte stanowią one konkretny powód, dla f(i=-1, i=-1)
którego zachowanie powinno być niezdefiniowane.
Zaakceptowałem odpowiedź Harmica, ponieważ najlepiej poradziła sobie ze wszystkimi znaczeniami tego, dlaczego, chociaż odpowiedź Paula Drapera lepiej odnosiła się do części „z jakiej przyczyny”.
inne odpowiedzi
JohnB zwraca uwagę, że jeśli weźmiemy pod uwagę przeciążone operatory przypisania (zamiast zwykłych skalarów), możemy również wpaść w kłopoty.
źródło
std::nullptr_t
oraz wersje tych typów zakwalifikowane do CV (3.9.3) są wspólnie nazywane typami skalarnymi . „f(i-1, i = -1)
coś podobnego.Odpowiedzi:
Ponieważ operacje nie są konsekwentne, nie można powiedzieć, że instrukcje wykonujące przypisanie nie mogą być przeplatane. Może to być optymalne, w zależności od architektury procesora. Odnośna strona stwierdza:
To samo w sobie nie wydaje się powodować problemu - zakładając, że wykonywana operacja zapisuje wartość -1 w miejscu pamięci. Ale nie ma też nic do powiedzenia, że kompilator nie może zoptymalizować tego w osobnym zestawie instrukcji, który ma ten sam efekt, ale który mógłby zawieść, gdyby operacja została przeplatana z inną operacją w tej samej lokalizacji pamięci.
Na przykład wyobraź sobie, że bardziej efektywne było wyzerowanie pamięci, a następnie zmniejszenie jej w porównaniu z ładowaniem wartości -1 do. Następnie:
może stać się:
Teraz mam -2.
Jest to prawdopodobnie fałszywy przykład, ale jest to możliwe.
źródło
load 8bit immediate and shift
to do 4 razy. Zwykle kompilator wykonuje adresowanie pośrednie, aby pobrać liczbę z tabeli, aby tego uniknąć. (-1 można wykonać w 1 instrukcji, ale można wybrać inny przykład).Po pierwsze, „skalar obiekt” oznacza typ niczym
int
,float
lub wskaźnik (zobacz Co jest skalarne obiektu w C ++? ).Po drugie, może się to wydawać bardziej oczywiste
miałby nieokreślone zachowanie. Ale
jest mniej oczywiste.
Nieco inny przykład:
Jakie zadanie miało miejsce „ostatnie”
i = 1
lubi = -1
? Nie jest to zdefiniowane w standardzie. Naprawdę,i
mogą to być środki5
(patrz odpowiedź Harmica, aby uzyskać całkowicie prawdopodobne wyjaśnienie, w jaki sposób może to mieć miejsce). Lub program może segfault. Lub sformatuj dysk twardy.Ale teraz pytasz: „Co z moim przykładem? Użyłem tej samej wartości (
-1
) dla obu zadań. Co może być niejasne?”Masz rację ... oprócz tego, jak opisał to komitet standardów C ++.
Oni mogli dokonały specjalny wyjątek dla szczególnego przypadku, ale nie zrobili tego. (A dlaczego mieliby? Jaki użytek miałby to kiedykolwiek mieć?) Tak więc
i
nadal może być5
. Lub twój dysk twardy może być pusty. Zatem odpowiedź na twoje pytanie brzmi:Jest to niezdefiniowane zachowanie, ponieważ nie jest zdefiniowane, jakie jest zachowanie.
(To zasługuje na podkreślenie, ponieważ wielu programistów uważa, że „niezdefiniowany” oznacza „losowy” lub „nieprzewidywalny”. Nie robi tego; nie oznacza to, że nie jest zdefiniowany w standardzie. Zachowanie może być w 100% spójne i nadal być niezdefiniowane.)
Czy mogło to być określone zachowanie? Tak. Czy to zostało zdefiniowane? Nie. Dlatego jest „niezdefiniowany”.
To powiedziawszy, „niezdefiniowany” nie oznacza, że kompilator sformatuje dysk twardy ... oznacza, że mógłby i nadal będzie kompilatorem zgodnym ze standardami. Realistycznie jestem pewien, że g ++, Clang i MSVC zrobią to, czego się spodziewałeś. Po prostu nie musieliby „musieć”.
Innym pytaniem może być: dlaczego komitet normalizacyjny C ++ postanowił, aby ten efekt uboczny nie był skutkiem? . Ta odpowiedź będzie obejmować historię i opinie komitetu. Lub Co jest dobrego w tym, że ten efekt uboczny nie występuje w C ++? , co pozwala na jakiekolwiek uzasadnienie, niezależnie od tego, czy było to rzeczywiste uzasadnienie komitetu normalizacyjnego. Możesz zadać te pytania tutaj lub na stronie programmers.stackexchange.com.
źródło
-Wsequence-point
g ++, to cię ostrzeże.undefined behavior
środkisomething random will happen
, które jest daleko od przypadku, przez większość czasu.Praktyczny powód, aby nie robić wyjątku od reguł tylko dlatego, że dwie wartości są takie same:
Rozważ przypadek, w którym było to dozwolone.
Kilka miesięcy później pojawia się potrzeba zmiany
Pozornie nieszkodliwy, prawda? A jednak nagle prog.cpp już się nie kompiluje. Uważamy jednak, że kompilacja nie powinna zależeć od wartości literału.
Konkluzja: nie ma wyjątku od reguły, ponieważ zależałoby to od kompilacji od wartości (raczej typu) stałej.
EDYTOWAĆ
@HeartWare wskazał, że stałe wyrażenia formularza
A DIV B
nie są dozwolone w niektórych językach, gdyB
wynosi 0, i powodują niepowodzenie kompilacji. Dlatego zmiana stałej może powodować błędy kompilacji w innym miejscu. Co jest, IMHO, niefortunne. Ale z pewnością dobrze jest ograniczyć takie rzeczy do nieuniknionego.źródło
f(i = VALUEA, i = VALUEB);
ma zdecydowanie potencjał do nieokreślonego zachowania. Mam nadzieję, że tak naprawdę nie kodujesz w oparciu o wartości kryjące się za identyfikatorami.SomeProcedure(A, B, B DIV (2-A))
. W każdym razie, jeśli język mówi, że CONST musi zostać w pełni oceniony w czasie kompilacji, to oczywiście moje twierdzenie nie jest ważne w tej sprawie. Ponieważ w jakiś sposób zaciera to rozróżnienie czasu kompilacji i czasu wykonywania. Czy zauważyłby również, jeśli piszemyCONST C = X(2-A); FUNCTION X:INTEGER(CONST Y:INTEGER) = B/Y;
? Czy funkcje nie są dozwolone?Zamieszanie polega na tym, że przechowywanie stałej wartości w zmiennej lokalnej nie jest jedną instrukcją atomową w każdej architekturze, dla której zaprojektowano C. W tym przypadku procesor, na którym działa kod, ma większe znaczenie niż kompilator. Na przykład na ARM, gdzie każda instrukcja nie może przenosić pełnej stałej 32-bitowej, przechowywanie wartości int w zmiennej wymaga więcej niż jednej instrukcji. Przykład z tym pseudo kodem, w którym można przechowywać tylko 8 bitów na raz i musi działać w rejestrze 32-bitowym, i jest int32:
Możesz sobie wyobrazić, że jeśli kompilator chce zoptymalizować, może przeplatać dwukrotnie tę samą sekwencję i nie wiesz, jaką wartość zostanie zapisana do i; i powiedzmy, że nie jest zbyt mądry:
Jednak w moich testach gcc jest na tyle miły, że rozpoznaje, że ta sama wartość jest używana dwa razy i generuje ją raz i nie robi nic dziwnego. Dostaję -1, -1 Ale mój przykład jest nadal aktualny, ponieważ ważne jest, aby wziąć pod uwagę, że nawet stała może nie być tak oczywista, jak się wydaje.
źródło
-1
(że kompilator gdzieś zapisał ), ale jest raczej3^81 mod 2^32
, ale stałe, to kompilator może zrobić dokładnie to, co tutaj zrobiono, a przy pewnej dźwigni omtimizacji przeplatam sekwencje wywołań aby uniknąć czekania.f(i = A, j = B)
gdziei
ij
są dwoma oddzielnymi obiektami. Ten przykład nie ma UB. Maszyna mający 3 krótkie rejestrów ma usprawiedliwienia dla kompilatora, aby wymieszać dwie wartościA
iB
w tym samym rejestrze (jak pokazano na odpowiedź użytkownika @ davidf), ponieważ byłoby złamać semantyki programu.Zachowanie jest powszechnie określane jako niezdefiniowane, jeśli istnieje jakiś możliwy powód, dla którego kompilator, który próbował być „pomocny”, mógł zrobić coś, co spowodowałoby całkowicie nieoczekiwane zachowanie.
W przypadku, gdy zmienna jest zapisywana wiele razy, ale nic nie gwarantuje, że zapisy odbywają się w różnych momentach, niektóre rodzaje sprzętu mogą umożliwiać wykonywanie wielu operacji „przechowywania” jednocześnie pod różnymi adresami przy użyciu pamięci z dwoma portami. Jednak niektóre pamięci z dwoma portami wyraźnie zabraniają scenariusza, w którym dwa sklepy trafiają jednocześnie na ten sam adres, niezależnie od tego, czy zapisane wartości są zgodne, czy nie. Jeśli kompilator dla takiej maszyny zauważy dwie nieudane próby zapisu tej samej zmiennej, może albo odmówić kompilacji, albo zapewnić, że dwa zapisy nie będą mogły zostać zaplanowane jednocześnie. Ale jeśli jeden lub oba z nich są dostępne za pośrednictwem wskaźnika lub odwołania, kompilator może nie zawsze być w stanie stwierdzić, czy oba zapisy mogą trafić w to samo miejsce przechowywania. W takim przypadku może zaplanować zapisy jednocześnie, powodując pułapkę sprzętową przy próbie dostępu.
Oczywiście fakt, że ktoś może zaimplementować kompilator C na takiej platformie, nie sugeruje, że takie zachowanie nie powinno być definiowane na platformach sprzętowych przy użyciu magazynów wystarczająco małych, aby można je było przetwarzać atomowo. Próba przechowywania dwóch różnych wartości w sposób niesekwencjonowany może spowodować dziwność, jeśli kompilator nie jest tego świadomy; na przykład biorąc pod uwagę:
jeśli kompilator wpisuje polecenie „moo” i może powiedzieć, że nie modyfikuje „v”, może zapisać 5 do v, następnie zapisać 6 do * p, a następnie przekazać 5 do „zoo”, a następnie przekazać zawartość v do „zoo”. Jeśli „zoo” nie modyfikuje „v”, nie powinno być mowy, aby oba wywołania miały inne wartości, ale i tak mogłoby się to zdarzyć. Z drugiej strony, w przypadkach, gdy oba sklepy zapisałyby tę samą wartość, taka dziwność nie mogłaby wystąpić i na większości platform nie byłoby rozsądnego powodu, aby implementacja zrobiła coś dziwnego. Niestety, niektórzy autorzy kompilatorów nie potrzebują żadnej wymówki dla głupich zachowań poza „ponieważ Standard na to pozwala”, więc nawet te przypadki nie są bezpieczne.
źródło
Fakt, że wynik byłby taki sam w większości wdrożeń w tym przypadku jest przypadkowe; kolejność oceny jest wciąż nieokreślona. Zastanów się
f(i = -1, i = -2)
: tutaj zamówienie ma znaczenie. Jedynym powodem, dla którego nie ma znaczenia w twoim przykładzie, jest wypadek, że obie wartości są-1
.Biorąc pod uwagę, że wyrażenie jest określone jako niezdefiniowane zachowanie, złośliwie zgodny kompilator może wyświetlać nieodpowiedni obraz podczas oceny
f(i = -1, i = -1)
i przerywania wykonania - i nadal być uważany za całkowicie poprawny. Na szczęście nie znam takich kompilatorów.źródło
Wydaje mi się, że jedyną regułą dotyczącą sekwencjonowania wyrażenia argumentu funkcji jest tutaj:
To nie definiuje sekwencjonowania między wyrażeniami argumentów, więc w tym przypadku kończymy:
W praktyce w większości kompilatorów cytowany przykład będzie działał dobrze (w przeciwieństwie do „wymazywania dysku twardego” i innych teoretycznych konsekwencji nieokreślonego zachowania).
Jest to jednak zobowiązanie, ponieważ zależy od konkretnego zachowania kompilatora, nawet jeśli dwie przypisane wartości są takie same. Oczywiście, jeśli spróbujesz przypisać różne wartości, wyniki będą „prawdziwie” niezdefiniowane:
źródło
C ++ 17 definiuje bardziej rygorystyczne zasady oceny. W szczególności sekwencjonuje argumenty funkcji (chociaż w nieokreślonej kolejności).
Pozwala na niektóre przypadki, które wcześniej byłyby UB:
źródło
f
podpis byłf(int a, int b)
, czy C ++ 17 gwarantuje, żea == -1
ib == -2
jeśli zostanie wywołany jak w drugim przypadku?a
ab
, wówczasi
-then-a
są inicjowane -1, potemi
-then-b
są inicjowane -2, lub odwrotnie. W obu przypadkach kończymy naa == -1
ib == -2
. Przynajmniej tak czytam: „ Inicjalizacja parametru, w tym każde powiązane obliczenie wartości i efekt uboczny, jest nieokreślona sekwencyjnie w stosunku do dowolnego innego parametru ”.Operator przypisania może być przeciążony, w takim przypadku kolejność może mieć znaczenie:
źródło
Odpowiada to po prostu: „Nie jestem pewien, co„ obiekt skalarny ”może oznaczać oprócz czegoś takiego jak int lub float”.
Zinterpretowałbym „obiekt skalarny” jako skrót od „obiektu typu skalarnego” lub po prostu „zmiennej typu skalarnego”. Następnie
pointer
,enum
(stały) są skalarne typu.To jest artykuł MSDN o typach skalarnych .
źródło
W rzeczywistości istnieje powód, aby nie zależeć od faktu, że kompilator sprawdzi, że
i
przypisano mu tę samą wartość dwa razy, aby możliwe było zastąpienie go pojedynczym przypisaniem. Co jeśli mamy jakieś wyrażenia?źródło
1
doi
. Albo oba argumenty przypisują1
i robi to „właściwą” rzecz, albo argumenty przypisują różne wartości i jest to nieokreślone zachowanie, więc nasz wybór jest nadal dozwolony.