Kiedy wywołanie funkcji członkowskiej w instancji o wartości null powoduje niezdefiniowane zachowanie?

120

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 xdla wskaźnika pustego. W praktyce (a)nie ulega awarii, ponieważ thiswskaźnik nigdy nie jest używany.

Ponieważ (b)wyłuskiwanie thispointer ( (*this).x = 5;) i thisjest 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?

GManNickG
źródło
Jeśli obie funkcje są statyczne , jak można odwołać się do x wewnątrz baz ? (x jest niestatyczną zmienną składową)
legends2k
4
@ legends2k: Udawaj, xże też jest statyczny. :)
GManNickG,
Z pewnością, ale w przypadku (a) działa to tak samo we wszystkich przypadkach, tj. Funkcja zostaje wywołana. Jednak zastąpienie wartości wskaźnika z 0 na 1 (powiedzmy przez reinterpret_cast), prawie zawsze ulega awarii. Czy alokacja wartości 0, a więc NULL, jak w przypadku a, reprezentuje coś specjalnego dla kompilatora? Dlaczego zawsze zawiesza się z jakąkolwiek inną przypisaną mu wartością?
Siddharth Shankaran
5
Interesujące: w następnej wersji C ++ nie będzie już w ogóle dereferencji wskaźników. Wykonamy teraz pośrednie poprzez wskaźniki. Aby dowiedzieć się więcej, skorzystaj pośrednio z tego linku: N3362
James McNellis,
3
Wywołanie funkcji członkowskiej na pustym wskaźniku jest zawsze niezdefiniowanym zachowaniem. Patrząc na twój kod, już czuję, że niezdefiniowane zachowanie powoli wkrada się po mojej szyi!
fredoverflow

Odpowiedzi:

113

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:

Wartość l odnosi się do obiektu lub funkcji.

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

Jeśli obiekt, do którego odnosi się lwartość, nie jest obiektem typu T i nie jest obiektem typu wywodzącego się z T lub jeśli obiekt nie jest zainicjowany, program, który wymaga tej konwersji, ma niezdefiniowane zachowanie.

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śli thisjest null, to niezależnie od zawartości funkcji wynik jest niezdefiniowany.

Wynika to z §5.2.5 / 3:

Jeśli E1ma typ „wskaźnik do klasy X”, wówczas wyrażenie E1->E2jest konwertowane na równoważną formę(*(E1)).E2;

*(E1)spowoduje niezdefiniowane zachowanie ze ścisłą interpretacją i .E2przekształ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):

Jeśli niestatyczna funkcja składowa klasy X jest wywoływana dla obiektu, który nie jest typu X lub typu pochodnego od X, zachowanie jest niezdefiniowane.


W przypadku funkcji statycznych różnica polega na interpretacji ścisłej i słabej. Ściśle mówiąc, jest niezdefiniowany:

Do statycznego elementu członkowskiego można się odwoływać przy użyciu składni dostępu do elementu członkowskiego klasy, w którym to przypadku obliczane jest wyrażenie obiektu.

Oznacza to, że jest oceniany tak, jakby był niestatyczny i po raz kolejny wyłuskujemy wskaźnik zerowy z (*(E1)).E2.

Jednak ponieważ E1nie 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ę.

GManNickG
źródło
5
+1. Kontynuując pedanterię, w ramach „słabej definicji” niestatyczna funkcja składowa nie została wywołana „dla obiektu, który nie jest typu X”. Został wywołany dla lwartości, która w ogóle nie jest obiektem. Zatem proponowane rozwiązanie dodaje tekst „lub jeśli lwartość jest pustą lwartością” do cytowanej klauzuli.
Steve Jessop
Czy mógłbyś trochę wyjaśnić? W szczególności w przypadku linków „zamknięta sprawa” i „aktywna sprawa”, jakie są numery zgłoszeń? Ponadto, jeśli jest to zamknięta kwestia, jaka dokładnie jest odpowiedź tak / nie dla funkcji statycznych? Czuję, że brakuje mi ostatniego kroku, próbując zrozumieć twoją odpowiedź.
Brooks Moses,
4
Nie sądzę, aby wada 315 CWG była tak „zamknięta”, jak sugeruje jej obecność na stronie „zamknięte problemy”. Uzasadnienie mówi, że powinno być dozwolone, ponieważ „ *pnie jest błędem, gdy pjest 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ść.
James McNellis
1
@JamesMcNellis: W moim rozumieniu, gdyby padres sprzętowy wyzwalał jakąś akcję po odczytaniu, ale nie zostałby zadeklarowany volatile, instrukcja*p; zostałby nie byłaby wymagana, ale mogłaby faktycznie odczytać ten adres; oświadczenie &(*p);byłoby jednak zabronione. Gdyby tak *pbyło volatile, 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.
supercat
1
„.E2 konwertuje to na wartość r”, - Uh, nie, nie robi
MM,
30

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

Mark Okup
źródło
1
GetSafeHwnd najpierw sprawdza! To sprawdzenie i jeśli prawda, zwraca NULL. Następnie rozpoczyna ramkę SEH i wyłuskuje wskaźnik. jeśli występuje naruszenie dostępu do pamięci (0xc0000005), zostaje ono przechwycone, a wywołującemu zwracana jest wartość NULL :) W przeciwnym razie zwracany jest HWND.
Петър Петров
@ ПетърПетров Minęło sporo lat, odkąd przyjrzałem się kodowi GetSafeHwnd, możliwe, że od tego czasu go ulepszyli . I nie zapominaj, że mają poufną wiedzę na temat działania kompilatora!
Mark Ransom
Podaję przykładową możliwą implementację, która ma ten sam efekt, co tak naprawdę robi, to
odtworzenie kodu
1
„mają poufną wiedzę na temat działania kompilatora!” - przyczyna wiecznych kłopotów dla projektów takich jak MinGW, które próbują pozwolić g ++ na kompilację kodu wywołującego API Windows
MM,
@MM Myślę, że wszyscy zgodzimy się, że to niesprawiedliwe. I z tego powodu uważam, że istnieje prawo dotyczące kompatybilności, które sprawia, że ​​utrzymanie go w takim stanie jest nieco nielegalne.
v.oddou