Rozważ następujący kod:
#include <iostream>
struct foo
{
// (a):
void bar() { std::cout << "gman was here" << std::endl; }
// (b):
void baz() { x = 5; }
int x;
};
int main()
{
foo* f = 0;
f->bar(); // (a)
f->baz(); // (b)
}
Spodziewamy (b)
się awarii, ponieważ nie ma odpowiedniego elementu członkowskiego x
dla wskaźnika pustego. W praktyce (a)
nie ulega awarii, ponieważ this
wskaźnik nigdy nie jest używany.
Ponieważ (b)
wyłuskiwanie this
pointer ( (*this).x = 5;
) i this
jest null, program wprowadza niezdefiniowane zachowanie, ponieważ dereferencjonowanie null jest zawsze uważane za niezdefiniowane zachowanie.
Ma (a)
doprowadzić do nieokreślonego zachowania? A co jeśli obie funkcje (i x
) są statyczne?
x
że też jest statyczny. :)Odpowiedzi:
Obie
(a)
i(b)
powodują niezdefiniowane zachowanie. Wywołanie funkcji składowej za pomocą wskaźnika pustego jest zawsze niezdefiniowanym zachowaniem. Jeśli funkcja jest statyczna, również jest technicznie niezdefiniowana, ale istnieje spór.Pierwszą rzeczą do zrozumienia jest to, dlaczego odwołanie do pustego wskaźnika jest niezdefiniowanym zachowaniem. W C ++ 03 jest tu właściwie trochę niejednoznaczności.
Mimo że „dereferencjonowanie pustego wskaźnika skutkuje niezdefiniowanym zachowaniem” jest wspomniane w uwagach w obu §1.9 / 4 i §8.3.2 / 4, nigdy nie zostało to wyraźnie powiedziane. (Uwagi są nienormatywne.)
Można jednak spróbować wywnioskować to z § 3.10/2:
Podczas dereferencji wynikiem jest lwartość. Wskaźnik zerowy nie odnosi się do obiektu, dlatego kiedy używamy lvalue, mamy niezdefiniowane zachowanie. Problem w tym, że poprzednie zdanie nigdy nie zostało wypowiedziane, więc co to znaczy „używać” lwartości? Po prostu wygeneruj go w ogóle, czy użyj go w bardziej formalnym sensie wykonywania konwersji lwartości na rwartość?
Niezależnie od tego zdecydowanie nie można go przekonwertować na wartość r (§4.1 / 1):
Tutaj jest to zdecydowanie niezdefiniowane zachowanie.
Niejednoznaczność wynika z tego, czy jest to niezdefiniowane zachowanie, aby uszanować, ale nie używa wartości z nieprawidłowego wskaźnika (to znaczy, pobiera lwartość, ale nie konwertuje jej na rwartość). Jeśli nie, to
int *i = 0; *i; &(*i);
jest dobrze zdefiniowane. To jest aktywny problem .Mamy więc ścisły widok „wyłuskiwanie wskaźnika zerowego, uzyskanie niezdefiniowanego zachowania” i słaby widok „użyj wyłuskanego wskaźnika zerowego, uzyskaj niezdefiniowane zachowanie”.
Teraz rozważymy pytanie.
Tak,
(a)
skutkuje niezdefiniowanym zachowaniem. W rzeczywistości, jeślithis
jest null, to niezależnie od zawartości funkcji wynik jest niezdefiniowany.Wynika to z §5.2.5 / 3:
*(E1)
spowoduje niezdefiniowane zachowanie ze ścisłą interpretacją i.E2
przekształci je na wartość r, co spowoduje niezdefiniowane zachowanie w przypadku słabej interpretacji.Wynika z tego również, że jest to niezdefiniowane zachowanie bezpośrednio z (§9.3.1 / 1):
W przypadku funkcji statycznych różnica polega na interpretacji ścisłej i słabej. Ściśle mówiąc, jest niezdefiniowany:
Oznacza to, że jest oceniany tak, jakby był niestatyczny i po raz kolejny wyłuskujemy wskaźnik zerowy z
(*(E1)).E2
.Jednak ponieważ
E1
nie jest używany w statycznym wywołaniu funkcji składowej, jeśli używamy słabej interpretacji, wywołanie jest dobrze zdefiniowane.*(E1)
daje lwartość, funkcja statyczna jest rozwiązywana,*(E1)
odrzucana i wywoływana. Nie ma konwersji lwartości na rwartość, więc nie ma niezdefiniowanego zachowania.W C ++ 0x, od n3126, niejednoznaczność pozostaje. Na razie bądź bezpieczny: stosuj ścisłą interpretację.
źródło
*p
nie jest błędem, gdyp
jest zerowe, chyba że lwartość jest konwertowana na rwartość”. Jednak opiera się to na koncepcji „pustej wartości l”, która jest częścią proponowanej rezolucji w sprawie błędu CWG 232 , ale która nie została przyjęta. Tak więc, z językiem w C ++ 03 i C ++ 0x, dereferencja pustego wskaźnika jest nadal niezdefiniowana, nawet jeśli nie ma konwersji l-wartość na r-wartość.p
adres sprzętowy wyzwalał jakąś akcję po odczytaniu, ale nie zostałby zadeklarowanyvolatile
, instrukcja*p;
zostałby nie byłaby wymagana, ale mogłaby faktycznie odczytać ten adres; oświadczenie&(*p);
byłoby jednak zabronione. Gdyby tak*p
byłovolatile
, odczyt byłby wymagany. W obu przypadkach, jeśli wskaźnik jest nieprawidłowy, nie widzę, jak pierwsza instrukcja nie byłaby niezdefiniowanym zachowaniem, ale nie widzę też, dlaczego miałaby tak być druga instrukcja.Oczywiście nieokreślony oznacza, że nie jest zdefiniowany , ale czasami może być przewidywalny. Informacje, które zamierzam podać, nigdy nie powinny być oparte na działającym kodzie, ponieważ z pewnością nie jest to gwarantowane, ale mogą się przydać podczas debugowania.
Można by pomyśleć, że wywołanie funkcji na wskaźniku obiektu spowoduje wyłuskiwanie wskaźnika i spowoduje UB. W praktyce, jeśli funkcja nie jest wirtualna, kompilator przekształci ją w zwykłe wywołanie funkcji, przekazując wskaźnik jako pierwszy parametr this , omijając wyłuskiwanie i tworząc bombę zegarową dla wywoływanej funkcji składowej. Jeśli funkcja składowa nie odwołuje się do żadnych zmiennych składowych ani funkcji wirtualnych, może się powieść bez błędu. Pamiętaj, że sukces mieści się w uniwersum „nieokreślonego”!
Funkcja MFC firmy Microsoft GetSafeHwnd faktycznie opiera się na tym zachowaniu. Nie wiem, co palili.
Jeśli wywołujesz funkcję wirtualną, wskaźnik musi zostać wyłuskany, aby dostać się do tabeli vtable, i na pewno otrzymasz UB (prawdopodobnie awaria, ale pamiętaj, że nie ma żadnych gwarancji).
źródło
GetSafeHwnd
, możliwe, że od tego czasu go ulepszyli . I nie zapominaj, że mają poufną wiedzę na temat działania kompilatora!