Kiedy wywołanie funkcji członka na instancji null powoduje nieokreślone zachowanie?

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

Oczekujemy, że (b) ulegnie awarii, ponieważ nie ma odpowiadającego elementu x dla wskaźnika null. W praktyce (a) nie ulega awarii, ponieważ wskaźnik this nigdy nie jest używany.

Ponieważ (b) dereferencja określa wskaźnik this ((*this).x = 5;), a this jest null, program wprowadza niezdefiniowane zachowanie, ponieważ dereferencja null jest zawsze określana jako niezdefiniowane zachowanie.

Czy (a) powoduje nieokreślone zachowanie? Co jeśli obie funkcje (i x) są statyczne?

Author: pbn, 2010-03-19

2 answers

Zarówno (a) jak i (b) skutkują niezdefiniowanym zachowaniem. Wywołanie funkcji członka za pomocą wskaźnika null jest zawsze niezdefiniowanym zachowaniem. Jeśli funkcja jest statyczna, jest również niezdefiniowana technicznie, ale jest pewien spór.


Pierwszą rzeczą do zrozumienia jest to, dlaczego niezdefiniowanym zachowaniem jest dereferencja wskaźnika null. W C++03 jest tu trochę niejasności.

Chociaż "dereferencja wskaźnika null powoduje nieokreślone zachowanie" jest wspomniane w uwagach zarówno w §1.9/4, jak i §8.3.2 / 4, nigdy nie zostało wyraźnie stwierdzone. (Uwagi są nienormatywne.)

Można jednak spróbować wywnioskować to z §3.10/ 2:

Lvalue odnosi się do obiektu lub funkcji.

Gdy dereferencja, wynikiem jest lvalue. Wskaźnik null Nie odnosi się do obiektu, dlatego gdy używamy lvalue, mamy niezdefiniowane zachowanie. Problem w tym, że poprzednie zdanie nigdy nie jest podane, więc co to oznacza dla "użyć" lvalue? Po prostu wygenerować go w ogóle, czy użyć go w bardziej formalnym sensie wykonywania konwersji lvalue-to-rvalue?

Niezależnie od tego, zdecydowanie nie może być przekonwertowana na wartość R (§4.1/1):

Jeśli obiekt, do którego odnosi się lvalue, nie jest obiektem typu T i nie jest obiektem typu pochodnego od T, lub jeśli obiekt jest niezinicjalizowany, program, który wymaga tej konwersji, ma niezdefiniowane zachowanie.

Tutaj jest definitywnie nieokreślony zachowanie.

Wieloznaczność wynika z tego, czy jest to nieokreślone zachowanie do deference , ale nie używa wartości z nieprawidłowego wskaźnika (tzn. pobiera wartość lvalue, ale nie konwertuje jej na wartość rvalue). Jeśli nie, to int *i = 0; *i; &(*i); jest dobrze zdefiniowane. To jest aktywny problem .

Mamy więc surowy widok" dereferenced a NULL pointer, get undefined behavior "oraz słaby widok" use a dereferenced null pointer, get undefined behavior".

Teraz rozważamy pytanie.


Tak, (a) powoduje nieokreślone zachowanie. W rzeczywistości, jeśli this jest null, to niezależnie od zawartości funkcji wynik jest niezdefiniowany.

Wynika to z §5.2.5/3:

Jeśli E1 mA typ "wskaźnik do klasy X", to wyrażenie E1->E2 jest konwertowane do postaci równoważnej (*(E1)).E2;

*(E1) spowoduje nieokreślone zachowanie ze ścisłą interpretacją i .E2 zamieni je na wartość R, co sprawi, że nieokreślone zachowanie dla słabej interpretacji.

Wynika również, że jest to nieokreślone zachowanie bezpośrednio z (§9.3.1/1):

Jeśli niestatyczna funkcja klasy X jest wywoływana dla obiektu, który nie jest typu X lub typu wywodzącego się z X, zachowanie jest niezdefiniowane.


Z FUNKCJAMI statycznymi interpretacja ścisła kontra słaba robi różnicę. Ściśle mówiąc, jest niezdefiniowany:

Członkiem statycznym może być odnosi się do użycia składni dostępu członka klasy, w którym to przypadku obiekt-wyrażenie jest oceniane.

Oznacza to, że jest on oceniany tak, jakby nie był statyczny i po raz kolejny dereferujemy wskaźnik null z (*(E1)).E2.

Jednakże, ponieważ {[5] } nie jest używane w statycznym wywołaniu funkcji member, jeśli użyjemy słabej interpretacji, wywołanie jest dobrze zdefiniowane. *(E1) daje lvalue, funkcja statyczna jest rozwiązywana, *(E1) jest odrzucana, a funkcja jest wywoływana. Nie ma konwersja lvalue-to-rvalue, więc nie ma niezdefiniowanego zachowania.

W C++0x, począwszy od n3126, niejasność pozostaje. Na razie bądź bezpieczny: użyj ścisłej interpretacji.

 104
Author: GManNickG,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-10-18 21:57:37

Oczywiście undefined oznacza, że jest nie zdefiniowany, ale czasami może być przewidywalny. Informacje, które mam zamiar dostarczyć, nigdy nie powinny być wykorzystywane do pracy kodu, ponieważ z pewnością nie jest gwarantowana, ale może się przydać podczas debugowania.

Można by pomyśleć, że wywołanie funkcji na wskaźniku obiektu spowoduje dereferencję wskaźnika i spowoduje ub. W praktyce, jeśli funkcja nie jest wirtualna, kompilator przekonwertuje ją do zwykłego wywołania funkcji przekazującego wskaźnik jako pierwszy parametr to , omijając dereferencję i tworząc bombę zegarową dla wywołanej funkcji member. Jeśli funkcja member nie odwołuje się do żadnych zmiennych członkowskich lub funkcji wirtualnych, może faktycznie odnieść sukces bez błędu. Pamiętaj, że sukces mieści się we wszechświecie "undefined"!

Funkcja Microsoft MFCGetSafeHwnd w rzeczywistości opiera się na tym zachowaniu. Nie wiem, co palili.

Jeśli wywołujesz funkcję wirtualną, aby dostać się do vtable, musisz mieć pewność, że dostaniesz UB (prawdopodobnie awarię, ale pamiętaj, że nie ma gwarancji).

 26
Author: Mark Ransom,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-09-23 21:11:33