(Dlaczego) używa niezdefiniowanej niezainicjowanej zmiennej?

82

Jeżeli mam:

jasne jest, że po tym wyrażeniu x powinno być zero, ale gdziekolwiek spojrzę, mówią, że zachowanie tego kodu jest nieokreślone, a nie tylko wartość x(aż do odejmowania).

Dwa pytania:

  • Czy zachowanie tego kodu jest rzeczywiście nieokreślone?
    (Np. Czy kod może ulec awarii [lub gorzej] w zgodnym systemie?)

  • Jeśli tak, dlaczego C mówi, że zachowanie jest nieokreślone, skoro jest całkowicie jasne, że xpowinno to wynosić zero?

    tj. jaka jest korzyść wynikająca z braku zdefiniowania tutaj zachowania?

Oczywiście kompilator mógłby po prostu użyć dowolnej wartości śmieciowej, którą uznałby za „przydatną” wewnątrz zmiennej i działałby zgodnie z przeznaczeniem… co jest złego w tym podejściu?

user541686
źródło
3
Jaka jest korzyść wynikająca z określenia tutaj specjalnego przypadku dla zachowania? Jasne, pozwala wszystkim powiększać i spowalniać nasze programy i biblioteki, ponieważ @Mehrdad chce uniknąć inicjowania zmiennej w jednym konkretnym i rzadkim przypadku.
Paul Tomblin,
9
@ W'rkncacnter Nie zgadzam się z tym, że jest naiwniakiem. Niezależnie od tego, jaką przybiera wartość, PO oczekuje, że będzie ona wynosić zero po x -= x. Powstaje pytanie, dlaczego w ogóle dostęp do niezainicjowanych wartości to UB.
Mysticial
6
Ciekawe, że instrukcja x = 0; jest zwykle konwertowany na xor x, x w asemblerze. To prawie to samo, co próbujesz tutaj zrobić, ale z xor zamiast odejmowania.
0xFE
1
„tj. jaka jest korzyść wynikająca z braku definicji zachowania w tym miejscu? '- pomyślałbym, że zaleta standardu, który nie wymienia nieskończoności wyrażeń z wartościami, które nie zależą od jednej lub więcej zmiennych, jest oczywista. Jednocześnie, @Paul, taka zmiana standardu nie spowodowałaby powiększenia programów i bibliotek.
Jim Balter,

Odpowiedzi:

90

Tak, to zachowanie jest nieokreślone, ale z innych powodów niż większość ludzi jest świadoma.

Po pierwsze, użycie wartości zjednostkowanej nie jest samo w sobie niezdefiniowanym zachowaniem, ale wartość jest po prostu nieokreślona. Dostęp do tego jest wtedy UB, jeśli wartość jest reprezentacją pułapki dla typu. Typy bez znaku rzadko mają reprezentacje pułapek, więc po tej stronie będziesz stosunkowo bezpieczny.

To, co sprawia, że ​​zachowanie jest niezdefiniowane, to dodatkowa właściwość twojej zmiennej, a mianowicie to, że „mogła być zadeklarowana z register”, czyli jej adres nigdy nie jest brany. Takie zmienne są traktowane specjalnie, ponieważ istnieją architektury, które mają rzeczywiste rejestry procesora, które mają rodzaj dodatkowego stanu, który jest „niezainicjowany” i który nie odpowiada wartości w domenie typu.

Edycja: Odpowiednia fraza normy to 6.3.2.1p2:

Jeśli lwartość wyznacza obiekt o automatycznym czasie przechowywania, który mógłby zostać zadeklarowany w klasie pamięci rejestru (nigdy nie miał odebranego adresu), a ten obiekt jest niezainicjalizowany (nie zadeklarowany za pomocą inicjatora i żadne przypisanie do niego nie zostało wykonane przed użyciem ), zachowanie jest nieokreślone.

Aby było jaśniej, poniższy kod jest legalny w każdych okolicznościach:

  • Tutaj pobierane są adresy ai b, więc ich wartość jest po prostu nieokreślona.
  • Ponieważ unsigned charnigdy nie ma reprezentacji pułapki, że nieokreślona wartość jest po prostu nieokreślona, ​​każda wartość unsigned charmoże się zdarzyć.
  • Na końcu a musi mieć wartość 0.

Edit2: a i bmają nieokreślone wartości:

3.19.3 wartość nieokreślona
ważna wartość odpowiedniego typu, jeśli niniejsza norma międzynarodowa nie nakłada żadnych wymagań dotyczących wyboru wartości w jakimkolwiek przypadku

Jens Gustedt
źródło
6
Być może czegoś mi brakuje, ale wydaje mi się, że z unsignedpewnością można przedstawić pułapki. Czy możesz wskazać część normy, która tak mówi? W §6.2.6.2 / 1 widzę, co następuje: „W przypadku typów całkowitych bez znaku innych niż znak bez znaku , bity reprezentacji obiektu powinny być podzielone na dwie grupy: bity wartości i bity wypełniające (nie musi być żadnej z tych ostatnich). ... to powinno być znane jako reprezentacja wartości. Wartości jakichkolwiek bitów wypełniających są nieokreślone. ⁴⁴⁾ "z komentarzem:" ⁴⁴⁾ Niektóre kombinacje bitów wypełniających mogą generować reprezentacje pułapek ".
conio
6
Kontynuując komentarz: „Niektóre kombinacje bitów wypełniających mogą generować reprezentacje pułapki, na przykład, jeśli jeden bit wypełniający jest bitem parzystości. Niezależnie od tego żadna operacja arytmetyczna na prawidłowych wartościach nie może wygenerować reprezentacji pułapki innej niż jako część wyjątkowego warunku, takiego jak przepełnienie i nie może wystąpić w przypadku typów bez znaku. " - To świetnie, gdy mamy prawidłową wartość do pracy, ale nieokreślona wartość może być reprezentacją pułapki przed inicjalizacją (np. Bit parzystości ustawiony nieprawidłowo).
conio
4
@conio Masz rację dla wszystkich typów innych niż unsigned char, ale ta odpowiedź jest używana unsigned char. Uwaga: ściśle zgodny program może obliczyć sizeof(unsigned) * CHAR_BITi określić, na podstawie UINT_MAXtego, że określone implementacje nie mogą mieć reprezentacji pułapek unsigned. Po tym, jak program określi to, może przystąpić do wykonania dokładnie tego, co robi ta odpowiedź unsigned char.
4
@JensGustedt: Czy nie jest memcpyto rozproszenie, tj. Twój przykład nie miałby zastosowania, gdyby został zastąpiony przez *&a = *&b;.
R .. GitHub PRZESTAŃ POMÓC W LODZIE
4
@R .. Nie jestem już pewien. Trwa dyskusja na liście mailingowej komitetu C i wydaje się, że wszystko to jest wielkim bałaganem, a mianowicie dużą luką między tym, jakie jest (lub było) zamierzonym zachowaniem, a tym, co faktycznie zostało zapisane. Jasne jest jednak, że dostęp do pamięci jako, unsigned chara zatem memcpypomaga, *&jest mniej jasny. Zgłoszę, gdy to się uspokoi.
Jens Gustedt
24

Standard C daje kompilatorom dużą swobodę w przeprowadzaniu optymalizacji. Konsekwencje tych optymalizacji mogą być zaskakujące, jeśli przyjmie się naiwny model programów, w których niezainicjowana pamięć jest ustawiona na jakiś losowy wzorzec bitowy, a wszystkie operacje są wykonywane w kolejności, w jakiej zostały zapisane.

Uwaga: poniższe przykłady są poprawne tylko dlatego, xże jego adres nigdy nie został zajęty, więc jest „podobny do rejestru”. Byłyby również ważne, gdyby typ xmiał reprezentacje pułapki; rzadko ma to miejsce w przypadku typów bez znaku (wymaga to „marnowania” co najmniej jednego bitu pamięci i musi być udokumentowane) i niemożliwe w przypadku unsigned char. Gdyby xmiał typ ze znakiem, to implementacja mogłaby zdefiniować wzór bitowy, który nie jest liczbą między - (2 n-1 -1) a 2 n-1 -1 jako reprezentację pułapki. Zobacz odpowiedź Jensa Gustedta .

Kompilatory próbują przypisać rejestry do zmiennych, ponieważ rejestry są szybsze niż pamięć. Ponieważ program może wykorzystywać więcej zmiennych niż procesor posiada rejestry, kompilatory dokonują alokacji rejestrów, co prowadzi do różnych zmiennych wykorzystujących ten sam rejestr w różnym czasie. Rozważ fragment programu

Kiedy wiersz 3 jest oceniany, xnie jest jeszcze zainicjowany, dlatego (uzasadnia kompilator) wiersz 3 musi być jakimś przypadkiem, który nie może się zdarzyć z powodu innych warunków, których kompilator nie był wystarczająco inteligentny, aby dowiedzieć się. Ponieważ znie jest używany po linii 4 i xnie jest używany przed linią 5, ten sam rejestr może być używany dla obu zmiennych. Tak więc ten mały program jest skompilowany do następujących operacji na rejestrach:

Końcowa wartość xto końcowa wartość r0, a końcowa wartość yto końcowa wartość r1. Te wartości to x = -3 i y = -4, a nie 5 i 4, jak by się stało, gdyby xzostał poprawnie zainicjowany.

Aby uzyskać bardziej rozbudowany przykład, rozważ następujący fragment kodu:

Załóżmy, że kompilator wykryje, że conditionnie ma to żadnego efektu ubocznego. Ponieważ conditionnie modyfikuje x, kompilator wie, że pierwszy przebieg pętli nie może uzyskać dostępu, xponieważ nie został jeszcze zainicjowany. Dlatego pierwsze wykonanie treści pętli jest równoważne x = some_value(), nie ma potrzeby testowania warunku. Kompilator może skompilować ten kod tak, jakbyś to napisał

Sposób, w jaki można to modelować w kompilatorze, polega na rozważeniu, że każda wartość zależna od xma jakąkolwiek wartość jest wygodna, o ile nie xjest zainicjowana. Ponieważ zachowanie, gdy niezainicjowana zmienna jest niezdefiniowana, a nie zmienna ma jedynie nieokreśloną wartość, kompilator nie musi śledzić żadnych specjalnych matematycznych relacji między wartościami, które są wygodne. Dlatego kompilator może przeanalizować powyższy kod w następujący sposób:

  • podczas pierwszej iteracji pętli nie xjest inicjowany do czasu -xoceny.
  • -x ma niezdefiniowane zachowanie, więc jego wartość jest taka, jaka jest-wygodna.
  • Obowiązuje reguła optymalizacji , więc ten kod można uprościć do .condition ? value : valuecondition; value

W konfrontacji z kodem w twoim pytaniu, ten sam kompilator analizuje, że kiedy x = - xjest oceniany, wartość -xjest cokolwiek-jest-wygodne. Dzięki temu można zoptymalizować przypisanie.

Nie szukałem przykładu kompilatora, który zachowuje się tak, jak opisano powyżej, ale jest to rodzaj optymalizacji, który dobre kompilatory próbują wykonać. Nie zdziwiłbym się, gdyby takiego spotkałem. Oto mniej prawdopodobny przykład kompilatora, z którym program ulega awarii. (Może to nie być takie nieprawdopodobne, jeśli kompilujesz swój program w jakimś zaawansowanym trybie debugowania).

Ten hipotetyczny kompilator mapuje każdą zmienną na innej stronie pamięci i ustawia atrybuty strony w taki sposób, że odczyt z niezainicjowanej zmiennej powoduje pułapkę procesora, która wywołuje debugger. Każde przypisanie do zmiennej najpierw upewnia się, że jej strona pamięci jest odwzorowana normalnie. Ten kompilator nie próbuje wykonywać żadnej zaawansowanej optymalizacji - działa w trybie debugowania, mającym na celu łatwe lokalizowanie błędów, takich jak niezainicjowane zmienne. Gdy x = - xjest oceniany, prawa strona powoduje pułapkę i uruchamia debuger.

Gilles 'SO- przestań być zły'
źródło
+1 Ładne wyjaśnienie, standardem jest szczególna troska o tę sytuację. Aby kontynuować tę historię, zobacz moją odpowiedź poniżej. (zbyt długi, aby mieć jako komentarz).
Jens Gustedt
@JensGustedt Och, twoja odpowiedź zawiera bardzo ważny punkt, który ja (i inni) przeoczyłem: chyba że typ ma wartości pułapki, które dla typu bez znaku wymaga „marnowania” co najmniej jednego bitu, xma niezainicjowaną wartość, ale zachowanie przy dostępie byłoby być zdefiniowane, jeśli x nie ma zachowania podobnego do rejestru.
SO- Gilles 'SO- przestań być zły'
@Gilles: przynajmniej clang dokonuje takich optymalizacji, o których wspomniałeś: (1) , (2) , (3) .
Vlad
1
Jaka praktyczna korzyść płynie z tego, że brzęk zajmuje się tym wszystkim? Jeśli dalszy kod nigdy nie używa wartości x, to wszystkie operacje na nim można pominąć, niezależnie od tego, czy jego wartość została zdefiniowana, czy nie. Gdyby kod następujący np. if (volatile1) x=volatile2; ... x = (x+volatile3) & 255;Byłby równie zadowolony z dowolnej wartości 0-255, która xmogłaby zawierać w przypadku, volatile1gdy przyniosłaby zero, pomyślałbym, że implementacja, która pozwoli programiście pominąć niepotrzebny zapis, xpowinna być uznana za wyższą jakość niż taka, która zachowywałby się ...
supercat
... w tym przypadku w zupełnie nieprzewidywalny sposób. Implementacja, która niezawodnie podniosłaby pułapkę zdefiniowaną przez implementację w tym przypadku, może, do pewnych celów, zostać uznana za mającą jeszcze wyższą jakość, ale zachowanie całkowicie nieprzewidywalne wydaje mi się zachowaniem o najniższej jakości w praktycznie dowolnym celu.
supercat
16

Tak, program może ulec awarii. Mogą na przykład istnieć reprezentacje pułapek (określone wzorce bitów, których nie można obsłużyć), które mogą spowodować przerwanie procesora, które nieobsłużone może spowodować awarię programu.

(6.2.6.1 na późnym szkicu C11 mówi) Niektóre reprezentacje obiektów nie muszą przedstawiać wartości typu obiektu. Jeśli przechowywana wartość obiektu ma taką reprezentację i jest odczytywana przez wyrażenie l-wartości, które nie ma typu znakowego, zachowanie jest niezdefiniowane. Jeśli taka reprezentacja jest wytwarzana przez efekt uboczny, który modyfikuje całość lub jakąkolwiek część obiektu za pomocą wyrażenia l-wartości, które nie ma typu znakowego, zachowanie jest niezdefiniowane.50) Taka reprezentacja nazywa się reprezentacją pułapki.

(To wyjaśnienie ma zastosowanie tylko na platformach, na których unsigned intmożna przedstawić pułapki, co jest rzadkością w rzeczywistych systemach; szczegółowe informacje i odniesienia do alternatywnych i być może bardziej powszechnych przyczyn, które prowadzą do aktualnego brzmienia normy, można znaleźć w komentarzach).

eq-
źródło
3
@VladLazarenko: Tu chodzi o C, a nie o konkretne procesory. Każdy może w trywialny sposób zaprojektować procesor, który ma wzorce bitowe dla liczb całkowitych, które doprowadzają go do szaleństwa. Rozważmy procesor, który ma „szalony bit” w swoich rejestrach.
David Schwartz,
2
Czy mogę więc powiedzieć, że zachowanie jest dobrze zdefiniowane w przypadku liczb całkowitych i x86?
3
Cóż, teoretycznie mógłbyś mieć kompilator, który zdecydował się używać tylko 28-bitowych liczb całkowitych (na x86) i dodać określony kod do obsługi każdego dodawania, mnożenia (i tak dalej) i upewnić się, że te 4 bity pozostaną nieużywane (lub wyemitują SIGSEGV w przeciwnym razie ). Może to spowodować niezinicjalizowana wartość.
eq-
4
Nienawidzę, gdy ktoś obraża wszystkich innych, ponieważ ktoś nie rozumie problemu. To, czy zachowanie jest nieokreślone, zależy wyłącznie od tego, co mówi norma. Aha, i nie ma nic praktycznego w scenariuszu eq ... jest całkowicie wymyślony.
Jim Balter,
7
@Vlad Lazarenko: Procesory Itanium mają flagę NaT (Not a Thing) dla każdego rejestru liczb całkowitych. Flaga NaT jest używana do kontrolowania wykonywania spekulatywnego i może pozostawać w rejestrach, które nie zostały poprawnie zainicjalizowane przed użyciem. Odczyt z takiego rejestru z zestawem bitów NaT daje wyjątek. Zobacz blogs.msdn.com/b/oldnewthing/archive/2004/01/19/60162.aspx
Nordic Mainframe
13

(Ta odpowiedź dotyczy C 1999. Dla C 2011, patrz odpowiedź Jensa Gustedta.)

Standard C nie mówi, że użycie wartości obiektu automatycznego czasu trwania przechowywania, który nie jest zainicjowany, jest niezdefiniowanym zachowaniem. Norma C 1999 mówi, w 6.7.8 10, „Jeśli obiekt, który ma automatyczny czas przechowywania, nie jest jawnie zainicjowany, jego wartość jest nieokreślona”. (W tym akapicie opisano, w jaki sposób inicjowane są obiekty statyczne, więc jedynymi niezainicjowanymi obiektami, o które się martwimy, są obiekty automatyczne).

3.17.2 definiuje „nieokreśloną wartość” jako „nieokreśloną wartość lub reprezentację pułapki”. 3.17.3 definiuje „nieokreśloną wartość” jako „ważną wartość odpowiedniego typu, jeśli niniejsza Norma Międzynarodowa nie nakłada żadnych wymagań dotyczących wyboru wartości w jakimkolwiek przypadku”.

Tak więc, jeśli niezainicjowany unsigned int xma nieokreśloną wartość, to x -= xmusi dać zero. Pozostaje pytanie, czy może to być reprezentacja pułapki. Dostęp do wartości pułapki powoduje niezdefiniowane zachowanie, zgodnie z 6.2.6.1 5.

Niektóre typy obiektów mogą mieć reprezentacje pułapek, takie jak sygnalizacyjne NaN liczb zmiennoprzecinkowych. Ale liczby całkowite bez znaku są wyjątkowe. Zgodnie z 6.2.6.2, każdy z N bitów wartości liczby int bez znaku reprezentuje potęgę 2, a każda kombinacja bitów wartości reprezentuje jedną z wartości od 0 do 2 N -1. Tak więc liczby całkowite bez znaku mogą mieć reprezentacje pułapek tylko z powodu pewnych wartości w ich bitach wypełniających (takich jak bit parzystości).

Jeśli na platformie docelowej bez znaku int nie ma bitów wypełnienia, to niezainicjowany int bez znaku nie może mieć reprezentacji pułapki, a użycie jego wartości nie może spowodować niezdefiniowanego zachowania.

Eric Postpischil
źródło
Jeśli xma reprezentację pułapki, to x -= xmoże pułapka, prawda? Mimo to +1 za wskazanie liczb całkowitych bez znaku bez dodatkowych bitów musi mieć określone zachowanie - jest to wyraźnie przeciwieństwo innych odpowiedzi i (zgodnie z cytatem) wydaje się, że jest to to, co sugeruje standard.
user541686
Tak, jeśli typ xma reprezentację pułapki, x -= xmoże to być pułapka. Nawet zwykłe xużycie jako wartości może spowodować pułapkę. (Bezpieczne jest użycie xjako lwartości; na zapis do obiektu nie wpłynie reprezentacja pułapki, która się w nim znajduje.)
Eric Postpischil
typy bez znaku rzadko mają reprezentację pułapki
Jens Gustedt
Cytując Raymonda Chena : „W ia64 każdy rejestr 64-bitowy ma w rzeczywistości 65 bitów. Dodatkowy bit nazywa się„ NaT ”, co oznacza„ nie jest rzeczą ”. Bit jest ustawiany, gdy rejestr nie zawiera ważnej wartości. Pomyśl o tym jako o całkowitej wersji zmiennoprzecinkowego NaN. ... jeśli masz rejestr, którego wartość to NaT i tak bardzo oddychasz nim w niewłaściwy sposób (na przykład, spróbuj zapisać jego wartość w pamięci), procesor zgłosi wyjątek STATUS_REG_NAT_CONSUMPTION ". To znaczy, kawałek pułapki może być całkowicie poza wartością.
Pozdrawiam i hth. - Alf
−1 Instrukcja "Jeśli na twojej platformie docelowej int bez znaku nie ma bitów wypełniających, to niezainicjowana int bez znaku nie może mieć reprezentacji pułapki, a użycie jej wartości nie może spowodować niezdefiniowanego zachowania." nie bierze pod uwagę schematów, takich jak bity x64 NaT.
Pozdrawiam i hth. - Alf
11

Tak, to nie jest zdefiniowane. Kod może ulec awarii. C mówi, że zachowanie jest nieokreślone, ponieważ nie ma konkretnego powodu, aby robić wyjątek od ogólnej reguły. Zaletą jest ta sama zaleta, co wszystkie inne przypadki niezdefiniowanego zachowania - kompilator nie musi wyprowadzać specjalnego kodu, aby to zadziałało.

Oczywiście kompilator mógłby po prostu użyć dowolnej wartości śmieciowej, którą uznałby za „przydatną” wewnątrz zmiennej i działałby zgodnie z przeznaczeniem… co jest złego w tym podejściu?

Jak myślisz, dlaczego tak się nie dzieje? Dokładnie takie podejście zostało przyjęte. Kompilator nie jest wymagany, aby działał, ale nie jest wymagany do tego, aby się nie udał.

David Schwartz
źródło
1
Jednak kompilator nie musi mieć do tego specjalnego kodu. Po prostu przydzielenie miejsca (jak zawsze) i brak inicjalizacji zmiennej zapewnia jej prawidłowe zachowanie. Nie sądzę, żeby to wymagało specjalnej logiki.
user541686
7
1) Jasne, że mogli. Ale nie przychodzi mi do głowy żaden argument, który by to poprawił. 2) Platforma wie, że nie można polegać na wartości niezainicjowanej pamięci, więc można ją zmienić. Na przykład może wyzerować niezainicjowaną pamięć w tle, aby wyzerowane strony były gotowe do użycia w razie potrzeby. (Zastanów się, czy tak się stanie: 1) Czytamy wartość do odjęcia, powiedzmy, że otrzymujemy 3. 2) Strona zostaje wyzerowana, ponieważ nie jest zainicjowana, zmieniając wartość na 0. 3) Wykonujemy odejmowanie atomowe, przydzielając stronę i tworząc wartość -3. Ups.)
David Schwartz,
2
-1, ponieważ w ogóle nie uzasadniasz swojego roszczenia. Istnieją sytuacje, w których należałoby oczekiwać, że kompilator po prostu przyjmie wartość zapisaną w lokalizacji pamięci.
Jens Gustedt
1
@JensGustedt: Nie rozumiem twojego komentarza. Czy możesz wyjaśnić?
David Schwartz
3
Ponieważ po prostu twierdzisz, że istnieje ogólna zasada, bez odwoływania się do niej. Jako taka jest to tylko próba „udowodnienia przez autorytet”, czego nie oczekuję od SO. I za nieudane argumentowanie, dlaczego nie może to być niespecyficzna wartość. Jedynym powodem, dla którego jest to UB w ogólnym przypadku, jest to, że xmożna go zadeklarować jako register, tj. Jego adres nigdy nie jest brany. Nie wiem, czy byłeś tego świadomy (jeśli skutecznie to ukrywałeś), ale poprawna odpowiedź musi o tym wspominać.
Jens Gustedt
7

W przypadku dowolnej zmiennej dowolnego typu, która nie została zainicjowana lub z innych powodów ma nieokreśloną wartość, do kodu odczytującego tę wartość stosuje się następujące zasady:

  • W przypadku, gdy zmienna ma automatyczny czas trwania i nie ma zajętego adresu, kod zawsze wywołuje niezdefiniowane zachowanie [1].
  • W przeciwnym razie, jeśli system obsługuje reprezentacje pułapek dla danego typu zmiennej, kod zawsze wywoła niezdefiniowane zachowanie [2].
  • W przeciwnym razie, jeśli nie ma reprezentacji pułapek, zmienna przyjmuje nieokreśloną wartość. Nie ma gwarancji, że ta nieokreślona wartość jest spójna przy każdym odczycie zmiennej. Jednak gwarantuje się, że nie będzie reprezentacją pułapki, a zatem gwarantuje się, że nie wywoła niezdefiniowanego zachowania [3].

    Wartość może być następnie bezpiecznie używana bez powodowania awarii programu, chociaż taki kod nie jest przenośny do systemów z reprezentacjami pułapek.


[1]: C11 6.3.2.1:

Jeśli lwartość wyznacza obiekt o automatycznym czasie przechowywania, który mógłby zostać zadeklarowany w klasie pamięci rejestru (nigdy nie miał odebranego adresu), a ten obiekt jest niezainicjalizowany (nie zadeklarowany za pomocą inicjatora i żadne przypisanie do niego nie zostało wykonane przed użyciem ), zachowanie jest nieokreślone.

[2]: C11 6.2.6.1:

Niektóre reprezentacje obiektów nie muszą przedstawiać wartości typu obiektu. Jeśli przechowywana wartość obiektu ma taką reprezentację i jest odczytywana przez wyrażenie l-wartości, które nie ma typu znakowego, zachowanie jest niezdefiniowane. Jeśli taka reprezentacja jest wytwarzana przez efekt uboczny, który modyfikuje całość lub jakąkolwiek część obiektu za pomocą wyrażenia l-wartości, które nie ma typu znakowego, zachowanie jest niezdefiniowane.50) Taka reprezentacja nazywa się reprezentacją pułapki.

[3] C11:

3.19.2
nieokreślona wartość
nieokreślona wartość lub reprezentacja pułapki

3.19.3
wartość nieokreślona
ważna wartość odpowiedniego typu, jeśli niniejsza Norma Międzynarodowa nie nakłada żadnych wymagań, co do której wartość jest wybierana w jakimkolwiek przypadku
UWAGA Nieokreślona wartość nie może być reprezentacją pułapki.

3.19.4
reprezentacja pułapki reprezentacja
obiektu, która nie musi przedstawiać wartości typu obiektu

Lundin
źródło
3
@Vality W prawdziwym świecie 99,9999% wszystkich komputerów to dwa uzupełniające się procesory bez reprezentacji pułapek. Dlatego żadna reprezentacja pułapki nie jest normą, a omówienie zachowania na takich komputerach w świecie rzeczywistym jest bardzo istotne. Założenie, że dziko egzotyczne komputery są normą, nie jest pomocne. Reprezentacje pułapek w świecie rzeczywistym są tak rzadkie, że obecność terminu reprezentacja pułapki w standardzie należy traktować jako standardową wadę odziedziczoną po latach 80. Podobnie jak wsparcie dla komputerów dopełniających, znaków i wielkości.
Lundin
3
Nawiasem mówiąc, jest to doskonały powód stdint.h zawsze powinno być używane zamiast natywnych typów C. Ponieważ stdint.hwymusza dopełnienie 2 i brak bitów dopełniających. Innymi słowy, stdint.htypy nie mogą być pełne bzdur.
Lundin,
2
Ponownie, odpowiedź komitetu na raport o defekcie mówi, że: „Odpowiedź na pytanie 2 jest taka, że ​​każda operacja wykonana na nieokreślonych wartościach będzie miała w rezultacie wartość nieokreśloną”. oraz „Odpowiedzią na pytanie 3 jest to, że funkcje biblioteczne będą wykazywać niezdefiniowane zachowanie, gdy zostaną użyte na nieokreślonych wartościach”.
Antti Haapala
2
DRs 451 i 260
Antti Haapala
1
@AnttiHaapala Tak, wiem o tym DR. To nie zaprzecza tej odpowiedzi. Możesz otrzymać nieokreśloną wartość podczas odczytywania niezainicjowanej lokalizacji pamięci i niekoniecznie jest to ta sama wartość za każdym razem. Ale to jest nieokreślone zachowanie, a nie nieokreślone zachowanie.
Lundin
2

Podczas gdy wiele odpowiedzi koncentruje się na procesorach, które pułapki na dostęp do niezainicjowanych rejestrów, dziwaczne zachowania mogą pojawić się nawet na platformach, które nie mają takich pułapek, przy użyciu kompilatorów, które nie podejmują żadnego szczególnego wysiłku w celu wykorzystania UB. Rozważ kod:

kompilator dla platformy takiej jak ARM, w której wszystkie instrukcje inne niż ładowanie i magazyny działają w 32-bitowych rejestrach, może rozsądnie przetwarzać kod w sposób równoważny z:

Jeśli którykolwiek z nietrwałych odczytów dadzą wartość niezerową, r0 zostanie załadowany wartością z zakresu 0 ... 65535. W przeciwnym razie zwróci wszystko, co trzymał, gdy wywołano funkcję (tj. Wartość przekazaną do x), co może nie być wartością z zakresu 0..65535. W standardzie brakuje terminologii opisującej zachowanie wartości typu uint16_t, ale której wartość jest poza zakresem 0..65535, z wyjątkiem stwierdzenia, że ​​każda akcja, która mogłaby spowodować takie zachowanie, wywołuje UB.

supercat
źródło
Ciekawy. Więc mówisz, że zaakceptowana odpowiedź jest błędna? A może twierdzisz, że w teorii jest to słuszne, ale w praktyce kompilatory mogą robić dziwniejsze rzeczy?
user541686
@Mehrdad: Często implementacje mają zachowanie wykraczające poza granice tego, co byłoby możliwe w przypadku braku UB. Myślę, że byłoby pomocne, gdyby Standard uznawał pojęcie wartości częściowo nieokreślonej, której „przydzielone” bity będą się zachowywać w sposób, w najgorszym przypadku, nieokreślony, ale z dodatkowymi górnymi bitami, które zachowują się niedeterministycznie (np. wynik powyższej funkcji jest przechowywany w zmiennej typu uint16_t, ta zmienna może czasami odczytywać jako 123, a czasami 6553623). Jeśli wynik zostanie zignorowany ...
supercat
... lub użyte w taki sposób, że każdy możliwy sposób jego odczytania dałby ostateczne wyniki spełniające wymagania, istnienie częściowo nieokreślonej wartości nie powinno stanowić problemu. Z drugiej strony, w standardzie nie ma niczego, co pozwoliłoby na istnienie częściowo nieokreślonych wartości w każdych okolicznościach, w których norma nakładałaby jakiekolwiek wymagania dotyczące zachowania.
supercat
Wydaje mi się, że to, co opisujesz, jest dokładnie tym, co znajduje się w zaakceptowanej odpowiedzi - że jeśli zmienna mogłaby zostać zadeklarowana za pomocą register, to może mieć dodatkowe bity, które sprawiają, że zachowanie jest potencjalnie niezdefiniowane. Dokładnie to mówisz, prawda?
user541686
@Mehrdad: Zaakceptowana odpowiedź skupia się na architekturach, których rejestry mają dodatkowy stan „niezainicjowany” i pułapka, jeśli załadowany jest niezainicjowany rejestr. Takie architektury istnieją, ale nie są powszechne. Opisuję scenariusz, w którym zwykły sprzęt może wykazywać zachowanie, które wykracza poza sferę czegokolwiek rozważanego przez standard C, ale byłoby użytecznie ograniczone, gdyby kompilator nie dodał własnego, dodatkowego zwichnięcia do miksu. Na przykład, jeśli funkcja ma parametr, który wybiera operację do wykonania, a niektóre operacje zwracają przydatne dane, a inne nie, ...
supercat