Quando invocare una funzione membro su un'istanza nulla risulta in un comportamento indefinito?


120

Considera il codice seguente:

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

Ci aspettiamo un (b)arresto anomalo, perché non esiste un membro corrispondente xper il puntatore nullo. In pratica, (a)non si blocca perché il thispuntatore non viene mai utilizzato.

Poiché (b)dereferenzia il thispointer ( (*this).x = 5;) ed thisè nullo, il programma entra in un comportamento indefinito, poiché dereferenziare null è sempre detto un comportamento indefinito.

Si (a)traduce in un comportamento indefinito? E se entrambe le funzioni (e x) fossero statiche?


Se entrambe le funzioni sono statiche , come si potrebbe fare riferimento a x all'interno di baz ? (x è una variabile membro non statica)
legends2k

4
@ legends2k: Anche finta è xstato reso statico. :)
GManNickG

Certamente, ma per il caso (a) funziona allo stesso modo in tutti i casi, cioè la funzione viene invocata. Tuttavia, sostituendo il valore del puntatore da 0 a 1 (ad esempio tramite reinterpret_cast), quasi sempre si blocca. L'allocazione del valore di 0 e quindi NULL, come nel caso a, rappresenta qualcosa di speciale per il compilatore? Perché si blocca sempre con qualsiasi altro valore assegnato?
Siddharth Shankaran

5
Interessante: alla prossima revisione di C ++, non ci sarà più alcuna dereferenziazione dei puntatori. Eseguiremo ora l' indirizzamento tramite puntatori. Per saperne di più, eseguire l'indirizzamento tramite questo collegamento: N3362
James

3
Invocare una funzione membro su un puntatore null è sempre un comportamento indefinito. Solo guardando il tuo codice, posso già sentire il comportamento indefinito che mi striscia lentamente sul collo!
fredoverflow

Risposte:


113

Entrambi (a)e (b)comportano un comportamento indefinito. È sempre un comportamento indefinito chiamare una funzione membro tramite un puntatore nullo. Se la funzione è statica, è anche tecnicamente indefinita, ma c'è qualche controversia.


La prima cosa da capire è perché è un comportamento indefinito dereferenziare un puntatore nullo. In C ++ 03, c'è effettivamente un po 'di ambiguità qui.

Sebbene "dereferenziare un puntatore nullo si traduca in un comportamento indefinito" sia menzionato nelle note sia in §1.9 / 4 che in §8.3.2 / 4, non viene mai dichiarato esplicitamente. (Le note non sono normative.)

Tuttavia, si può provare a dedurlo dal §3.10 / 2:

Un lvalue si riferisce a un oggetto o una funzione.

Quando si dereferenzia, il risultato è un lvalue. Un puntatore nullo non si riferisce a un oggetto, quindi quando usiamo lvalue abbiamo un comportamento indefinito. Il problema è che la frase precedente non viene mai pronunciata, quindi cosa significa "usare" lvalue? Basta anche solo generarlo o usarlo nel senso più formale di eseguire una conversione da valore a valore?

Indipendentemente da ciò, sicuramente non può essere convertito in un rvalue (§4.1 / 1):

Se l'oggetto a cui si riferisce lvalue non è un oggetto di tipo T e non è un oggetto di un tipo derivato da T, o se l'oggetto non è inizializzato, un programma che necessita di questa conversione ha un comportamento indefinito.

Qui è un comportamento decisamente indefinito.

L'ambiguità deriva dal fatto che sia o meno un comportamento indefinito da deferire ma non utilizzare il valore da un puntatore non valido (ovvero, ottenere un lvalue ma non convertirlo in un rvalue). In caso contrario, int *i = 0; *i; &(*i);è ben definito. Questo è un problema attivo .

Quindi abbiamo una vista rigorosa "dereferenziare un puntatore nullo, ottenere un comportamento non definito" e una vista debole "utilizzare un puntatore nullo dereferenziato, ottenere un comportamento indefinito".

Ora consideriamo la domanda.


Sì, si (a)traduce in un comportamento indefinito. Infatti, se thisè nullo, indipendentemente dal contenuto della funzione il risultato è indefinito.

Ciò segue dal §5.2.5 / 3:

Se E1ha il tipo "puntatore alla classe X", l'espressione E1->E2viene convertita nella forma equivalente(*(E1)).E2;

*(E1)si tradurrà in un comportamento indefinito con un'interpretazione rigorosa e lo .E2converte in un rvalue, rendendolo un comportamento indefinito per l'interpretazione debole.

Ne consegue anche che si tratta di un comportamento indefinito direttamente da (§9.3.1 / 1):

Se una funzione membro non statica di una classe X viene chiamata per un oggetto che non è di tipo X o di un tipo derivato da X, il comportamento è indefinito.


Con le funzioni statiche, l'interpretazione rigorosa rispetto a quella debole fa la differenza. A rigor di termini, è indefinito:

È possibile fare riferimento a un membro statico utilizzando la sintassi di accesso ai membri della classe, nel qual caso viene valutata l'espressione dell'oggetto.

Cioè, viene valutato come se non fosse statico e dereferenziamo ancora una volta un puntatore nullo con (*(E1)).E2.

Tuttavia, poiché E1non viene utilizzato in una chiamata statica di funzione membro, se usiamo l'interpretazione debole la chiamata è ben definita. *(E1)restituisce un lvalue, la funzione statica viene risolta, *(E1)viene scartata e la funzione viene chiamata. Non esiste una conversione da lvalue a rvalue, quindi non esiste un comportamento indefinito.

In C ++ 0x, a partire da n3126, l'ambiguità rimane. Per ora, stai al sicuro: usa l'interpretazione rigorosa.


5
+1. Continuando la pedanteria, sotto la "definizione debole" la funzione membro non statico non è stata chiamata "per un oggetto che non è di tipo X". È stato richiesto un valore che non sia affatto un oggetto. Quindi la soluzione proposta aggiunge il testo "o se lvalue è un lvalue vuoto" alla clausola citata.
Steve Jessop

Potresti chiarire un po '? In particolare, con i tuoi link "problema chiuso" e "problema attivo", quali sono i numeri del problema? Inoltre, se questo è un problema chiuso, qual è esattamente la risposta sì / no per le funzioni statiche? Mi sento come se mi mancasse l'ultimo passaggio per cercare di capire la tua risposta.
Brooks Moses

4
Non penso che il difetto 315 di CWG sia "chiuso" come implica la sua presenza nella pagina "questioni chiuse". La logica dice che dovrebbe essere consentito perché " *pnon è un errore quando pè nullo a meno che lvalue non venga convertito in un rvalue". Tuttavia, ciò si basa sul concetto di un "valore vuoto", che fa parte della proposta di risoluzione del difetto 232 CWG , ma che non è stato adottato. Quindi, con il linguaggio sia in C ++ 03 che in C ++ 0x, la dereferenziazione del puntatore nullo è ancora indefinita, anche se non c'è conversione da lvalue a rvalue.
James McNellis

1
@JamesMcNellis: Per quanto ne so, se pfosse un indirizzo hardware che attivasse un'azione quando letto, ma non fosse dichiarato volatile, l'istruzione *p;non sarebbe richiesta, ma sarebbe consentita , per leggere effettivamente quell'indirizzo; la dichiarazione &(*p);, tuttavia, sarebbe vietata. Se lo *pfosse volatile, sarebbe necessaria la lettura. In entrambi i casi, se il puntatore non è valido, non riesco a vedere come la prima istruzione non sarebbe un comportamento indefinito, ma non riesco nemmeno a vedere perché lo sarebbe la seconda istruzione.
supercat

1
".E2 lo converte in un rvalue," - Uh, no non lo fa
MM

30

Ovviamente indefinito significa che non è definito , ma a volte può essere prevedibile. Le informazioni che sto per fornire non dovrebbero mai essere invocate per il codice funzionante poiché certamente non sono garantite, ma potrebbero tornare utili durante il debug.

Potresti pensare che chiamare una funzione su un puntatore a un oggetto dereferenzierà il puntatore e causerà UB. In pratica se la funzione non è virtuale, il compilatore l'avrà convertita in una semplice chiamata di funzione passando il puntatore come primo parametro this , bypassando la dereferenziazione e creando una bomba a orologeria per la funzione membro chiamata. Se la funzione membro non fa riferimento a variabili membro o funzioni virtuali, potrebbe effettivamente riuscire senza errori. Ricorda che il successo rientra nell'universo del "non definito"!

La funzione MFC di Microsoft GetSafeHwnd si basa effettivamente su questo comportamento. Non so cosa stessero fumando.

Se stai chiamando una funzione virtuale, il puntatore deve essere dereferenziato per arrivare a vtable, e sicuramente otterrai UB (probabilmente un crash ma ricorda che non ci sono garanzie).


1
GetSafeHwnd prima esegue un! Questo controllo e, se vero, restituisce NULL. Quindi inizia un frame SEH e dereferenzia il puntatore. se è presente una violazione dell'accesso alla memoria (0xc0000005), viene rilevato e viene restituito NULL al chiamante :) Altrimenti viene restituito l'HWND.
Петър Петров

@ ПетърПетров sono passati parecchi anni da quando ho guardato il codice GetSafeHwnd, è possibile che lo abbiano migliorato da allora. E non dimenticare che hanno una conoscenza approfondita del funzionamento del compilatore!
Mark Ransom

Sto indicando una possibile implementazione di esempio che ha lo stesso effetto, quello che fa veramente è essere decodificato usando un debugger :)
Петър Петров

1
"hanno una conoscenza privilegiata del funzionamento del compilatore!" - la causa di problemi eterni per progetti come MinGW che tentano di consentire a g ++ di compilare codice che chiama l'API di Windows
MM

@MM Penso che saremmo tutti d'accordo sul fatto che è ingiusto. E per questo motivo, penso anche che ci sia una legge sulla compatibilità che rende un po 'illegale mantenerlo così.
v.oddou
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.