Kod, który wywołuje niezdefiniowane zachowanie (w tym przykładzie dzielenie przez zero) nigdy nie zostanie wykonany, czy program jest nadal niezdefiniowanym zachowaniem?
int main(void)
{
int i;
if(0)
{
i = 1/0;
}
return 0;
}
Myślę, że nadal jest to niezdefiniowane zachowanie, ale nie mogę znaleźć w standardzie żadnych dowodów, które by mnie wspierały lub zaprzeczały.
A więc jakieś pomysły?
c
language-lawyer
Yu Hao
źródło
źródło
Odpowiedzi:
Przyjrzyjmy się, jak standard C definiuje terminy „zachowanie” i „niezdefiniowane zachowanie”.
Odniesienia dotyczą projektu normy ISO C 2011 N1570 ; Nie znam żadnych istotnych różnic w żadnej z trzech opublikowanych norm ISO C (1990, 1999 i 2011).
Sekcja 3.4:
Ok, to trochę niejasne, ale twierdzę, że dana instrukcja nie ma „wyglądu”, a na pewno nie ma „akcji”, chyba że jest faktycznie wykonana.
Sekcja 3.4.3:
Mówi „ po użyciu ” takiej konstrukcji. Słowo „używać” nie jest zdefiniowane w standardzie, więc wracamy do potocznego angielskiego znaczenia. Konstrukcja nie jest „używana”, jeśli nigdy nie została wykonana.
Pod tą definicją jest uwaga:
Zatem kompilator może odrzucić twój program w czasie kompilacji, jeśli jego zachowanie jest nieokreślone. Ale moja interpretacja jest taka, że może to zrobić tylko wtedy, gdy może udowodnić, że każde wykonanie programu napotka niezdefiniowane zachowanie. Co oznacza, jak sądzę, że:
if (rand() % 2 == 0) { i = i / 0; }
które z pewnością mogą mieć niezdefiniowane zachowanie, nie można ich odrzucić w czasie kompilacji.
Z praktycznego punktu widzenia programy muszą mieć możliwość wykonywania testów w czasie wykonywania, aby chronić się przed wywołaniem niezdefiniowanego zachowania, a standard musi im na to zezwalać.
Twój przykład to:
if (0) { i = 1/0; }
który nigdy nie wykonuje dzielenia przez 0. Bardzo popularnym idiomem jest:
int x, y; /* set values for x and y */ if (y != 0) { x = x / y; }
Podział z pewnością ma niezdefiniowane zachowanie, jeśli
y == 0
, ale nigdy nie jest wykonywany, jeśliy == 0
. Zachowanie jest dobrze zdefiniowane iz tego samego powodu, z którego dobrze zdefiniowano twój przykład: ponieważ potencjał niezdefiniowane zachowanie nigdy nie może się wydarzyć.(Chyba że
INT_MIN < -INT_MAX && x == INT_MIN && y == -1
(tak, dzielenie liczb całkowitych może się przepełnić), ale to osobny problem.)W komentarzu (od czasu usunięcia) ktoś wskazał, że kompilator może oceniać stałe wyrażenia w czasie kompilacji. Co jest prawdą, ale nie ma znaczenia w tym przypadku, ponieważ w kontekście
i = 1/0;
1/0
nie jest wyrażeniem stałym .Stała ekspresja jest składniowym kategoria, która redukuje się do warunkowej ekspresji (która nie obejmuje zadania i wyrażenia przecinek). Wyrażenie stałe produkcji pojawia się w gramatyce tylko w kontekstach, które faktycznie wymagają stałego wyrażenia, takich jak etykiety przypadków. Więc jeśli napiszesz:
switch (...) { case 1/0: ... }
wtedy
1/0
jest wyrażeniem stałym - i takim, które narusza ograniczenie w 6.6p4: „Każde wyrażenie stałe będzie obliczane na stałą, która znajduje się w zakresie reprezentowalnych wartości dla swojego typu.”, dlatego wymagana jest diagnostyka. Ale prawa strona przypisania nie wymaga wyrażenia stałego , a jedynie wyrażenia warunkowego , więc ograniczenia wyrażeń stałych nie mają zastosowania. Kompilator może ocenić dowolne wyrażenie, że to jest w stanie w czasie kompilacji, ale tylko wtedy, gdy zachowanie jest takie samo, jak gdyby były oceniane w trakcie realizacji (lub, w kontekścieif (0)
, nie ocenianego w trakcie realizacji ().(Coś, co wygląda dokładnie jak wyrażenie stałe, niekoniecznie jest wyrażeniem stałym , tak jak w
x + y * z
przypadku sekwencjax + y
nie jest wyrażeniem addytywnym ze względu na kontekst, w którym się pojawia).Co oznacza przypis w N1570 sekcja 6.6, który zamierzałem zacytować:
właściwie nie ma związku z tym pytaniem.
Na koniec jest kilka rzeczy, które są zdefiniowane jako powodujące niezdefiniowane zachowanie, które nie dotyczy tego, co dzieje się podczas wykonywania. Załącznik J, sekcja 2 normy C (ponownie, patrz projekt N1570 ) wymienia rzeczy, które powodują nieokreślone zachowanie, zebrane z pozostałej części normy. Oto kilka przykładów (nie twierdzę, że jest to pełna lista):
Te szczególne przypadki to rzeczy, które kompilator może wykryć. Myślę, że ich zachowanie jest nieokreślone, ponieważ komitet nie chciał lub nie mógł narzucić tego samego zachowania we wszystkich implementacjach, a określenie zakresu dozwolonych zachowań po prostu nie było warte wysiłku. Tak naprawdę nie należą one do kategorii „kodu, który nigdy nie zostanie wykonany”, ale wspominam o nich tutaj dla kompletności.
źródło
1/0
bycia stałą ekspresją?1/0
nie jest wyrażeniem stałym w kontekście pytania, ponieważ nie jest analizowane jako wyrażenie stałe , a jedynie jako wyrażenie warunkowe, które jest częścią wyrażenia przypisania .case 1/0:
naruszyłoby to ograniczenie i wymagałoby diagnozy.W tym artykule omówiono to pytanie w sekcji 2.6:
int main(void){ guard(); 5 / 0; }
Autorzy uważają, że program jest definiowany, gdy
guard()
się nie kończy. Wyróżniają również pojęcia „statycznie niezdefiniowany” i „dynamicznie niezdefiniowany”, np .:Poleciłbym przejrzeć cały artykuł. Razem tworzy spójny obraz.
Fakt, że autorzy artykułu musieli omówić to pytanie z członkiem komisji, potwierdza, że standard jest obecnie niejasny w odpowiedzi na Twoje pytanie.
źródło
guard()
nie jest niezdefiniowane), zachowanie jest niezdefiniowane wtedy i tylko wtedy, gdy instrukcja5 / 0;
jest faktycznie wykonywana. (Zauważ, że kompilator może w uzasadniony sposób zastąpić ocenę przez5 / 0
wywołanieabort()
lub coś podobnego; program przerwałby wtedy wtedy i tylko wtedy, gdy wykonanie osiągnęło ten punkt.) Kompilator może odrzucić ten program tylko wtedy, gdy może stwierdzić, żeguard()
zawsze się zakończy.guard()
nie kończy działania, nie musi w ogóle generować żadnego kodu dla 5/0. W przeciwieństwie do tego, nie ma sposobu na wygenerowanie kodu(int)(void)5
, nie można po prostu wygenerować kodu(int)(void)z
, ponieważ to też nie jest poprawne. Więc autorzy uważają, że…if (0) (int)(void)5;
ze względu na zagadkę, którą przedstawia naiwnemu kompilatorowi, podczas gdy nieosiągalny dynamiczny UB, taki jakif (0) 5 / 0;
nieszkodliwy. To wynika z ich dyskusji z członkiem komisji i widziałem podobny argument wysuwany gdzie indziej (ale być może z tego samego źródła, zwłaszcza że nie pamiętam, gdzie to było). W tej chwili przechodzę przez uzasadnienie C99, jeśli zobaczę jakąkolwiek wzmiankę o tym, wrócę i zwrócę uwagę.(int)(void)5
jest naruszeniem ograniczenia. N1570 6.5.4, opisujący operator rzutowania: „Ograniczenia: O ile nazwa typu nie określa typu pustego, nazwa typu powinna określać niepodzielny, kwalifikowany lub niekwalifikowany typ skalarny, a argument operacji powinien mieć typ skalarny.”.(void)5
nie ma typu skalarnego, więc(int)(void)5
narusza to ograniczenie, niezależnie od tego, czy zawierający go kod jest kiedykolwiek wykonywany.W tym przypadku niezdefiniowane zachowanie jest wynikiem wykonania kodu. Więc jeśli kod nie zostanie wykonany, nie ma niezdefiniowanego zachowania.
Niewykonany kod mógłby wywołać niezdefiniowane zachowanie, gdyby niezdefiniowane zachowanie było wynikiem wyłącznie deklaracji kodu (np. Gdyby jakiś przypadek cieniowania zmiennej był niezdefiniowany).
źródło
#include "//e"
który wywołuje UB.Poszedłbym z ostatnim akapitem tej odpowiedzi: https://stackoverflow.com/a/18384176/694576
Więc nie, nie ma wywoływanego UB.
źródło
Tylko wtedy, gdy standard wprowadza istotne zmiany i Twój kod nagle nie jest już „nigdy wykonywany”. Ale nie widzę żadnego logicznego sposobu, w jaki mogłoby to spowodować „niezdefiniowane zachowanie”. To nic nie powoduje .
źródło
W przypadku niezdefiniowanych zachowań często trudno oddzielić aspekty formalne od praktycznych. To jest definicja niezdefiniowanego zachowania w standardzie z 1989 roku (nie mam pod ręką nowszej wersji, ale nie spodziewam się, że zmieni się to znacząco):
Z formalnego punktu widzenia powiedziałbym, że twój program wywołuje niezdefiniowane zachowanie, co oznacza, że standard nie nakłada żadnych wymagań na to, co zrobi po uruchomieniu, tylko dlatego, że zawiera dzielenie przez zero.
Z drugiej strony, z praktycznego punktu widzenia, zdziwiłbym się, znajdując kompilator, który nie zachowywał się tak, jak intuicyjnie się tego spodziewasz.
źródło
Norma mówi, że o ile dobrze pamiętam, od tej chwili można robić wszystko, złamano zasadę. Może są jakieś specjalne wydarzenia o charakterze globalnym (ale nigdy nie słyszałem ani nie czytałem o czymś takim) ... Więc powiedziałbym: Nie, to nie może być UB, ponieważ tak długo, jak zachowanie jest dobrze zdefiniowane, 0 jest zawsze false, więc reguła nie może zostać złamana w czasie wykonywania.
źródło
Myślę, że program nie wywołuje niezdefiniowanego zachowania.
Raport o defektach nr 109 dotyczy podobnego pytania i mówi:
źródło
Zależy to od tego, jak zdefiniowane jest wyrażenie „niezdefiniowane zachowanie” i czy „niezdefiniowane zachowanie” instrukcji jest tym samym, co „niezdefiniowane zachowanie” dla programu.
Ten program wygląda jak C, więc głębsza analiza tego, jaki standard C jest używany przez kompilator (jak to robiły niektóre odpowiedzi) jest właściwa.
W przypadku braku określonego standardu prawidłowa odpowiedź brzmi „to zależy”. W niektórych językach kompilatory po pierwszym błędzie próbują odgadnąć, co programista może mieć na myśli i nadal generują kod, zgodnie z domysłami kompilatorów. W innych, bardziej czystych językach, gdy coś jest nieokreślone, to nieokreśloność rozprzestrzenia się na cały program.
W innych językach występuje pojęcie „ograniczonych błędów”. W przypadku niektórych ograniczonych rodzajów błędów języki te określają, ile szkód może spowodować błąd. W niektórych językach z niejawnym wyrzucaniem elementów bezużytecznych często decyduje o tym, czy błąd unieważnia system pisania, czy też nie.
źródło