Niezdefiniowane zachowanie i punkty sekwencji

986

Co to są „punkty sekwencji”?

Jaki jest związek między niezdefiniowanym zachowaniem a punktami sekwencji?

Często używam śmiesznych i skomplikowanych wyrażeń, takich jak a[++i] = i;, aby poczuć się lepiej. Dlaczego mam przestać ich używać?

Jeśli to przeczytałeś, koniecznie odwiedź kolejne pytanie Niezdefiniowane zachowanie i punkty sekwencji ponownie załadowane .

(Uwaga: ma to być wpis do często zadawanych pytań na temat C ++ w programie Stack Overflow . Jeśli chcesz skrytykować pomysł podania w tym formularzu odpowiedzi na najczęściej zadawane pytania, to miejsce na publikację na meta, które to wszystko rozpoczęło, byłoby odpowiednim miejscem. Odpowiedzi na to pytanie jest monitorowane w czacie C ++ , gdzie pomysł FAQ powstał w pierwszej kolejności, więc twoje odpowiedzi prawdopodobnie zostaną przeczytane przez tych, którzy wpadli na ten pomysł).

nieznanych
źródło

Odpowiedzi:

682

C ++ 98 i C ++ 03

Ta odpowiedź dotyczy starszych wersji standardu C ++. Wersje standardu C ++ 11 i C ++ 14 nie zawierają formalnie „punktów sekwencji”; zamiast tego operacje są „sekwencjonowane przed” lub „niesekwencjonowane” lub „nieokreślone”. Efekt netto jest zasadniczo taki sam, ale terminologia jest inna.


Uwaga : OK. Ta odpowiedź jest trochę długa. Więc miej cierpliwość podczas czytania. Jeśli już znasz te rzeczy, ich ponowne przeczytanie nie doprowadzi Cię do szaleństwa.

Wymagania wstępne : Podstawowa znajomość standardu C ++


Co to są punkty sekwencji?

Standard mówi

W określonych punktach sekwencji wykonania, zwanych punktami sekwencji , wszystkie skutki uboczne poprzednich ocen będą kompletne i nie będą miały miejsca żadne skutki uboczne kolejnych ocen. (§1,9 / 7)

Skutki uboczne? Jakie są skutki uboczne?

Ocena wyrażenia wytwarza coś, a jeśli dodatkowo nastąpi zmiana stanu środowiska wykonawczego, mówi się, że wyrażenie (jego ocena) ma pewne skutki uboczne.

Na przykład:

int x = y++; //where y is also an int

Oprócz operacji inicjalizacji wartość yzmienia się również z powodu działania ubocznego ++operatora.

Na razie w porządku. Przechodzenie do punktów sekwencji. Alternatywna definicja punktów sekwencyjnych podana przez autora comp.lang.c Steve Summit:

Punkt sekwencyjny to punkt w czasie, w którym kurz osiadł, a wszystkie efekty uboczne, które do tej pory zaobserwowano, są zagwarantowane jako całkowite.


Jakie są wspólne punkty sekwencji wymienione w standardzie C ++?

To są:

  • na końcu oceny pełnego wyrażenia ( §1.9/16) (Pełne wyrażenie to wyrażenie, które nie jest podwyrażeniem innego wyrażenia). 1

    Przykład:

    int a = 5; // ; is a sequence point here
  • w ocenie każdego z poniższych wyrażeń po ocenie pierwszego wyrażenia ( §1.9/18) 2

    • a && b (§5.14)
    • a || b (§5.15)
    • a ? b : c (§5.16)
    • a , b (§5.18)(tutaj a, b jest operatorem przecinkowym; in func(a,a++) ,nie jest operatorem przecinkowym, jest jedynie separatorem między argumentami ai a++. W związku z tym zachowanie jest niezdefiniowane (jeśli ajest uważane za typ pierwotny))
  • w wywołaniu funkcji (bez względu na to, czy funkcja jest wbudowana), po ocenie wszystkich argumentów funkcji (jeśli istnieją), które mają miejsce przed wykonaniem wyrażeń lub instrukcji w treści funkcji ( §1.9/17).

1: Uwaga: ocena pełnego wyrażenia może obejmować ocenę podwyrażeń, które nie są leksykalnie częścią pełnego wyrażenia. Na przykład podwyrażenia zaangażowane w ocenę domyślnych wyrażeń argumentów (8.3.6) uważa się za utworzone w wyrażeniu wywołującym funkcję, a nie wyrażeniu definiującym domyślny argument

2: Wskazane operatory to wbudowane operatory, jak opisano w klauzuli 5. Gdy jeden z tych operatorów jest przeciążony (klauzula 13) w ważnym kontekście, wyznaczając w ten sposób zdefiniowaną przez użytkownika funkcję operatora, wyrażenie oznacza wywołanie funkcji i operandy tworzą listę argumentów, bez domyślnego punktu sekwencji między nimi.


Co to jest niezdefiniowane zachowanie?

Standard określa niezdefiniowane zachowanie w sekcji §1.3.12jako

zachowanie, które może powstać w wyniku użycia błędnej konstrukcji programu lub błędnych danych, dla których niniejszy standard międzynarodowy nie nakłada żadnych wymagań 3 .

Nieokreślonego zachowania można się również spodziewać, gdy w niniejszej Normie Międzynarodowej pominięto opis jakiejkolwiek wyraźnej definicji zachowania.

3: dopuszczalne niezdefiniowane zachowanie sięga od całkowitego zignorowania sytuacji z nieprzewidzianymi wynikami, do zachowania podczas tłumaczenia lub wykonywania programu w udokumentowany sposób charakterystyczny dla środowiska (z wydaniem lub bez komunikatu diagnostycznego), do zakończenia tłumaczenia lub wykonania (z wydaniem komunikatu diagnostycznego).

Krótko mówiąc, niezdefiniowane zachowanie oznacza, że wszystko może się zdarzyć, od demonów wylatujących z nosa po zajście w ciążę przez dziewczynę.


Jaki jest związek między niezdefiniowanym zachowaniem a punktami sekwencji?

Zanim do tego przejdę, musisz poznać różnicę między zachowaniem nieokreślonym, zachowaniem nieokreślonym a zachowaniem zdefiniowanym przy wdrażaniu .

Musisz także o tym wiedzieć the order of evaluation of operands of individual operators and subexpressions of individual expressions, and the order in which side effects take place, is unspecified.

Na przykład:

int x = 5, y = 6;

int z = x++ + y++; //it is unspecified whether x++ or y++ will be evaluated first.

Kolejny przykład tutaj .


Teraz §5/4mówi Standard

  • 1) Między poprzednim a następnym punktem sekwencji obiekt skalarny powinien mieć zmodyfikowaną wartość zapisaną co najwyżej raz na podstawie oceny wyrażenia.

Co to znaczy?

Nieformalnie oznacza to, że między dwoma punktami sekwencji zmiennej nie wolno modyfikować więcej niż jeden raz. W wyrażeniu wyrażenie next sequence pointzwykle znajduje się na kończącym średniku, a previous sequence pointna końcu poprzedniej instrukcji. Wyrażenie może również zawierać pośrednie sequence points.

Z powyższego zdania następujące wyrażenia wywołują Niezdefiniowane zachowanie:

i++ * ++i;   // UB, i is modified more than once btw two SPs
i = ++i;     // UB, same as above
++i = 2;     // UB, same as above
i = ++i + 1; // UB, same as above
++++++i;     // UB, parsed as (++(++(++i)))

i = (i, ++i, ++i); // UB, there's no SP between `++i` (right most) and assignment to `i` (`i` is modified more than once btw two SPs)

Ale następujące wyrażenia są w porządku:

i = (i, ++i, 1) + 1; // well defined (AFAIK)
i = (++i, i++, i);   // well defined 
int j = i;
j = (++i, i++, j*i); // well defined

  • 2) Ponadto dostęp do poprzedniej wartości można uzyskać wyłącznie w celu ustalenia wartości, która ma być przechowywana.

Co to znaczy? Oznacza to, że jeśli obiekt jest zapisany w pełnym wyrażeniu, każdy dostęp do niego w tym samym wyrażeniu musi być bezpośrednio zaangażowany w obliczenie wartości, która ma zostać zapisana .

Na przykład w i = i + 1całym dostępie i(w LHS i RHS) są bezpośrednio zaangażowani w obliczanie wartości, która ma zostać zapisana. Więc w porządku.

Ta reguła skutecznie ogranicza wyrażenia prawne do tych, w których dostęp wyraźnie demonstruje modyfikację.

Przykład 1:

std::printf("%d %d", i,++i); // invokes Undefined Behaviour because of Rule no 2

Przykład 2:

a[i] = i++ // or a[++i] = i or a[i++] = ++i etc

jest niedozwolone, ponieważ jeden z dostępów i(ten w a[i]) nie ma nic wspólnego z wartością, która ostatecznie jest przechowywana w i (co dzieje się w in i++), więc nie ma dobrego sposobu na zdefiniowanie - ani dla naszego zrozumienia, ani dla kompilatora - czy dostęp powinien mieć miejsce przed czy po zapisaniu przyrostowej wartości. Zachowanie jest więc niezdefiniowane.

Przykład 3:

int x = i + i++ ;// Similar to above

Odpowiedz na odpowiedź dla C ++ 11 tutaj .

Prasoon Saurav
źródło
45
*p++ = 4 nie jest niezdefiniowanym zachowaniem. *p++jest interpretowany jako *(p++). p++zwraca p(kopia), a wartość jest przechowywana pod poprzednim adresem. Dlaczego miałoby to wywoływać UB? Jest całkowicie w porządku.
Prasoon Saurav
6
@Mike: AFAIK, nie ma (legalnych) kopii standardu C ++, do których można by linkować.
sbi
11
Cóż, możesz mieć link do odpowiedniej strony zamówienia ISO. W każdym razie, myśląc o tym, wyrażenie „elementarna znajomość standardu C ++” wydaje się nieco sprzeczne pod względem, ponieważ jeśli czytasz ten standard, przekraczasz poziom elementarny. Może moglibyśmy wymienić rzeczy, których potrzebujesz w podstawowym języku, takie jak składnia wyrażeń, kolejność operacji, a może przeciążenie operatora?
Mike DeSimone
41
Nie jestem pewien, czy cytowanie standardu jest najlepszym sposobem nauczania początkujących
Odwrotnie
6
@Adrian Pierwsze wyrażenie wywołuje UB, ponieważ nie ma punktu sekwencji między ostatnim ++ia przypisaniem do i. Drugie wyrażenie nie wywołuje UB, ponieważ wyrażenie inie zmienia wartości i. W drugim przykładzie i++po nim następuje punkt sekwencji ( ,) przed wywołaniem operatora przypisania.
Kolyunya
276

Jest to kontynuacja mojej poprzedniej odpowiedzi i zawiera materiały związane z C ++ 11. .


Wymagania wstępne : Podstawowa znajomość relacji (matematyka).


Czy to prawda, że ​​w C ++ 11 nie ma Punktów Sekwencji?

Tak! To jest najprawdziwsza prawda.

Sekwencja Punkty zostały zastąpione przez zsekwencjonowany Przed i Sequenced Po (a Unsequenced i Indeterminately Sequenced ) stosunki w C ++ 11.


Czym dokładnie jest ta sekwencja przed?

Sekwencją wcześniej (§ 1.9 / 13) jest relacja, która jest:

między ocenami wykonanymi przez pojedynczy wątek i indukuje ścisłą częściową kolejność 1

Formalnie oznacza to, że otrzymano dowolne dwie oceny (patrz poniżej), A a Bjeśli Azostanie to zsekwencjonowane wcześniej B , wykonanie A poprzedza wykonanie B. Jeśli Anie jest sekwencjonowany wcześniej Bi Bnie jest sekwencjonowany wcześniej A, to Ai nie Bsekwencjonowane 2 .

Oceny Ai Bsą one sekwencjonowane w nieokreślony sposób, gdy albo Ajest sekwencjonowane przed, Balbo Bsekwencjonowane przed A, ale nie jest określone, które 3 .

[Uwagi]
1: Dokładna kolejność częściowo jest binarny związek "<" przez zestaw P, który jest asymmetric, a transitive, to znaczy dla wszystkich a, bi cw Pmamy że:
........ (I). jeśli a <b, to ¬ (b <a) ( asymmetry);
........ (ii). jeśli a <b i b <c, to a <c ( transitivity).
2: Wykonanie niesekwencjonowanych ocen może się nakładać .
3: Oceny nieokreślone sekwencyjnie nie mogą się pokrywać , ale można je wykonać najpierw.


Jakie jest znaczenie słowa „ocena” w kontekście C ++ 11?

W C ++ 11 ocena wyrażenia (lub podwyrażenia) ogólnie obejmuje:

  • obliczenia wartości (w tym określenie tożsamości obiektu do oceny wartości glwaue i pobranie wartości wcześniej przypisanej do obiektu do oceny wartości )

  • inicjacja efektów ubocznych .

Teraz (§ 1.9 / 14) mówi:

Każde obliczenie wartości i efekt uboczny związany z pełnym wyrażeniem jest sekwencjonowane przed każdym obliczeniem wartości i efektem ubocznym związanym z następnym pełnym wyrażeniem podlegającym ocenie .

  • Trywialny przykład:

    int x; x = 10; ++x;

    Obliczenia wartości i związane z nimi skutki uboczne ++xsą sekwencjonowane po obliczeniu wartości i skutkach ubocznychx = 10;


Więc musi być jakiś związek między niezdefiniowanym zachowaniem a wyżej wymienionymi rzeczami, prawda?

Tak! Dobrze.

W (§ 1.9 / 15) wspomniano o tym

O ile nie zaznaczono inaczej, oceny operandów poszczególnych operatorów i podwyrażeń poszczególnych wyrażeń nie mają konsekwencji 4 .

Na przykład :

int main()
{
     int num = 19 ;
     num = (num << 3) + (num >> 3);
} 
  1. Ocena operandów +operatora nie ma wpływu na siebie nawzajem.
  2. Ocena operandów <<i >>operatorów nie ma wpływu na siebie nawzajem.

4: W wyrażeniu, które jest oceniane więcej niż raz podczas wykonywania programu, niesekwencjonowane i nieokreślone sekwencyjnie oceny jego podwyrażeń nie muszą być wykonywane konsekwentnie w różnych ocenach.

(§1.9 / 15) Obliczenia wartości operandów operatora są sekwencjonowane przed obliczeniem wartości wyniku operatora.

Oznacza to w x + yobliczaniu wartości xi ysą one sekwencjonowane przed obliczeniem wartości dla (x + y).

Co ważniejsze

(§1.9 / 15) Jeśli efekt uboczny na obiekcie skalarnym nie ma wpływu na żaden z nich

(a) inny efekt uboczny na tym samym obiekcie skalarnym

lub

(b) obliczenie wartości z wykorzystaniem wartości tego samego obiektu skalarnego.

zachowanie jest niezdefiniowane .

Przykłady:

int i = 5, v[10] = { };
void  f(int,  int);
  1. i = i++ * ++i; // Undefined Behaviour
  2. i = ++i + i++; // Undefined Behaviour
  3. i = ++i + ++i; // Undefined Behaviour
  4. i = v[i++]; // Undefined Behaviour
  5. i = v[++i]: // Well-defined Behavior
  6. i = i++ + 1; // Undefined Behaviour
  7. i = ++i + 1; // Well-defined Behaviour
  8. ++++i; // Well-defined Behaviour
  9. f(i = -1, i = -1); // Undefined Behaviour (see below)

Podczas wywoływania funkcji (niezależnie od tego, czy funkcja jest wbudowana), każde obliczenie wartości i efekt uboczny związany z dowolnym wyrażeniem argumentu lub wyrażeniem postfiksowym oznaczającym wywoływaną funkcję jest sekwencjonowany przed wykonaniem każdego wyrażenia lub instrukcji w treści nazywana funkcją. [ Uwaga: Obliczenia wartości i skutki uboczne związane z różnymi wyrażeniami argumentów nie są uwzględniane . - uwaga końcowa ]

Wyrażenia (5), (7)a (8)nie powoływać niezdefiniowanej zachowanie. Bardziej szczegółowe wyjaśnienia znajdziesz w poniższych odpowiedziach.


Uwaga końcowa :

Jeśli znajdziesz jakiś błąd w poście, zostaw komentarz. Zaawansowani użytkownicy (z rep> 20000) nie wahaj się edytować wpisu w celu poprawienia literówek i innych błędów.

Prasoon Saurav
źródło
3
Zamiast „asymetrycznego” sekwencjonowane przed / po są relacje „antysymetryczne”. Należy to zmienić w tekście, aby był zgodny z definicją częściowego zamówienia podaną później (co również zgadza się z Wikipedią).
TemplateRex
1
Dlaczego 7) pozycja w ostatnim przykładzie jest UB? Może powinno być f(i = -1, i = 1)?
Michaił
1
Poprawiłem opis relacji „sekwencjonowanie przed”. Jest to ścisłe zamówienie częściowe . Oczywiście, wyrażenie nie może być zsekwencjonowane przed sobą, więc relacja nie może być zwrotna. Dlatego jest asymetryczny, a nie antysymetryczny.
ThomasMcLeod
1
5) bycie dobrze upieczonym oszołomiło mnie. wyjaśnienie Johannesa Schauba nie było całkiem proste. Zwłaszcza, że ​​wierzyłem, że nawet w ++i(będąc ocenianą wartość przed +operatorem, który jej używa) standard wciąż nie mówi, że jego efekt uboczny musi zostać zakończony. Ale w rzeczywistości, ponieważ zwraca ref Do lvaluektóry jest isam, musi mieć gotowego efektu bocznego ponieważ ocena musi być zakończona, zatem wartość ta musi być na bieżąco. To była szalona część.
v.oddou
„Członkowie Komitetu ISO C ++ uważali, że kwestie związane z punktami sekwencyjnymi są dość trudne do zrozumienia. Postanowili więc zastąpić je wyżej wymienionymi relacjami, aby uzyskać bardziej jasne sformułowania i większą precyzję”. - Czy masz odniesienie do tego roszczenia? Wydaje mi się, że nowe relacje są trudniejsze do zrozumienia.
MM
30

C ++ 17 ( N4659) zawiera propozycję Udoskonalenia kolejności oceny wyrażeń dla Idiomatic C ++, która określa bardziej rygorystyczną kolejność oceny wyrażeń.

W szczególności następujące zdanie

8.18 Operatorzy przypisania i przypisania złożonego :
....

We wszystkich przypadkach przypisanie jest sekwencjonowane po obliczeniu wartości prawego i lewego operandu, a przed obliczeniem wartości wyrażenia przypisania. Prawy operand jest sekwencjonowany przed lewym operandem.

wraz z następującym wyjaśnieniem

Wyrażenie X mówi się sekwencjonowaniu przed wyrażenia Y jeśli każdy obliczenie wartości i każdy skutek ubocznych towarzyszących ekspresyjnej X sekwencjonuje się przed każdym obliczeniu wartości i każdej skutkiem ubocznym związanym z wyrażenia Y .

unieważnić kilka przypadków wcześniej nieokreślonego zachowania, w tym ten, o którym mowa:

a[++i] = i;

Jednak kilka innych podobnych przypadków nadal prowadzi do nieokreślonego zachowania.

W N4140:

i = i++ + 1; // the behavior is undefined

Ale w N4659

i = i++ + 1; // the value of i is incremented
i = i++ + i; // the behavior is undefined

Oczywiście użycie kompilatora zgodnego z C ++ 17 niekoniecznie oznacza, że ​​należy zacząć pisać takie wyrażenia.

AlexD
źródło
dlaczego i = i++ + 1;jest zdefiniowane zachowanie w c ++ 17, myślę, że nawet jeśli „prawy operand jest sekwencjonowany przed lewym operandem”, jednak modyfikacja „i ++” i efekt uboczny przypisania nie są konsekwentne, proszę podać więcej szczegółów, aby je zinterpretować
jack X
@jackX Rozszerzyłem odpowiedź :).
AlexD
tak, myślę, że szczegół interpretacji zdania „Prawy operand jest sekwencjonowany przed lewym operandem” jest bardziej przydatny. na przykład „Prawy operand jest sekwencjonowany przed lewym operandem” oznacza, że ​​obliczenie wartości i efekt uboczny związany z prawym operandem są zsekwencjonowane przed operatorem lewego operandu. tak jak ty :-)
jack X
11

Domyślam się, że istnieje podstawowa przyczyna zmiany, ale nie tylko kosmetyczne jest wyjaśnienie starej interpretacji: przyczyną jest współbieżność. Nieokreślona kolejność opracowywania polega jedynie na wybraniu jednego z kilku możliwych szeregowań szeregowych, jest to zupełnie inna sytuacja niż przed i po zamówieniach, ponieważ jeśli nie ma określonego zamówienia, możliwa jest równoczesna ocena: nie w przypadku starych reguł. Na przykład w:

f (a,b)

poprzednio albo a następnie b, albo b następnie a. Teraz, aib można ocenić za pomocą instrukcji przeplecionych lub nawet na różnych rdzeniach.

Yttrill
źródło
5
Uważam jednak, że jeśli „a” lub „b” zawiera wywołanie funkcji, są one nieokreślone sekwencyjnie, a nie sekwencyjne, co oznacza, że ​​wszystkie skutki uboczne jednego muszą wystąpić przed wystąpieniem jakichkolwiek skutków ubocznych inne, chociaż kompilator nie musi być spójny w kwestii tego, który z nich jest pierwszy. Jeśli nie jest to już prawdą, spowodowałoby to uszkodzenie dużej części kodu, który opiera się na operacjach, które się nie nakładają (np. Jeśli „a” i „b” każda konfiguruje, używa i usuwa wspólny stan statyczny).
supercat,
2

W C99(ISO/IEC 9899:TC3)dotychczasowej dyskusji, która wydaje się być nieobecna, poczyniono następujące rozważania dotyczące kolejności oceny.

[...] kolejność oceny podwyrażeń i kolejność występowania działań niepożądanych są nieokreślone. (Sekcja 6.5 s. 67)

Kolejność oceny operandów jest nieokreślona. W przypadku próby zmodyfikowania wyniku operatora przypisania lub uzyskania dostępu do niego po następnym punkcie sekwencji zachowanie [sic] jest niezdefiniowane (Rozdział 6.5.16 s. 91)

awiebe
źródło
2
Pytanie jest oznaczone jako C ++, a nie C, co jest dobre, ponieważ zachowanie w C ++ 17 jest zupełnie inne niż zachowanie w starszych wersjach - i nie ma żadnego związku z zachowaniem w C11, C99, C90 itp. Lub nosi bardzo mało związek z tym. Ogólnie sugeruję usunięcie tego. Co ważniejsze, musimy znaleźć równoważne pytania i odpowiedzi dla C i upewnić się, że jest w porządku (i zauważa, że ​​w szczególności C ++ 17 zmienia reguły - zachowanie w C ++ 11 i wcześniejszych było mniej więcej takie samo jak w C11, chociaż słowo opisujące to w C nadal używa „punktów sekwencji”, podczas gdy C ++ 11 i nowsze wersje tego nie robią
Jonathan Leffler