Co to jest niezdefiniowane zachowanie w C i C ++? Co z nieokreślonym zachowaniem i zachowaniem zdefiniowanym w implementacji? Jaka jest różnica między nimi?
529
Co to jest niezdefiniowane zachowanie w C i C ++? Co z nieokreślonym zachowaniem i zachowaniem zdefiniowanym w implementacji? Jaka jest różnica między nimi?
Odpowiedzi:
Niezdefiniowane zachowanie jest jednym z tych aspektów języka C i C ++, które mogą być zaskakujące dla programistów pochodzących z innych języków (inne języki starają się to lepiej ukrywać). Zasadniczo możliwe jest pisanie programów C ++, które nie zachowują się w przewidywalny sposób, mimo że wiele kompilatorów C ++ nie zgłasza żadnych błędów w programie!
Spójrzmy na klasyczny przykład:
Zmienna
p
wskazuje na literał łańcucha"hello!\n"
, a dwa poniższe przypisania próbują zmodyfikować ten literał łańcucha. Co robi ten program? Zgodnie z sekcją 2.14.5 pkt 11 standardu C ++ wywołuje niezdefiniowane zachowanie :Słyszę krzyki ludzi: „Ale poczekaj, mogę to skompilować bez problemu i uzyskać wynik
yellow
” lub „Co to znaczy niezdefiniowane, literały łańcuchowe są przechowywane w pamięci tylko do odczytu, więc pierwsza próba przypisania powoduje zrzut pamięci”. To jest dokładnie problem z niezdefiniowanym zachowaniem. Zasadniczo standard pozwala na wszystko, co się stanie, gdy wywołasz niezdefiniowane zachowanie (nawet demony nosowe). Jeśli zachowuje się „prawidłowe” zachowanie zgodnie z twoim mentalnym modelem języka, ten model jest po prostu zły; Standard C ++ ma jedyny głos, kropkę.Inne przykłady nieokreślonego zachowania obejmują dostęp do tablicy poza jej granice, dereferencję wskaźnika zerowego , dostęp do obiektów po zakończeniu ich życia lub pisanie rzekomo sprytnych wyrażeń, takich jak
i++ + ++i
.W sekcji 1.9 standardu C ++ wspomniano również o dwóch mniej niebezpiecznych zachowaniach braci, zachowaniu nieokreślonym i zachowaniu zdefiniowanym w ramach implementacji :
W szczególności sekcja 1.3.24 stanowi:
Co możesz zrobić, aby uniknąć nieokreślonego zachowania? Zasadniczo musisz czytać dobre książki C ++ autorów, którzy wiedzą o czym mówią. Pieprzyć samouczki internetowe. Pieprzyć bullschildt.
źródło
int f(){int a; return a;}
: wartośća
może zmieniać się między wywołaniami funkcji.Cóż, jest to w zasadzie prosta kopia-wklej ze standardu
źródło
int foo(int x) { if (x >= 0) launch_missiles(); return x << 1; }
że kompilator może ustalić, że ponieważ wszystkie sposoby wywoływania funkcji, która nie uruchamia pocisków, wywołują Nieokreślone zachowanie, mogą wywołaćlaunch_missiles()
bezwarunkowe wywołanie .Może łatwiejsze do zrozumienia sformułowanie byłoby łatwiejsze niż rygorystyczna definicja norm.
zachowanie zdefiniowane w implementacji
Język mówi, że mamy typy danych. Dostawcy kompilatorów określają, jakich rozmiarów będą używać, i dostarczają dokumentację tego, co zrobili.
niezdefiniowane zachowanie
Robisz coś złego. Na przykład masz bardzo dużą wartość
int
, która nie pasujechar
. Jak obliczyć tę wartośćchar
? właściwie nie ma mowy! Wszystko może się zdarzyć, ale najrozsądniejszą rzeczą byłoby wziąć pierwszy bajt tego inta i wstawić gochar
. Po prostu źle jest to zrobić, aby przypisać pierwszy bajt, ale tak dzieje się pod maską.nieokreślone zachowanie
Która z tych funkcji jest wykonywana jako pierwsza?
Język nie określa oceny, od lewej do prawej lub od prawej do lewej! Tak więc nieokreślone zachowanie może, ale nie musi, skutkować nieokreślonym zachowaniem, ale z pewnością twój program nie powinien generować nieokreślonego zachowania.
@eSKay Myślę, że twoje pytanie jest warte edycji odpowiedzi, aby wyjaśnić więcej :)
Różnica między definicją implementacji a nieokreśloną polega na tym, że kompilator powinien wybrać zachowanie w pierwszym przypadku, ale nie musi tak być w drugim przypadku. Na przykład implementacja musi mieć jedną i tylko jedną definicję
sizeof(int)
. Nie można więc powiedzieć, żesizeof(int)
dla jednej części programu jest to 4, a dla innych 8. W przeciwieństwie do nieokreślonego zachowania, w którym kompilator może powiedzieć OK, będę oceniał te argumenty od lewej do prawej, a argumenty następnej funkcji są oceniane od prawej do lewej. Może się to zdarzyć w tym samym programie, dlatego nazywa się to nieokreślonym . W rzeczywistości C ++ można by uprościć, gdyby określono niektóre nieokreślone zachowania. Spójrz tutaj na odpowiedź dr Stroustrupa na to :źródło
fun(fun1(), fun2());
to nie zachowanie"implementation defined"
? W końcu kompilator musi wybrać jeden lub drugi kurs?"I am gonna evaluate these arguments left-to-right and the next function's arguments are evaluated right-to-left"
rozumiem , że tak sięcan
stało. Czy tak naprawdę jest w kompilatorach, których używamy obecnie?Z oficjalnego dokumentu uzasadnienia C.
źródło
Niezdefiniowane zachowanie vs. Nieokreślone zachowanie ma krótki opis.
Ich końcowe podsumowanie:
źródło
Historycznie zarówno zachowanie zdefiniowane w implementacji, jak i zachowanie nieokreślone reprezentowały sytuacje, w których autorzy standardu oczekiwali, że osoby piszące implementacje wysokiej jakości wykorzystają osąd, aby zdecydować, jakie gwarancje behawioralne, jeśli takie, będą przydatne dla programów w zamierzonym polu aplikacji działających na zamierzone cele. Potrzeby wysokiej klasy kodu do łamania liczb są zupełnie inne niż w przypadku kodów systemów niskiego poziomu, a zarówno UB, jak i IDB dają autorom kompilatorów elastyczność w zaspokajaniu tych różnych potrzeb. Żadna z kategorii nie nakazuje, aby implementacje zachowywały się w sposób przydatny do określonego celu, a nawet do dowolnego celu. Wdrożenia jakościowe, które twierdzą, że są odpowiednie do określonego celu, powinny jednak zachowywać się w sposób odpowiadający temu celowiczy standard tego wymaga, czy nie .
Jedyna różnica między zachowaniem zdefiniowanym w implementacji a zachowaniem niezdefiniowanym polega na tym, że ten pierwszy wymaga, aby implementacje definiowały i dokumentowały spójne zachowanie, nawet w przypadkach, w których wdrożenie nic nie mogłoby być przydatne . Linia podziału między nimi nie polega na tym, czy generalnie przydatne byłyby implementacje w celu zdefiniowania zachowań (autorzy kompilatora powinni zdefiniować użyteczne zachowania, gdy jest to praktyczne, czy standard tego wymaga, czy też nie), ale czy mogą istnieć implementacje, w których zdefiniowanie zachowania byłoby jednocześnie kosztowne i bezużyteczne . Ocena, że takie implementacje mogą istnieć, w żaden sposób, nie kształtuje ani nie formułuje, nie sugeruje żadnej oceny przydatności wspierania określonego zachowania na innych platformach.
Niestety, od połowy lat 90. autorzy kompilatorów zaczęli interpretować brak mandatów behawioralnych jako osąd, że gwarancje behawioralne nie są warte kosztów nawet w obszarach zastosowań, w których są one niezbędne, a nawet w systemach, w których praktycznie nic nie kosztują. Zamiast traktować UB jako zaproszenie do rozsądnego osądu, autorzy kompilatorów zaczęli traktować to jako wymówkę, by tego nie robić.
Na przykład biorąc pod uwagę następujący kod:
implementacja uzupełnienia dwójkowego nie musiałaby podejmować żadnego wysiłku, aby traktować wyrażenie
v << pow
jako przesunięcie uzupełnienia dwójkowego bez względu na to, czyv
było ono pozytywne czy negatywne.Preferowana filozofia niektórych współczesnych autorów kompilatorów sugerowałaby jednak, że ponieważ
v
może być negatywna tylko wtedy, gdy program zamierza zaangażować się w Niezdefiniowane Zachowanie, nie ma powodu, aby program przycinał ujemny zakresv
. Mimo że przesunięcie w lewo wartości ujemnych było obsługiwane na każdym kompilatorze znaczenia, a duża ilość istniejącego kodu opiera się na tym zachowaniu, współczesna filozofia zinterpretowałaby fakt, że Standard mówi, że przesunięcie w lewo wartości ujemnych jest UB jako sugerując, że autorzy kompilatorów powinni swobodnie to zignorować.źródło
<<
jest UB na liczbach ujemnych, jest paskudną małą pułapką i cieszę się, że mi o tym przypominają!i+j>k
daje 1, czy 0 w przypadkach, gdy dodatek się przepełnia, pod warunkiem, że nie ma innych skutków ubocznych , kompilator może być w stanie dokonać pewnych masowych optymalizacji, które nie byłyby możliwe, gdyby programista napisał kod jako(int)((unsigned)i+j) > k
.Standard C ++ n3337 § 1.3.10 zachowanie zdefiniowane w implementacji
Czasami C ++ Standard nie narzuca określonego zachowania niektórym konstruktom, ale zamiast tego mówi, że konkretne, dobrze zdefiniowane zachowanie musi zostać wybrane i opisane przez konkretną implementację (wersję biblioteki). Dzięki temu użytkownik nadal może dokładnie wiedzieć, jak zachowa się program, nawet jeśli Standard tego nie opisuje.
Standard C ++ n3337 § 1.3.24 nieokreślone zachowanie
Gdy program napotka konstrukcję, która nie jest zdefiniowana zgodnie ze standardem C ++, można robić, co chce (może wysłać do mnie wiadomość e-mail lub wiadomość e-mail lub całkowicie zignorować kod).
Standard C ++ n3337 § 1.3.25 nieokreślone zachowanie
Standard C ++ nie narzuca określonych zachowań niektórym konstruktom, ale zamiast tego mówi, że konkretne, dobrze zdefiniowane zachowania muszą być wybrane ( bot nie jest koniecznie opisany ) przez określoną implementację (wersję biblioteki). Tak więc w przypadku, gdy nie podano opisu, może być trudno użytkownikowi dokładnie wiedzieć, jak zachowa się program.
źródło
Wdrożenie zdefiniowane
Nieokreślony -
Nieokreślony-
źródło
uint32_t s;
,1u<<s
kiedys
33 ma być może dać 0, a może 2, ale nie robić nic dziwnego. Nowsze kompilatory, jednak ocena1u<<s
może spowodować, że kompilator ustali, że ponieważ wcześniejs
musiał mieć mniej niż 32, dowolny kod przed lub po tym wyrażeniu, który byłby istotny tylko, gdybys
miał 32 lub więcej, może zostać pominięty.