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?
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:
#include <iostream>
int main()
{
char* p = "hello!\n"; // yes I know, deprecated conversion
p[0] = 'y';
p[5] = 'w';
std::cout << p;
}
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 :
Efekt próby modyfikacji literału łańcuchowego jest niezdefiniowany.
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 :
Opisy semantyczne w niniejszej Normie Międzynarodowej definiują sparametryzowaną niedeterministyczną maszynę abstrakcyjną.
Niektóre aspekty i operacje maszyny abstrakcyjnej są opisane w niniejszej Normie Międzynarodowej jako zdefiniowane w implementacji (na przykład
sizeof(int)
). Stanowią one parametry abstrakcyjnej maszyny. Każde wdrożenie powinno zawierać dokumentację opisującą jego cechy i zachowanie w tym zakresie.Niektóre inne aspekty i operacje maszyny abstrakcyjnej są opisane w niniejszej Normie Międzynarodowej jako nieokreślone (na przykład kolejność oceny argumentów funkcji). Tam, gdzie to możliwe, niniejszy standard międzynarodowy określa zestaw dopuszczalnych zachowań. Definiują one niedeterministyczne aspekty abstrakcyjnej maszyny.
Niektóre inne operacje są opisane w niniejszej Normie Międzynarodowej jako niezdefiniowane (na przykład efekt dereferencji wskaźnika zerowego). [ Uwaga : ta Norma Międzynarodowa nie nakłada żadnych wymagań na zachowanie programów, które zawierają niezdefiniowane zachowanie. - uwaga końcowa ]
W szczególności sekcja 1.3.24 stanowi:
Dopuszczalne niezdefiniowane zachowanie obejmuje od całkowitego zignorowania sytuacji z nieprzewidzianymi rezultatami , zachowania podczas tłumaczenia lub wykonywania programu w udokumentowany sposób charakterystyczny dla środowiska (z wydaniem komunikatu diagnostycznego lub bez niego), aż do zakończenia tłumaczenia lub wykonania (z wydaniem komunikatu diagnostycznego).
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.
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
3.4.1 1 zachowanie zdefiniowane w ramach wdrożenia nieokreślone zachowanie, w którym każde wdrożenie dokumentuje sposób dokonania wyboru
2 PRZYKŁAD Przykładem zachowania zdefiniowanego w implementacji jest propagacja bitu wyższego rzędu, gdy liczba całkowita ze znakiem jest przesunięta w prawo.
3.4.3 1 zachowanie nieokreślone , po zastosowaniu nieprzenośnej lub błędnej konstrukcji programu lub błędnych danych, dla których niniejsza Norma Międzynarodowa nie nakłada żadnych wymagań
2 UWAGA Możliwe niezdefiniowane zachowanie się obejmuje 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 komunikatu diagnostycznego lub bez niego), do zakończenia tłumaczenia lub wykonania (z wydanie komunikatu diagnostycznego).
3 PRZYKŁAD Przykładem niezdefiniowanego zachowania jest zachowanie przy przepełnieniu liczb całkowitych.
3.4.4 1 nieokreślone zachowanie użycie nieokreślonej wartości lub inne zachowanie, w przypadku gdy niniejsza norma międzynarodowa zapewnia dwie lub więcej możliwości i nie nakłada żadnych dalszych wymagań, w odniesieniu do których wybrano się w każdym przypadku
2 PRZYKŁAD Przykładem nieokreślonego zachowania jest kolejność, w jakiej są oceniane argumenty funkcji.
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 pasuje char
. 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ć go char
. 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?
void fun(int n, int m);
int fun1()
{
cout << "fun1";
return 1;
}
int fun2()
{
cout << "fun2";
return 2;
}
...
fun(fun1(), fun2()); // which one is executed first?
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 :)
bo
fun(fun1(), fun2());
czy zachowanie nie jest „zdefiniowane”? W końcu kompilator musi wybrać jeden lub drugi kurs?
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ć, że sizeof(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 :
Twierdzi się, że różnica między tym, co można wytworzyć, dając kompilatorowi swobodę i wymagając „zwykłej oceny od lewej do prawej” może być znacząca. Nie jestem przekonany, ale z niezliczonymi kompilatorami „tam” korzystającymi z wolności i niektórymi ludźmi z pasją broniącymi tej wolności, zmiana byłaby trudna i może zająć dziesięciolecia, aby dotrzeć do odległych zakątków światów C i C ++. Jestem rozczarowany, że nie wszystkie kompilatory ostrzegają przed kodem takim jak ++ i + i ++. Podobnie, kolejność oceny argumentów jest nieokreślona.
IMO zdecydowanie zbyt wiele „rzeczy” pozostaje niezdefiniowanych, nieokreślonych, zdefiniowanych w implementacji itp. Łatwo jednak powiedzieć, a nawet podać przykłady, ale trudne do naprawienia. Należy również zauważyć, że nie jest wcale tak trudno uniknąć większości problemów i stworzyć przenośny kod.
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.
Terminy nieokreślone zachowanie, niezdefiniowane zachowanie i zachowanie zdefiniowane w implementacji są używane do kategoryzacji wyników pisania programów, których właściwości Standard nie opisuje lub nie może całkowicie opisać. Celem przyjęcia tej kategoryzacji jest dopuszczenie pewnej różnorodności wśród implementacji, która pozwala, aby jakość implementacji była aktywną siłą na rynku, a także dopuszczenie niektórych popularnych rozszerzeń, bez usuwania cła zgodności ze standardem. Dodatek F do katalogu Standardowego zawiera zachowania, które należą do jednej z tych trzech kategorii.
Nieokreślone zachowanie daje implementatorowi pewną swobodę w tłumaczeniu programów. Ta szerokość geograficzna nie rozciąga się na tyle, że nie udało się przetłumaczyć programu.
Niezdefiniowane zachowanie pozwala licencji na implementację nie wychwytywać niektórych błędów programu, które są trudne do zdiagnozowania. Identyfikuje również obszary możliwego rozszerzenia języka zgodnego: implementator może ulepszyć język, podając definicję oficjalnie niezdefiniowanego zachowania.
Zachowanie zdefiniowane w implementacji daje implementatorowi swobodę wyboru odpowiedniego podejścia, ale wymaga wyjaśnienia użytkownikowi tego wyboru. Zachowania oznaczone jako zdefiniowane w implementacji to na ogół te, w których użytkownik może podejmować znaczące decyzje dotyczące kodowania na podstawie definicji implementacji. Realizatorzy powinni wziąć pod uwagę to kryterium przy podejmowaniu decyzji o tym, jak szeroka powinna być definicja implementacji. Podobnie jak w przypadku nieokreślonego zachowania, zwykła niemożność przetłumaczenia źródła zawierającego zachowanie zdefiniowane w implementacji nie jest odpowiednią odpowiedzią.
Niezdefiniowane zachowanie vs. Nieokreślone zachowanie ma krótki opis.
Ich końcowe podsumowanie:
Podsumowując, nieokreślone zachowanie jest zwykle czymś, o co nie powinieneś się martwić, chyba że twoje oprogramowanie musi być przenośne. I odwrotnie, niezdefiniowane zachowanie jest zawsze niepożądane i nigdy nie powinno wystąpić.
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:
int scaled_velocity(int v, unsigned char pow)
{
if (v > 250)
v = 250;
if (v < -250)
v = -250;
return v << pow;
}
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, czy v
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 zakres v
. 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ć.
<<
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
zachowanie, w przypadku poprawnie sformułowanego programu konstruuj i poprawiaj dane, które zależą od implementacji i tego, że każda implementacja dokumentuje
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
zachowanie, w stosunku do którego niniejszy standard międzynarodowy nie nakłada żadnych wymagań [Uwaga: Można oczekiwać nieokreślonego zachowania, gdy ten standard międzynarodowy pominie jakąkolwiek wyraźną definicję zachowania lub gdy program użyje błędnej konstrukcji lub błędnych danych. Dopuszczalne niezdefiniowane zachowanie obejmuje od całkowitego zignorowania sytuacji z nieprzewidzianymi rezultatami, zachowania podczas tłumaczenia lub wykonywania programu w udokumentowany sposób charakterystyczny dla środowiska (z wydaniem komunikatu diagnostycznego lub bez niego), aż do zakończenia tłumaczenia lub wykonania (z wydaniem komunikatu diagnostycznego). Wiele błędnych konstrukcji programów nie powoduje nieokreślonego zachowania; należy je zdiagnozować. - uwaga końcowa]
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
zachowanie, dla poprawnie sformułowanej konstrukcji programu i poprawnych danych, które zależą od implementacji [Uwaga: Implementacja nie jest wymagana do udokumentowania zachowań. Zakres możliwych zachowań jest zwykle określony przez ten Międzynarodowy Standard. - uwaga końcowa]
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.
Wdrożenie zdefiniowane
Wdrażający chcą, powinien być dobrze udokumentowany, standard daje wybory, ale na pewno się skompiluje
Nieokreślony -
Taki sam jak zdefiniowany w implementacji, ale nieudokumentowany
Nieokreślony-
Cokolwiek może się zdarzyć, zajmij się tym.
uint32_t s;
, 1u<<s
kiedy s
33 ma być może dać 0, a może 2, ale nie robić nic dziwnego. Nowsze kompilatory, jednak ocena 1u<<s
może spowodować, że kompilator ustali, że ponieważ wcześniej s
musiał mieć mniej niż 32, dowolny kod przed lub po tym wyrażeniu, który byłby istotny tylko, gdyby s
miał 32 lub więcej, może zostać pominięty.