Jakie są wszystkie typowe niezdefiniowane zachowania, o których powinien wiedzieć programista C ++? [Zamknięte]

201

Jakie są wszystkie typowe niezdefiniowane zachowania, o których powinien wiedzieć programista C ++?

Powiedz:

a[i] = i++;

tak
źródło
3
Jesteś pewny. To wygląda dobrze zdefiniowane.
Martin York,
17
6.2.2. Zamówienie ewaluacyjne [expr. Ocena] w języku programowania C ++ tak. Nie mam żadnych innych odniesień
yesraaj
4
Ma rację .. właśnie spojrzał na 6.2.2 w języku programowania C ++ i mówi, że v [i] = i ++ jest niezdefiniowany
dancavallaro,
4
Wyobrażam sobie, ponieważ program kompilujący wykonuje i ++ przed lub po obliczeniu lokalizacji pamięci v [i]. jasne, zawsze będę tam przydzielony. ale może pisać do v [i] lub v [i + 1] w zależności od kolejności operacji.
Evan Teran
2
Wszystko, co mówi język programowania C ++ to: „Kolejność operacji podwyrażeń w wyrażeniu jest niezdefiniowana. W szczególności nie można założyć, że wyrażenie jest oceniane od lewej do prawej”.
dancavallaro,

Odpowiedzi:

233

Wskaźnik

  • Dereferencje NULLwskaźnika
  • Dereferencje wskaźnika zwróconego przez „nowy” przydział wielkości zero
  • Używanie wskaźników do obiektów, których okres użytkowania dobiegł końca (na przykład układanie obiektów przydzielonych lub usuwanie obiektów)
  • Dereferencje wskaźnika, który nie został jeszcze zdecydowanie zainicjowany
  • Wykonywanie arytmetyki wskaźnika, która daje wynik poza granicami (powyżej lub poniżej) tablicy.
  • Dereferencje wskaźnika w miejscu poza końcem tablicy.
  • Konwertowanie wskaźników na obiekty niezgodnych typów
  • Używanie memcpydo kopiowania nakładających się buforów .

Przepełnienie bufora

  • Odczyt lub zapis do obiektu lub tablicy z przesunięciem ujemnym lub większym niż rozmiar tego obiektu (przepełnienie stosu / sterty)

Przepełnienia liczb całkowitych

  • Przepełnienie ze znakiem całkowitym
  • Ocena wyrażenia, które nie jest zdefiniowane matematycznie
  • Wartości przesunięcia w lewo o wartość ujemną (przesunięcia w prawo o wartości ujemne są zdefiniowane w realizacji)
  • Przesunięcie wartości o wartość większą lub równą liczbie bitów w liczbie (np. int64_t i = 1; i <<= 72Jest niezdefiniowana)

Rodzaje, obsada i Const

  • Rzucanie wartości liczbowej na wartość, która nie może być reprezentowana przez typ docelowy (bezpośrednio lub przez static_cast)
  • Korzystanie ze zmiennej automatycznej przed jej definitywnym przypisaniem (np. int i; i++; cout << i;)
  • Wykorzystanie wartości dowolnego obiektu typu innego niż volatilelub sig_atomic_tprzy odbiorze sygnału
  • Próba modyfikacji literału łańcuchowego lub dowolnego innego obiektu const w trakcie jego życia
  • Łączenie wąskiego z szerokim dosłownym ciągiem podczas wstępnego przetwarzania

Funkcja i szablon

  • Nie zwraca wartości z funkcji zwracającej wartość (bezpośrednio lub przez odpływ z bloku try)
  • Wiele różnych definicji tego samego obiektu (klasa, szablon, wyliczenie, funkcja wstawiana, funkcja członka statycznego itp.)
  • Nieskończona rekurencja w tworzeniu szablonów
  • Wywołanie funkcji przy użyciu różnych parametrów lub powiązanie z parametrami i powiązaniem, które funkcja jest zdefiniowana jako używanie.

OOP

  • Kaskadowe niszczenie obiektów o statycznym czasie przechowywania
  • Wynik przypisania do częściowo nakładających się obiektów
  • Rekurencyjne ponowne wprowadzanie funkcji podczas inicjalizacji jej obiektów statycznych
  • Wywoływanie funkcji wirtualnych do czysto wirtualnych funkcji obiektu z jego konstruktora lub destruktora
  • Odnosząc się do niestatycznych elementów obiektów, które nie zostały zbudowane lub zostały już zniszczone

Plik źródłowy i wstępne przetwarzanie

  • Niepusty plik źródłowy, który nie kończy się znakiem nowej linii lub kończy się odwrotnym ukośnikiem (wcześniej niż C ++ 11)
  • Ukośnik odwrotny, po którym następuje znak, który nie jest częścią określonych kodów specjalnych w stałej znaku lub łańcucha (jest to zdefiniowane w C ++ 11).
  • Przekroczenie limitów implementacji (liczba zagnieżdżonych bloków, liczba funkcji w programie, dostępne miejsce na stosie ...)
  • Wartości numeryczne preprocesora, które nie mogą być reprezentowane przez long int
  • Dyrektywa w sprawie przetwarzania wstępnego po lewej stronie definicji makra podobnej do funkcji
  • Dynamiczne generowanie zdefiniowanego tokena w #ifwyrażeniu

Do sklasyfikowania

  • Wywołanie wyjścia podczas niszczenia programu o statycznym czasie przechowywania
Diomidis Spinellis
źródło
Hm ... NaN (x / 0) i Infinity (0/0) zostały objęte IEE 754, jeśli C ++ został zaprojektowany później, dlaczego zapisuje x / 0 jako niezdefiniowany?
new123456
Re: „Ukośnik odwrotny, po którym następuje znak, który nie jest częścią określonych kodów specjalnych w stałej znaku lub ciągu”. To jest UB w C89 (§3.1.3.4) i C ++ 03 (który zawiera C89), ale nie w C99. C99 mówi, że „wynik nie jest tokenem i wymagana jest diagnostyka” (§6.4.4.4). Prawdopodobnie C ++ 0x (który zawiera C89) będzie taki sam.
Adam Rosenfield,
1
Standard C99 zawiera listę niezdefiniowanych zachowań w dodatku J.2. Dostosowanie tej listy do C ++ wymagałoby trochę pracy. Trzeba będzie zmienić odniesienia do poprawnych klauzul C ++ zamiast klauzul C99, usunąć wszystko, co nieistotne, a także sprawdzić, czy wszystkie te rzeczy są naprawdę niezdefiniowane zarówno w C ++, jak i C. Ale to daje początek.
Steve Jessop,
1
@ new123456 - nie wszystkie jednostki zmiennoprzecinkowe są kompatybilne z IEE754. Gdyby C ++ wymagało zgodności z IEE754, kompilatory musiałyby przetestować i obsłużyć przypadek, w którym RHS wynosi zero, poprzez jawne sprawdzenie. Poprzez niezdefiniowanie tego zachowania kompilator może uniknąć tego narzutu, mówiąc „jeśli użyjesz FPU innego niż IEE754, nie uzyskasz zachowania IEEE754 FPU”.
SecurityMatt
1
„Ocena wyrażenia, którego wynik nie mieści się w zakresie odpowiednich typów” .... Przepełnienie liczb całkowitych jest dobrze zdefiniowane dla NIEZALEŻNYCH typów całkowych, tylko tych niepodpisanych.
nacitar sevaht
31

Kolejność oceny parametrów funkcji jest nieokreślonym zachowaniem . (Nie spowoduje to awarii programu, wybuchu ani zamawiania pizzy ... w przeciwieństwie do niezdefiniowanego zachowania ).

Jedynym wymaganiem jest to, że wszystkie parametry muszą zostać w pełni ocenione przed wywołaniem funkcji.


To:

// The simple obvious one.
callFunc(getA(),getB());

Może być równoważne z tym:

int a = getA();
int b = getB();
callFunc(a,b);

Albo to:

int b = getB();
int a = getA();
callFunc(a,b);

Może być albo; to zależy od kompilatora. Wynik może mieć znaczenie, w zależności od skutków ubocznych.

Martin York
źródło
23
Zamówienie jest nieokreślone, nie jest niezdefiniowane.
Rob Kennedy,
1
Nienawidzę tego :) Straciłem cały dzień pracy, kiedy wyśledziłem jeden z tych przypadków ... w każdym razie nauczyłem się mojej lekcji i na szczęście nie upadłem ponownie
Robert Gould
2
@Rob: Kłóciłbym się z tobą o zmianę znaczenia tutaj, ale wiem, że komitet normalizacyjny jest bardzo wybredny co do dokładnej definicji tych dwóch słów. Więc po prostu to zmienię :-)
Martin York,
2
Mam szczęście w tej sprawie. Ukąsiłem go, gdy byłem na studiach i miałem profesora, który rzucił na to okiem i powiedział mi o moim problemie za około 5 sekund. Nie mówię, ile czasu zmarnowałbym na debugowanie w przeciwnym razie.
Bill the Lizard,
27

Kompilator może dowolnie zamawiać części wyrażenia ewaluacyjnego (zakładając, że znaczenie nie ulegnie zmianie).

Z pierwotnego pytania:

a[i] = i++;

// This expression has three parts:
(a) a[i]
(b) i++
(c) Assign (b) to (a)

// (c) is guaranteed to happen after (a) and (b)
// But (a) and (b) can be done in either order.
// See n2521 Section 5.17
// (b) increments i but returns the original value.
// See n2521 Section 5.2.6
// Thus this expression can be written as:

int rhs  = i++;
int lhs& = a[i];
lhs = rhs;

// or
int lhs& = a[i];
int rhs  = i++;
lhs = rhs;

Blokada podwójnie sprawdzona. I jeden łatwy do popełnienia błąd.

A* a = new A("plop");

// Looks simple enough.
// But this can be split into three parts.
(a) allocate Memory
(b) Call constructor
(c) Assign value to 'a'

// No problem here:
// The compiler is allowed to do this:
(a) allocate Memory
(c) Assign value to 'a'
(b) Call constructor.
// This is because the whole thing is between two sequence points.

// So what is the big deal.
// Simple Double checked lock. (I know there are many other problems with this).
if (a == null) // (Point B)
{
    Lock   lock(mutex);
    if (a == null)
    {
        a = new A("Plop");  // (Point A).
    }
}
a->doStuff();

// Think of this situation.
// Thread 1: Reaches point A. Executes (a)(c)
// Thread 1: Is about to do (b) and gets unscheduled.
// Thread 2: Reaches point B. It can now skip the if block
//           Remember (c) has been done thus 'a' is not NULL.
//           But the memory has not been initialized.
//           Thread 2 now executes doStuff() on an uninitialized variable.

// The solution to this problem is to move the assignment of 'a'
// To the other side of the sequence point.
if (a == null) // (Point B)
{
    Lock   lock(mutex);
    if (a == null)
    {
        A* tmp = new A("Plop");  // (Point A).
        a = tmp;
    }
}
a->doStuff();

// Of course there are still other problems because of C++ support for
// threads. But hopefully these are addresses in the next standard.
Martin York
źródło
co oznacza punkt sekwencji?
yesraaj,
1
Ooh ... to paskudne, szczególnie odkąd widziałem tę dokładną strukturę zalecaną w Javie
Tom
Zauważ, że niektóre kompilatory definiują zachowanie w tej sytuacji. Na przykład w VC ++ 2005+, jeśli a jest niestabilny, potrzebne są bariery pamięci, aby zapobiec zmianie kolejności instrukcji, dzięki czemu działa podwójne sprawdzanie blokowania.
Eclipse
Martin York: <i> // (c) ma miejsce po (a) i (b) </i> Czy tak jest? Wprawdzie w tym konkretnym przykładzie jedynym scenariuszem, w którym mogłoby mieć znaczenie, byłoby, gdyby „i” było zmienną zmienną zamapowaną na rejestr sprzętowy, a [i] (stara wartość „i”) została do niej przypisana, ale czy istnieje jakikolwiek zagwarantować, że przyrost nastąpi przed punktem sekwencyjnym?
supercat
5

Moim ulubionym jest „nieskończona rekurencja w tworzeniu instancji szablonów”, ponieważ uważam, że jest to jedyny przypadek, w którym niezdefiniowane zachowanie występuje w czasie kompilacji.

Daniel Earwicker
źródło
Zrobiłem to wcześniej, ale nie rozumiem, jak to jest niezdefiniowane. To całkiem oczywiste, że dokonujesz nieskończonej rekurencji po namyśle.
Robert Gould
Problem polega na tym, że kompilator nie może zbadać twojego kodu i zdecydować, czy będzie cierpieć z powodu nieskończonej rekurencji, czy nie. Jest to przykład problemu z zatrzymaniem. Zobacz: stackoverflow.com/questions/235984/...
Daniel Earwicker
Tak, to zdecydowanie problem z zatrzymaniem
Robert Gould,
spowodował awarię mojego systemu z powodu wymiany spowodowanej zbyt małą pamięcią.
Johannes Schaub - litb
2
Stałe preprocesora, które nie pasują do int, to także czas kompilacji.
Joshua,
5

Przypisywanie do stałej po constrozebraniu za pomocą const_cast<>:

const int i = 10; 
int *p =  const_cast<int*>( &i );
*p = 1234; //Undefined
tak
źródło
5

Oprócz nieokreślonego zachowania istnieje również równie nieprzyjemne zachowanie zdefiniowane w implementacji .

Niezdefiniowane zachowanie występuje, gdy program robi coś, czego wynik nie jest określony przez standard.

Zachowanie zdefiniowane w implementacji jest działaniem programu, którego wynik nie jest zdefiniowany w standardzie, ale wdrożenie musi udokumentować. Przykładem są „Wielobajtowe literały znaków” z pytania Przepełnienie stosu Czy istnieje kompilator C, który tego nie skompilował? .

Zachowanie zdefiniowane w implementacji gryzie cię dopiero po rozpoczęciu przenoszenia (ale uaktualnianie do nowej wersji kompilatora również się przenosi!)

Constantin
źródło
4

Zmienne mogą być aktualizowane tylko raz w wyrażeniu (technicznie raz między punktami sekwencji).

int i =1;
i = ++i;

// Undefined. Assignment to 'i' twice in the same expression.
Martin York
źródło
Dotrzyj przynajmniej raz między dwoma punktami sekwencji.
Prasoon Saurav
2
@Prasoon: Myślę, że miałeś na myśli: najwyżej raz między dwoma punktami sekwencji. :-)
Nawaz
3

Podstawowe zrozumienie różnych ograniczeń środowiskowych. Pełna lista znajduje się w sekcji 5.2.4.1 specyfikacji C. Tu jest kilka;

  • 127 parametrów w definicji jednej funkcji
  • 127 argumentów w jednym wywołaniu funkcji
  • 127 parametrów w jednej definicji makra
  • 127 argumentów w jednym wywołaniu makra
  • 4095 znaków w logicznej linii źródłowej
  • 4095 znaków w dosłownym ciągu znaków lub w dosłownym ciągu znaków (po konkatenacji)
  • 65535 bajtów w obiekcie (tylko w środowisku hostowanym)
  • 15 poziomów testowania dla plików zawartych w pakiecie
  • 1023 etykiety przypadków dla instrukcji zamiany (z wyłączeniem etykiet dla jakichkolwiek nowych instrukcji zamiany)

Byłem trochę zaskoczony limitem 1023 etykiet przypadków dla instrukcji switch. Mogę zauważyć, że przekroczenie generowanego kodu / lex / parserów jest dość łatwe.

Jeśli limity te zostaną przekroczone, masz niezdefiniowane zachowanie (awarie, wady bezpieczeństwa itp.).

Tak, wiem, że pochodzi ze specyfikacji C, ale C ++ udostępnia te podstawowe funkcje obsługi.

RandomNickName42
źródło
9
Jeśli osiągniesz te limity, masz więcej problemów niż niezdefiniowane zachowanie.
new123456
Możesz ŁATWO przekroczyć 65535 bajtów w obiekcie, takim jak STD :: vector
Demi
2

Używanie memcpydo kopiowania między nakładającymi się regionami pamięci. Na przykład:

char a[256] = {};
memcpy(a, a, sizeof(a));

Zachowanie jest niezdefiniowane zgodnie ze standardem C, który jest objęty standardem C ++ 03.

7.21.2.1 Funkcja memcpy

Streszczenie

1 / #include void * memcpy (void * ograniczenia s1, const void * ograniczenia s2, size_t n);

Opis

2 / Funkcja memcpy kopiuje n znaków z obiektu wskazywanego przez s2 do obiektu wskazywanego przez s1. Jeśli kopiowanie odbywa się między nakładającymi się obiektami, zachowanie jest niezdefiniowane. Zwraca 3 Funkcja memcpy zwraca wartość s1.

7.21.2.2 Funkcja memmove

Streszczenie

1 #include void * memmove (void * s1, const void * s2, size_t n);

Opis

2 Funkcja memmove kopiuje n znaków z obiektu wskazywanego przez s2 do obiektu wskazywanego przez s1. Kopiowanie odbywa się tak, jakby n znaków z obiektu wskazanego przez s2 zostało najpierw skopiowanych do tymczasowej tablicy n znaków, która nie zachodzi na obiekty wskazane przez s1 i s2, a następnie n znaków z tablicy tymczasowej jest skopiowanych do obiekt wskazywany przez s1. Zwroty

3 Funkcja memmove zwraca wartość s1.

John Dibling
źródło
2

Jedynym typem, dla którego C ++ gwarantuje rozmiar, jest char. Rozmiar to 1. Rozmiar wszystkich innych typów zależy od platformy.

JaredPar
źródło
Czy nie po to jest <cstdint>? Definiuje typy, takie jak uint16_6 i tak dalej.
Jasper Bekkers,
Tak, ale rozmiar większości typów, powiedzmy długi, nie jest dobrze zdefiniowany.
JaredPar,
także cstdint nie jest jeszcze częścią obecnego standardu c ++. zobacz boost / stdint.hpp, aby znaleźć obecnie przenośne rozwiązanie.
Evan Teran,
To nie jest niezdefiniowane zachowanie. Norma mówi, że platforma zgodna definiuje rozmiary, a nie norma definiująca je.
Daniel Earwicker,
1
@JaredPar: To złożony post z wieloma wątkami, więc podsumowałem to tutaj . Najważniejsze jest to: „5. Aby reprezentować -2147483647 i +2147483647 w formacie binarnym, potrzebujesz 32 bitów”.
John Dibling
2

Obiekty na poziomie przestrzeni nazw w różnych jednostkach kompilacji nigdy nie powinny zależeć od siebie przy inicjalizacji, ponieważ ich kolejność inicjowania jest niezdefiniowana.

tak
źródło