Czy gałęzie z niezdefiniowanym zachowaniem można uznać za nieosiągalne i zoptymalizowane jako martwy kod?


88

Rozważ następujące stwierdzenie:

*((char*)NULL) = 0; //undefined behavior

Wyraźnie wywołuje niezdefiniowane zachowanie. Czy istnienie takiej instrukcji w danym programie oznacza, że ​​cały program jest niezdefiniowany, czy też zachowanie staje się niezdefiniowane dopiero, gdy przepływ sterowania osiągnie tę instrukcję?

Czy następujący program byłby dobrze zdefiniowany, gdyby użytkownik nigdy nie wprowadził numeru 3?

while (true) {
 int num = ReadNumberFromConsole();
 if (num == 3)
  *((char*)NULL) = 0; //undefined behavior
}

A może jest to całkowicie niezdefiniowane zachowanie bez względu na to, co wprowadzi użytkownik?

Czy kompilator może również założyć, że niezdefiniowane zachowanie nigdy nie zostanie wykonane w czasie wykonywania? To pozwoliłoby na cofanie się w czasie:

int num = ReadNumberFromConsole();

if (num == 3) {
 PrintToConsole(num);
 *((char*)NULL) = 0; //undefined behavior
}

W tym przypadku kompilator może stwierdzić, że w przypadku, num == 3gdy zawsze będziemy wywoływać niezdefiniowane zachowanie. Dlatego ten przypadek musi być niemożliwy, a numer nie musi być drukowany. Cała ifinstrukcja mogłaby zostać zoptymalizowana. Czy tego rodzaju rozumowanie wstecz jest dozwolone zgodnie ze standardem?


19
czasami zastanawiam się, czy użytkownicy z wieloma przedstawicielami dostają więcej głosów za pytania, ponieważ „och, mają wielu przedstawicieli, to musi być dobre pytanie”… ale w tym przypadku przeczytałem pytanie i pomyślałem „wow, to jest świetne „zanim nawet spojrzałem na pytającego.
turbulencetoo

4
Myślę, że czas, w którym pojawia się niezdefiniowane zachowanie, jest nieokreślony.
eerorika

6
Standard C ++ wyraźnie mówi, że ścieżka wykonania z niezdefiniowanym zachowaniem w dowolnym momencie jest całkowicie niezdefiniowana. Zinterpretowałbym to nawet jako stwierdzenie, że każdy program z niezdefiniowanym zachowaniem na ścieżce jest całkowicie niezdefiniowany (co obejmuje rozsądne wyniki w innych częściach, ale nie gwarantuje). Kompilatory mogą swobodnie używać niezdefiniowanego zachowania do modyfikowania programu. blog.llvm.org/2011/05/what-every-c-programmer-should-know.html zawiera kilka fajnych przykładów.
Jens

4
@Jens: To naprawdę oznacza tylko ścieżkę wykonawczą. W przeciwnym razie będziesz mieć kłopoty const int i = 0; if (i) 5/i;.
MSalters

1
Kompilator generalnie nie może udowodnić, że PrintToConsolenie wywołuje, std::exitwięc musi wykonać to wywołanie.
MSalters

Odpowiedzi:


65

Czy istnienie takiej instrukcji w danym programie oznacza, że ​​cały program jest niezdefiniowany, czy też zachowanie staje się niezdefiniowane dopiero, gdy przepływ sterowania osiągnie tę instrukcję?

Ani. Pierwszy warunek jest za silny, a drugi za słaby.

Dostęp do obiektów jest czasami sekwencjonowany, ale standard opisuje zachowanie programu poza czasem. Danvil już cytował:

jeżeli jakiekolwiek takie wykonanie zawiera nieokreśloną operację, niniejsza Norma Międzynarodowa nie nakłada żadnych wymagań na implementację wykonującą ten program z tymi danymi wejściowymi (nawet w odniesieniu do operacji poprzedzających pierwszą niezdefiniowaną operację)

Można to zinterpretować:

Jeśli wykonanie programu daje niezdefiniowane zachowanie, to cały program ma niezdefiniowane zachowanie.

Tak więc nieosiągalna instrukcja z UB nie daje programowi UB. Osiągalna instrukcja, która (ze względu na wartości wejść) nigdy nie jest osiągnięta, nie daje programowi UB. Dlatego twój pierwszy stan jest zbyt silny.

Teraz kompilator nie może ogólnie powiedzieć, co ma UB. Tak więc, aby umożliwić optymalizatorowi zmianę kolejności instrukcji z potencjalnym UB, który byłby możliwy do ponownego uporządkowania w przypadku zdefiniowania ich zachowania, konieczne jest zezwolenie UB na „cofnięcie się w czasie” i popełnienie błędu przed poprzednim punktem sekwencji (lub w C ++ 11 terminologia, aby UB wpływał na rzeczy, które są sekwencjonowane przed rzeczą UB). Dlatego twój drugi stan jest zbyt słaby.

Głównym tego przykładem jest sytuacja, w której optymalizator opiera się na ścisłym aliasingu. Cały sens ścisłych reguł aliasingu polega na umożliwieniu kompilatorowi zmiany kolejności operacji, które nie mogłyby zostać poprawnie uporządkowane, gdyby było możliwe, że odnośne wskaźniki aliasują tę samą pamięć. Więc jeśli użyjesz nielegalnych wskaźników aliasingu, a UB wystąpi, może to łatwo wpłynąć na instrukcję „przed” instrukcją UB. Jeśli chodzi o maszynę abstrakcyjną, instrukcja UB nie została jeszcze wykonana. Jeśli chodzi o rzeczywisty kod wynikowy, został on częściowo lub w całości wykonany. Ale norma nie próbuje wchodzić w szczegóły dotyczące tego, co oznacza dla optymalizatora ponowne uporządkowanie instrukcji ani jakie są tego konsekwencje dla UB. Po prostu daje licencję wdrożeniową na błąd, gdy tylko zechce.

Możesz myśleć o tym jako o „UB ma maszynę czasu”.

W szczególności, aby odpowiedzieć na twoje przykłady:

  • Zachowanie jest niezdefiniowane tylko wtedy, gdy odczytuje się 3.
  • Kompilatory mogą i eliminują kod jako martwy, jeśli podstawowy blok zawiera operację, która z pewnością jest niezdefiniowana. Są dozwolone (i przypuszczam, że tak) w przypadkach, które nie są podstawowym blokiem, ale wszystkie gałęzie prowadzą do UB. Ten przykład nie jest kandydatem, chyba że w PrintToConsole(3)jakiś sposób wiadomo, że wróci. Może zgłosić wyjątek lub cokolwiek.

Podobnym przykładem do twojego drugiego jest opcja gcc -fdelete-null-pointer-checks, która może przyjmować taki kod (nie sprawdzałem tego konkretnego przykładu, uważam, że ilustruje on ogólną ideę):

void foo(int *p) {
    if (p) *p = 3;
    std::cout << *p << '\n';
}

i zmień go na:

*p = 3;
std::cout << "3\n";

Czemu? Ponieważ jeśli pjest null, to i tak kod ma UB, więc kompilator może założyć, że nie jest null i odpowiednio zoptymalizować. Jądro Linuksa potknęło się o to ( https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2009-1897 ) głównie dlatego, że działa w trybie, w którym wyłuskiwanie wskaźnika zerowego nie powinno być UB, oczekuje się, że spowoduje zdefiniowany wyjątek sprzętowy, który jądro może obsłużyć. Gdy optymalizacja jest włączona, gcc wymaga użycia -fno-delete-null-pointer-checksw celu zapewnienia ponadstandardowej gwarancji.

PS Praktyczna odpowiedź na pytanie „kiedy pojawia się niezdefiniowane zachowanie?” to „10 minut przed planowanym wyjazdem na cały dzień”.


4
Właściwie w przeszłości było z tego powodu sporo problemów z bezpieczeństwem. W szczególności, jakakolwiek kontrola przepełnienia po fakcie może zostać z tego powodu zoptymalizowana. Na przykład void can_add(int x) { if (x + 100 < x) complain(); }mogą być optymalizowane z dala całkowicie, bo jeśli x+100 robi” przepełnienie nic się nie dzieje, a jeśli x+100 nie przepełnienia, to UB, zgodnie z normą, więc nic nie może się wydarzyć.
fgp

3
@fgp: prawda, to jest optymalizacja, na którą ludzie gorzko narzekają, jeśli się o nią potkną, ponieważ zaczyna mieć wrażenie, że kompilator celowo łamie twój kod, aby cię ukarać. "Dlaczego miałbym to tak napisać, gdybym chciał, żebyś to usunął!" ;-) Ale myślę, że czasami przydaje się optymalizatorowi podczas manipulowania większymi wyrażeniami arytmetycznymi, aby założyć, że nie ma przepełnienia i uniknąć wszystkiego, co drogie, co byłoby potrzebne tylko w takich przypadkach.
Steve Jessop

2
Czy słuszne byłoby stwierdzenie, że program nie jest niezdefiniowany, jeśli użytkownik nigdy nie wprowadzi 3, ale jeśli wprowadzi 3 podczas wykonywania, całe wykonanie stanie się niezdefiniowane? Jak tylko jest 100% pewne, że program wywoła niezdefiniowane zachowanie (i nie wcześniej niż to), zachowanie staje się czymś. Czy te moje stwierdzenia są w 100% poprawne?
usr

3
@usr: Myślę, że to prawda, tak. Biorąc pod uwagę twój konkretny przykład (i przyjmując pewne założenia dotyczące nieuchronności przetwarzanych danych) myślę, że implementacja mogłaby w zasadzie spojrzeć w przyszłość w buforowanym STDIN, 3jeśli chce, i spakować się do domu na cały dzień, gdy tylko go zobaczy przychodzący.
Steve Jessop

3
Dodatkowe +1 (jeśli mógłbym) dla twojego PS
Fred Larson

10

Norma wskazuje na 1,9 / 4

[Uwaga: Niniejsza Norma Międzynarodowa nie nakłada żadnych wymagań dotyczących zachowania programów, które zawierają nieokreślone zachowanie. - notatka końcowa]

Ciekawostką jest prawdopodobnie to, co oznacza „zawierać”. Nieco później przy 1,9 / 5 mówi:

Jednakże, jeśli jakiekolwiek takie wykonanie zawiera nieokreśloną operację, niniejsza Norma Międzynarodowa nie nakłada żadnych wymagań na implementację wykonującą ten program z tym wejściem (nawet w odniesieniu do operacji poprzedzających pierwszą niezdefiniowaną operację)

Tutaj konkretnie wspomina się o „wykonaniu… z tym wejściem”. Zinterpretowałbym to jako niezdefiniowane zachowanie w jednej możliwej gałęzi, która nie jest teraz wykonywana, nie wpływa na bieżącą gałąź wykonania.

Inną kwestią są jednak założenia oparte na niezdefiniowanym zachowaniu podczas generowania kodu. Zobacz odpowiedź Steve'a Jessopa, aby uzyskać więcej informacji na ten temat.


1
Jeśli potraktować dosłownie, jest to wyrok śmierci dla wszystkich istniejących programów.
usr

6
Nie sądzę, aby pytanie brzmiało, czy UB może pojawić się przed faktycznym osiągnięciem kodu. Pytanie, jak to zrozumiałem, brzmiało, czy UB może się pojawić, jeśli kod nie zostałby nawet osiągnięty. I oczywiście odpowiedź na to pytanie brzmi „nie”.
sepp2k

Cóż, standard nie jest taki jasny w 1.9 / 4, ale 1.9 / 5 można prawdopodobnie zinterpretować tak, jak powiedziałeś.
Danvil

1
Notatki są nienormatywne. 1,9 / 5 przebija nutę w 1,9 / 4
MSalters

5

Pouczającym przykładem jest

int foo(int x)
{
    int a;
    if (x)
        return a;
    return 0;
}

Zarówno obecne GCC, jak i bieżące Clang zoptymalizują to (na x86) do

xorl %eax,%eax
ret

ponieważ wywnioskowali, że xjest to zawsze zero z UB w if (x)ścieżce sterowania. GCC nawet nie daje ostrzeżenia o użyciu niezainicjowanej wartości! (ponieważ przebieg, który stosuje powyższą logikę, jest uruchamiany przed przebiegiem, który generuje ostrzeżenia o niezainicjowanej wartości)


1
Ciekawy przykład. To raczej paskudne, że włączenie optymalizacji powoduje ukrycie ostrzeżenia. Nie jest to nawet udokumentowane - dokumentacja GCC mówi tylko, że włączenie optymalizacji powoduje więcej ostrzeżeń.
sleske

@sleske To paskudne, zgadzam się, ale niezainicjowane ostrzeżenia o wartości są notorycznie trudne do „poprawienia” - perfekcyjne ich wykonanie jest równoznaczne z problemem zatrzymania, a programiści są dziwnie irracjonalni, jeśli chodzi o dodawanie „niepotrzebnych” inicjalizacji zmiennych w celu wyciszenia fałszywych alarmów, więc autorzy kompilatorów kończą na beczce. Kiedyś włamałem się do GCC i przypominam sobie, że wszyscy bali się zepsuć niezainicjalizowaną przepustkę ostrzegającą o wartości.
zwol

@zwol: Zastanawiam się, w jakim stopniu „optymalizacja” wynikająca z takiej eliminacji martwego kodu faktycznie zmniejsza użyteczny kod, a jak bardzo powoduje, że programiści powiększają kod (np. dodając kod do zainicjowania, anawet jeśli we wszystkich okolicznościach niezainicjowany azostałby przekazany do funkcji, której funkcja nigdy nie zrobiłaby z nią nic)?
supercat

@supercat Nie byłem głęboko zaangażowany w prace kompilatora od około 10 lat i prawie niemożliwe jest uzasadnienie optymalizacji na podstawie przykładów zabawek. Ten rodzaj optymalizacji zwykle wiąże się z 2-5% całkowitym zmniejszeniem rozmiaru kodu w rzeczywistych aplikacjach, jeśli dobrze pamiętam.
zwol

1
@supercat 2-5% jest ogromne, jak to się dzieje. Widziałem ludzi pocących się o 0,1%.
zwol

4

Obecna robocza wersja robocza C ++ mówi, że w 1.9.4

Niniejsza Norma Międzynarodowa nie nakłada żadnych wymagań na zachowanie programów, które wykazują nieokreślone zachowanie.

Na tej podstawie powiedziałbym, że program zawierający niezdefiniowane zachowanie na dowolnej ścieżce wykonywania może zrobić wszystko w każdym momencie swojego wykonania.

Istnieją dwa naprawdę dobre artykuły na temat niezdefiniowanego zachowania i tego, co zwykle robią kompilatory:


1
To nie ma sensu. Funkcja z int f(int x) { if (x > 0) return 100/x; else return 100; }pewnością nigdy nie wywołuje niezdefiniowanego zachowania, mimo że 100/0jest oczywiście niezdefiniowane.
fgp

1
@fgp Standard (zwłaszcza 1.9 / 5) mówi jednak, że jeśli można osiągnąć niezdefiniowane zachowanie , nie ma znaczenia, kiedy zostanie osiągnięte. Na przykład printf("Hello, World"); *((char*)NULL) = 0 nie ma gwarancji, że cokolwiek wydrukujesz. Pomaga to w optymalizacji, ponieważ kompilator może dowolnie zmieniać kolejność operacji (oczywiście z zastrzeżeniem ograniczeń zależności), o których wie, że w końcu wystąpią, bez konieczności uwzględniania niezdefiniowanego zachowania.
fgp

Powiedziałbym, że program z twoją funkcją nie zawiera niezdefiniowanego zachowania, ponieważ nie ma danych wejściowych, na których zostanie ocenione 100/0.
Jens

1
Dokładnie - ważne jest więc, czy UB faktycznie może zostać wyzwolony, czy nie, a nie, czy teoretycznie można go uruchomić. A może jesteś gotowy argumentować, że int x,y; std::cin >> x >> y; std::cout << (x+y);można powiedzieć, że „1 + 1 = 17”, tylko dlatego, że istnieją pewne dane wejściowe, w których x+yprzepełnienia (czyli UB, ponieważ intjest to typ ze znakiem).
fgp

Formalnie powiedziałbym, że program ma nieokreślone zachowanie, ponieważ istnieją dane wejściowe, które go wyzwalają. Ale masz rację, że nie ma to sensu w kontekście C ++, ponieważ byłoby niemożliwe napisanie programu bez niezdefiniowanego zachowania. Chciałbym, żeby było mniej nieokreślonych zachowań w C ++, ale nie tak działa język (i jest kilka dobrych powodów, ale nie dotyczą one mojego codziennego użytku ...).
Jens

3

Słowo „zachowanie” oznacza, że ​​coś się dzieje . Stan, który nigdy nie jest wykonywany, nie jest „zachowaniem”.

Ilustracja:

*ptr = 0;

Czy to niezdefiniowane zachowanie? Załóżmy, że jesteśmy na 100% pewni ptr == nullptrprzynajmniej raz podczas wykonywania programu. Odpowiedź powinna brzmieć tak.

A co z tym?

 if (ptr) *ptr = 0;

Czy to jest nieokreślone? (Pamiętasz ptr == nullptrprzynajmniej raz?) Mam nadzieję, że nie, bo inaczej nie będziesz w stanie napisać żadnego użytecznego programu.

Udzielając tej odpowiedzi, żaden srandardese nie ucierpiał.


3

Niezdefiniowane zachowanie pojawia się, gdy program spowoduje niezdefiniowane zachowanie bez względu na to, co stanie się później. Jednak podałeś następujący przykład.

int num = ReadNumberFromConsole();

if (num == 3) {
 PrintToConsole(num);
 *((char*)NULL) = 0; //undefined behavior
}

Dopóki kompilator nie zna definicji PrintToConsole, nie może usunąć if (num == 3)warunku. Załóżmy, że masz LongAndCamelCaseStdio.hnagłówek systemowy z następującą deklaracją PrintToConsole.

void PrintToConsole(int);

Nic zbyt pomocnego, w porządku. Teraz zobaczmy, jak zły (lub może nie tak zły, niezdefiniowany sposób mógł być gorszy) sprzedawca, sprawdzając rzeczywistą definicję tej funkcji.

int printf(const char *, ...);
void exit(int);

void PrintToConsole(int num) {
    printf("%d\n", num);
    exit(0);
}

W rzeczywistości kompilator musi założyć, że dowolna funkcja, której kompilator nie wie, co robi, może zakończyć działanie lub zgłosić wyjątek (w przypadku C ++). Możesz zauważyć, że *((char*)NULL) = 0;nie zostanie to wykonane, ponieważ wykonanie nie będzie kontynuowane po PrintToConsolewywołaniu.

Nieokreślone zachowanie uderza, gdy PrintToConsolefaktycznie powraca. Kompilator spodziewa się, że tak się nie stanie (ponieważ spowodowałoby to wykonanie przez program niezdefiniowanego zachowania bez względu na wszystko), dlatego wszystko może się zdarzyć.

Zastanówmy się jednak nad czymś innym. Powiedzmy, że robimy sprawdzanie wartości null i używamy zmiennej po sprawdzeniu wartości null.

int putchar(int);

const char *warning;

void lol_null_check(const char *pointer) {
    if (!pointer) {
        warning = "pointer is null";
    }
    putchar(*pointer);
}

W tym przypadku łatwo zauważyć, że lol_null_checkwymaga to wskaźnika innego niż NULL. Przypisanie do globalnej warningzmiennej nieulotnej nie jest czymś, co mogłoby zakończyć działanie programu lub zgłosić wyjątek. pointerJest nieulotna, więc nie może magicznie zmienić jego wartość w środku funkcji (jeśli tak, to niezdefiniowane zachowanie). Wywołanie lol_null_check(NULL)spowoduje niezdefiniowane zachowanie, które może spowodować nieprzypisanie zmiennej (ponieważ w tym momencie znany jest fakt, że program wykonuje niezdefiniowane zachowanie).

Jednak niezdefiniowane zachowanie oznacza, że ​​program może zrobić wszystko. Dlatego nic nie powstrzymuje niezdefiniowanego zachowania przed cofnięciem się w czasie i awarią programu przed wykonaniem pierwszej linii int main(). To niezdefiniowane zachowanie, nie musi mieć sensu. Równie dobrze może się zawiesić po wpisaniu 3, ale niezdefiniowane zachowanie cofnie się w czasie i ulegnie awarii, zanim wpiszesz 3. A kto wie, być może niezdefiniowane zachowanie nadpisze pamięć RAM systemu i spowoduje awarię systemu 2 tygodnie później, gdy niezdefiniowany program nie jest uruchomiony.


Wszystkie ważne punkty. PrintToConsoleto moja próba wstawienia zewnętrznego efektu ubocznego programu, który jest widoczny nawet po awarii i jest silnie uporządkowany. Chciałem stworzyć sytuację, w której możemy z całą pewnością stwierdzić, czy ta instrukcja została zoptymalizowana. Ale masz rację, że może nigdy nie powrócić; Twój przykład pisania do globalnego może podlegać innym optymalizacjom, które nie są związane z UB. Na przykład nieużywany globalny może zostać usunięty. Masz pomysł na stworzenie zewnętrznego efektu ubocznego w sposób gwarantujący przywrócenie kontroli?
usr

Czy jakiekolwiek efekty uboczne obserwowalne w świecie zewnętrznym mogą zostać wygenerowane przez kod, który kompilator mógłby założyć, że zwraca? W moim rozumieniu, nawet metoda, która po prostu odczytuje volatilezmienną, mogłaby legalnie wyzwolić operację we / wy, która z kolei mogłaby natychmiast przerwać bieżący wątek; program obsługi przerwań mógłby wtedy zabić wątek, zanim będzie miał szansę wykonać cokolwiek innego. Nie widzę uzasadnienia, dzięki któremu kompilator mógłby wypchnąć niezdefiniowane zachowanie przed tym punktem.
supercat

Z punktu widzenia standardu C nie byłoby nic nielegalnego w tym, że niezdefiniowane zachowanie powoduje, że komputer wysyła wiadomość do niektórych osób, które wyśledzą i zniszczą wszystkie dowody wcześniejszych działań programu, ale jeśli akcja mogłaby zakończyć wątek, to wszystko, co jest sekwencjonowane przed tym działaniem, musiałoby się wydarzyć przed jakimkolwiek Nieokreślonym Zachowaniem, które nastąpiło po nim.
supercat

1

Jeśli program dotrze do instrukcji wywołującej niezdefiniowane zachowanie, żadne wymagania nie są nakładane na żadne wyjście / zachowanie programu; nie ma znaczenia, czy miałyby miejsce „przed” czy „po” wywołaniu niezdefiniowanego zachowania.

Twoje rozumowanie dotyczące wszystkich trzech fragmentów kodu jest poprawne. W szczególności kompilator może potraktować każdą instrukcję, która bezwarunkowo wywołuje niezdefiniowane zachowanie, tak jak traktuje GCC __builtin_unreachable(): jako wskazówkę optymalizacyjną, że instrukcja jest nieosiągalna (a tym samym, że wszystkie ścieżki kodu prowadzące do niej bezwarunkowo są również nieosiągalne). Możliwe są oczywiście inne podobne optymalizacje.


1
Z ciekawości, kiedy __builtin_unreachable()zaczęły pojawiać się efekty, które postępowały wstecz i do przodu w czasie? Biorąc pod uwagę coś takiego extern volatile uint32_t RESET_TRIGGER; void RESET(void) { RESET_TRIGGER = 0xAA55; __memorybarrier(); __builtin_unreachable(); }, jak mogę uznać, builtin_unreachable()że dobrze jest poinformować kompilator, że może pominąć returninstrukcję, ale byłoby to raczej inne niż stwierdzenie, że poprzedni kod można pominąć.
supercat

@supercat, ponieważ RESET_TRIGGER jest niestabilny, zapis do tej lokalizacji może mieć dowolne skutki uboczne. Dla kompilatora jest to jak nieprzezroczyste wywołanie metody. Dlatego nie można udowodnić (i nie jest to przypadek), że __builtin_unreachablezostało osiągnięte. Ten program jest zdefiniowany.
usr

@usr: Myślę, że kompilatory niskiego poziomu powinny traktować dostęp ulotny jako nieprzejrzyste wywołania metod, ale ani clang, ani gcc tego nie robią. Między innymi nieprzezroczyste wywołanie metody może spowodować, że wszystkie bajty dowolnego obiektu, którego adres został ujawniony światu zewnętrznemu i do którego nie był ani nie będzie dostępny restrictwskaźnik na żywo , zostaną zapisane przy użyciu rozszerzenia unsigned char*.
supercat

@usr: Jeśli kompilator nie traktuje dostępu ulotnego jako nieprzezroczystego wywołania metody w odniesieniu do dostępu do ujawnionych obiektów, nie widzę żadnego szczególnego powodu, aby oczekiwać, że zrobi to do innych celów. Standard nie wymaga tego od implementacji, ponieważ istnieją platformy sprzętowe, na których kompilator może znać wszystkie możliwe efekty wynikające z dostępu ulotnego. Kompilator nadający się do użytku osadzonego powinien jednak rozpoznać, że dostępy ulotne mogą wyzwalać sprzęt, który nie został wynaleziony podczas pisania kompilatora.
supercat

@supercat Myślę, że masz rację. Wydaje się, że niestabilne operacje „nie mają wpływu na abstrakcyjną maszynę” i dlatego nie mogą zakończyć programu ani spowodować skutków ubocznych.
usr

1

Wiele standardów dla wielu rodzajów rzeczy wymaga wiele wysiłku na opisanie rzeczy, których implementacje POWINNY lub NIE POWINNY robić, używając nazewnictwa podobnego do zdefiniowanego w IETF RFC 2119 (choć niekoniecznie cytując definicje w tym dokumencie). W wielu przypadkach opisy rzeczy, które implementacje powinny robić, z wyjątkiem przypadków, w których byłyby bezużyteczne lub niepraktyczne, są ważniejsze niż wymagania, które muszą spełniać wszystkie zgodne implementacje.

Niestety, standardy C i C ++ mają tendencję do unikania opisów rzeczy, które chociaż nie są wymagane w 100%, to jednak nie należy się ich spodziewać po implementacjach wysokiej jakości, które nie dokumentują sprzecznych zachowań. Sugestia, że ​​implementacje powinny coś zrobić, może być postrzegana jako sugerująca, że ​​te, które nie są gorsze, aw przypadkach, w których ogólnie byłoby oczywiste, które zachowania byłyby przydatne lub praktyczne, w porównaniu z niepraktycznymi i bezużytecznymi, w danej implementacji Niewielka dostrzegana potrzeba, aby Norma ingerowała w takie osądy.

Sprytny kompilator mógłby być zgodny ze standardem, eliminując jednocześnie kod, który nie miałby żadnego efektu, z wyjątkiem sytuacji, gdy kod otrzymuje dane wejściowe, które nieuchronnie spowodowałyby niezdefiniowane zachowanie, ale „sprytny” i „głupi” nie są antonimami. Fakt, że autorzy Standardu uznali, że mogą istnieć rodzaje implementacji, w których użyteczne zachowanie w danej sytuacji byłoby bezużyteczne i niepraktyczne, nie oznacza żadnego osądu, czy takie zachowania należy uznać za praktyczne i przydatne dla innych. Gdyby implementacja mogła utrzymać gwarancję behawioralną bez żadnych kosztów poza utratą możliwości przycinania „martwej gałęzi”, prawie każda wartość, jaką kod użytkownika mógłby uzyskać z tej gwarancji, przekroczyłaby koszt jej dostarczenia. Eliminacja martwych gałęzi może być w porządku w przypadkach, w których nieale jeśli w danej sytuacji kod użytkownika mógłby obsłużyć prawie każde możliwe zachowanie inne niż eliminacja martwej gałęzi, każdy wysiłek, jaki kod użytkownika musiałby poświęcić, aby uniknąć UB, prawdopodobnie przekroczyłby wartość uzyskaną z DBE.


To dobra uwaga, że ​​unikanie UB może nałożyć koszt na kod użytkownika.
usr

@usr: Jest to punkt, którego moderniści całkowicie przegapiają. Czy powinienem dodać przykład? Np. Jeśli kod musi oceniać, x*y < zkiedy x*ysię nie przepełnia, aw przypadku przepełnienia daje 0 lub 1 w dowolny sposób, ale bez skutków ubocznych, na większości platform nie ma powodu, dla którego spełnienie drugiego i trzeciego wymagania powinno być droższe niż spełnienie pierwszego, ale jakikolwiek sposób zapisania wyrażenia w celu zagwarantowania zachowania zdefiniowanego przez standard we wszystkich przypadkach może w niektórych przypadkach spowodować znaczne koszty. Pisanie wyrażenia, które (int64_t)x*y < zmoże ponad czterokrotnie zwiększyć koszt obliczeń ...
superkat

... na niektórych platformach i zapisanie go w (int)((unsigned)x*y) < ztaki sposób, aby zapobiec zastosowaniu przez kompilator czegoś, co mogłoby być użytecznymi podstawieniami algebraicznymi (np. jeśli wie o tym xi zsą równe i dodatnie, może uprościć oryginalne wyrażenie y<0, ale wersja używająca zmusi kompilator do wykonania mnożenia). Jeśli kompilator może zagwarantować, nawet jeśli norma tego nie nakazuje, to spełni wymóg „wydajność 0 lub 1 bez skutków ubocznych”, kod użytkownika może dać kompilatorowi możliwości optymalizacji, których w innym przypadku nie mógłby uzyskać.
supercat

Tak, wydaje się, że pomocna byłaby tu łagodniejsza forma niezdefiniowanego zachowania. Programista może włączyć tryb, który powoduje x*yemisję normalnej wartości w przypadku przepełnienia, ale w ogóle jakiejkolwiek wartości. Konfigurowalny UB w C / C ++ wydaje mi się ważny.
usr

@usr: Gdyby autorzy standardu C89 byli prawdomówni, mówiąc, że promowanie krótkich niepodpisanych wartości w celu zalogowania się było najpoważniejszą zmianą przełomową i nie byli ignorantami, oznaczałoby to, że spodziewali się tego w przypadkach, gdy platformy definiowali użyteczne gwarancje behawioralne, implementacje dla takich platform udostępniały te gwarancje programistom, a programiści je wykorzystywali, kompilatory dla takich platform będą nadal oferować takie gwarancje behawioralne, niezależnie od tego, czy Standard nakazał im, czy nie .
supercat
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.