Czy kod, który nigdy nie zostanie wykonany, może wywołać niezdefiniowane zachowanie?

81

Kod, który wywołuje niezdefiniowane zachowanie (w tym przykładzie dzielenie przez zero) nigdy nie zostanie wykonany, czy program jest nadal niezdefiniowanym zachowaniem?

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?

Yu Hao
źródło
7
Powiedziałbym, że nie jest to „zachowanie”, jeśli nigdy nie zostanie wykonane
Kevin
1
Jeśli UB jest runtime one (w ten sposób) - nie byłoby. Ale bardzo wątpię, że standard cokolwiek o tym mówi.
keltar
13
Brzmi jak kwestia semantyki, a nie programowania.
Wooble
14
@Wooble Nie zgadzam się. Wyrażenie niezdefiniowane zachowanie ma specjalne znaczenie w C / C ++. A to pytanie jest związane z innymi sytuacjami, które determinują nieokreślone zachowanie lub nie. Dla przypomnienia, jeśli przeczytałeś standard C / C ++, wszędzie znajdziesz zwrot niezdefiniowany zachowanie .
Yu Hao
10
@Cornstalks: Standard C nie używa wyrażenia „wywołuje niezdefiniowane zachowanie”, więc nie można wnioskować o standardzie C na podstawie tego, co może oznaczać to zdanie. Używanie go do opisu C jest niewłaściwe, ponieważ sugeruje, że „niezdefiniowane zachowanie” to coś takiego jak ściana, na którą natkniesz się, gdy przekroczysz granice. W rzeczywistości „niezdefiniowane zachowanie” to brak rzeczy; to koniec granic. Opuszczając dobrze zdefiniowane miasto, które jest standardem C, znajdujesz się na otwartym polu, na którym można zbudować wszystko.
Eric Postpischil

Odpowiedzi:

70

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:

zachowanie
wygląd zewnętrzny lub działanie

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:

niezdefiniowane zachowanie
, po zastosowaniu nieprzenoszalnej lub błędnej konstrukcji programu lub błędnych danych, dla których niniejsza Norma Międzynarodowa nie nakłada żadnych wymagań

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:

UWAGA Możliwe niezdefiniowane zachowanie obejmuje zarówno całkowite zignorowanie sytuacji z nieprzewidywalnymi skutkami, zachowanie podczas tłumaczenia lub wykonywania programu w udokumentowany sposób charakterystyczny dla środowiska (z lub bez wydania komunikatu diagnostycznego), jak i przerwanie tłumaczenia lub wykonania (z wydanie komunikatu diagnostycznego).

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:

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:

który nigdy nie wykonuje dzielenia przez 0. Bardzo popularnym idiomem jest:

Podział z pewnością ma niezdefiniowane zachowanie, jeśli y == 0, ale nigdy nie jest wykonywany, jeśli y == 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

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:

wtedy 1/0jest 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ście if (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 * zprzypadku sekwencja x + ynie 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ć:

Zatem w poniższej inicjalizacji
static int i = 2 || 1 / 0;
wyrażenie jest prawidłowym wyrażeniem stałym o wartości całkowitej o wartości jeden.

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):

  • Niepusty plik źródłowy nie kończy się znakiem nowego wiersza, który nie jest bezpośrednio poprzedzony znakiem ukośnika odwrotnego lub kończy się częściowym znacznikiem wstępnego przetwarzania lub komentarzem
  • Łączenie tokenów tworzy sekwencję znaków zgodną ze składnią uniwersalnej nazwy znaku
  • W pliku źródłowym napotkano znak spoza podstawowego zestawu znaków źródłowych, z wyjątkiem identyfikatora, stałej znakowej, literału ciągu, nazwy nagłówka, komentarza lub tokenu przetwarzania wstępnego, który nigdy nie jest konwertowany na token
  • Identyfikator, komentarz, literał ciągu, stała znakowa lub nazwa nagłówka zawiera nieprawidłowy znak wielobajtowy lub nie zaczyna się i nie kończy w początkowym stanie przesunięcia
  • Ten sam identyfikator ma powiązania wewnętrzne i zewnętrzne w tej samej jednostce tłumaczeniowej

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.

Keith Thompson
źródło
2
@EricPostpischil 6.6 / 4 mówi: "Każde wyrażenie stałe będzie obliczane na stałą, która znajduje się w zakresie reprezentowalnych wartości dla swojego typu." Czy to nie wykluczałoby 1/0bycia stałą ekspresją?
Casey
2
@EricPostpischil: Nie sądzę, żeby to było do końca w porządku. Naruszenie ograniczenia ogólnie oznacza, że ​​wymagana jest diagnostyka w czasie kompilacji, a nie tylko to, że coś, co w przeciwnym razie mogłoby być foo, nie jest foo . 1/0nie 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.
Keith Thompson
DR # 109 wydaje się wskazywać, że program nie jest UB, zobacz nową odpowiedź, którą właśnie opublikowałem.
ouah
1
Re „ więc wracamy do powszechnego angielskiego znaczenia ”. W znaczeniu angielskim program używa konstrukcji, jeśli jest ona obecna w programie. Dlaczego więc twoja odpowiedź zakłada, że ​​użycie konstrukcji oznacza wykonanie konstrukcji? Twój wniosek nie wynika z twojego wyjaśnienia!
ikegami
31

W tym artykule omówiono to pytanie w sekcji 2.6:

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 .:

Wydaje się, że intencją standardu 11 jest to, że ogólnie sytuacje są statycznie niezdefiniowane, jeśli nie jest łatwo wygenerować dla nich kod. Tylko wtedy, gdy można wygenerować kod, sytuacja może być niezdefiniowana dynamicznie.

11) Korespondencja prywatna z członkiem komisji.

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.

Pascal Cuoq
źródło
1
Ten przykład przedstawia trudności tylko w kwestii tego, czy można statycznie określić, czy jego zachowanie jest niezdefiniowane. Po uruchomieniu (zakładając, że zachowanie guard()nie jest niezdefiniowane), zachowanie jest niezdefiniowane wtedy i tylko wtedy, gdy instrukcja 5 / 0;jest faktycznie wykonywana. (Zauważ, że kompilator może w uzasadniony sposób zastąpić ocenę przez 5 / 0wywołanie abort()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ć, że guard()zawsze się zakończy.
Keith Thompson
@KeithThompson Aby wyjaśnić różnicę statyczną / dynamiczną w artykule, 5/0 jest uważane za dynamiczne, ponieważ kompilator może wygenerować kod, który dzieli przez zero: po prostu wygeneruj zwykły kod, który dzieli przez z po ustawieniu z na 0. Zatem naiwny kompilator potrafi wygenerować instrukcję dzielenia. Wyrafinowany kompilator, który stwierdza, że 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…
Pascal Cuoq
@KeithThompson… kompilator może odrzucić program if (0) (int)(void)5;ze względu na zagadkę, którą przedstawia naiwnemu kompilatorowi, podczas gdy nieosiągalny dynamiczny UB, taki jak if (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ę.
Pascal Cuoq
2
(int)(void)5jest 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)5nie ma typu skalarnego, więc (int)(void)5narusza to ograniczenie, niezależnie od tego, czy zawierający go kod jest kiedykolwiek wykonywany.
Keith Thompson
@KeithThompson Tak, wydaje się, że wybrali zły przykład, ale na długiej liście w J.2 jest taki, który nie narusza ograniczenia i jest z pewnością „statyczny”? A co z tym starym klasykiem: „Niepusty plik źródłowy nie kończy się znakiem nowej linii…”? Nie ma pojęcia osiągalności, które odnosi się do tego, ale nie jest to naruszenie ograniczenia, prawda?
Pascal Cuoq
5

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).

Arnaud Le Blanc
źródło
Jako przykład przypadku nr 2 rozważmy, #include "//e"który wywołuje UB.
Michael Foukarakis
2

Poszedłbym z ostatnim akapitem tej odpowiedzi: https://stackoverflow.com/a/18384176/694576

... UB to problem z uruchomieniem, a nie z kompilacją ...

Więc nie, nie ma wywoływanego UB.

alk
źródło
2
Nie powinieneś wierzyć we wszystko, co czytasz w Internecie, zwłaszcza nie w odpowiedzi na StackOverflow.
Pascal Cuoq
1
@PascalCuoq To podważa wiarę kilku tak wierzących jak ja. Dokąd teraz iść?
0decimal0
2

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 .

Hrishi
źródło
2

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):

1 niezdefiniowane zachowanie
  zachowanie, po użyciu nieprzenoszalnej lub błędnej konstrukcji programu lub programu
  błędne dane, dla których niniejsza Norma Międzynarodowa nie narzuca żadnych wymagań
2 UWAGA Możliwe niezdefiniowane zachowanie waha się od całkowitego ignorowania sytuacji
  z nieprzewidywalnymi skutkami, do zachowania podczas tłumaczenia lub wykonywania programu
  w udokumentowany sposób charakterystyczny dla środowiska (z lub bez
  wydanie komunikatu diagnostycznego), przerwania tłumaczenia lub
  wykonanie (wraz z wydaniem komunikatu diagnostycznego).

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.

Nicola Musatti
źródło
2

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.

dhein
źródło
0 jest zawsze prawdziwe? czy nawet zawsze prawda? czy jesteś rubinem ?!
Grady Player
@Grady PlayerNo Byłem tylko jakimś Brain afk. Naprawię to, przepraszam
dhein
2

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.

Myślę, że program nie wywołuje niezdefiniowanego zachowania.

Raport o defektach nr 109 dotyczy podobnego pytania i mówi:

Ponadto, jeśli każde możliwe wykonanie danego programu powodowałoby niezdefiniowane zachowanie, to dany program nie jest ściśle zgodny. Zgodna implementacja nie może zawieść w tłumaczeniu ściśle zgodnego programu po prostu dlatego, że niektóre możliwe wykonanie tego programu spowodowałoby niezdefiniowane zachowanie. Ponieważ foo może nigdy nie zostać wywołane, podany przykład musi zostać pomyślnie przetłumaczony przez zgodną implementację.

ouah
źródło
-1

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.

user3138160
źródło