Considere o seguinte código:
#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)
}
Esperamos (b)
travar, porque não há membro correspondente x
para o ponteiro nulo. Na prática, (a)
não falha porque o this
ponteiro nunca é usado.
Como (b)
desreferencia o this
ponteiro ( (*this).x = 5;
) e this
é nulo, o programa insere um comportamento indefinido, pois a desreferenciação nula sempre é considerada um comportamento indefinido.
O (a)
resultado é um comportamento indefinido? E se as duas funções (e x
) forem estáticas?
x
também ficar estático. :)Respostas:
Ambos
(a)
e(b)
resultam em comportamento indefinido. É sempre um comportamento indefinido chamar uma função de membro por meio de um ponteiro nulo. Se a função é estática, também é tecnicamente indefinida, mas há alguma disputa.A primeira coisa a entender é por que é um comportamento indefinido desreferenciar um ponteiro nulo. No C ++ 03, há realmente um pouco de ambiguidade aqui.
Embora "desreferenciar um ponteiro nulo resulte em comportamento indefinido" seja mencionado nas notas nos §1.9 / 4 e §8.3.2 / 4, nunca é explicitamente declarado. (As notas não são normativas.)
No entanto, pode-se tentar deduzi-lo de §3.10 / 2:
Ao cancelar a referência, o resultado é um valor l. Um ponteiro nulo não se refere a um objeto; portanto, quando usamos o lvalue, temos um comportamento indefinido. O problema é que a sentença anterior nunca é declarada, então o que significa "usar" o lvalue? Apenas gerá-lo ou usá-lo no sentido mais formal de realizar conversão de valor em valor?
Independentemente disso, definitivamente não pode ser convertido em um rvalor (§4.1 / 1):
Aqui é definitivamente um comportamento indefinido.
A ambiguidade vem do fato de ser ou não um comportamento indefinido para deferência, mas não usar o valor de um ponteiro inválido (ou seja, obter um lvalue, mas não convertê-lo em um rvalue). Caso contrário,
int *i = 0; *i; &(*i);
está bem definido. Este é um problema ativo .Portanto, temos uma exibição estrita de "desreferenciar um ponteiro nulo, obter comportamento indefinido" e uma exibição fraca de "usar um ponteiro nulo não referenciado, obter comportamento indefinido".
Agora consideramos a questão.
Sim,
(a)
resulta em comportamento indefinido. De fato, sethis
for nulo, independentemente do conteúdo da função, o resultado será indefinido.Isto segue do §5.2.5 / 3:
*(E1)
resultará em comportamento indefinido com uma interpretação estrita e o.E2
converterá em um rvalor, tornando-o um comportamento indefinido para a interpretação fraca.Segue-se também que é um comportamento indefinido diretamente de (§9.3.1 / 1):
Com funções estáticas, a interpretação estrita versus fraca faz a diferença. A rigor, é indefinido:
Ou seja, é avaliado como se não fosse estático e, mais uma vez, desreferenciamos um ponteiro nulo
(*(E1)).E2
.No entanto, como
E1
não é usada em uma chamada de função de membro estática, se usarmos a interpretação fraca, a chamada será bem definida.*(E1)
resulta em um lvalue, a função estática é resolvida,*(E1)
é descartada e a função é chamada. Não há conversão de valor em valor, portanto, não há comportamento indefinido.No C ++ 0x, a partir do n3126, a ambiguidade permanece. Por enquanto, esteja seguro: use a interpretação estrita.
fonte
*p
não é um erro quandop
é nulo, a menos que o lvalue seja convertido em um rvalue". No entanto, isso se baseia no conceito de um "valor vazio vazio", que faz parte da resolução proposta para o defeito do CWG 232 , mas que não foi adotado. Portanto, com a linguagem em C ++ 03 e C ++ 0x, a desreferenciação do ponteiro nulo ainda é indefinida, mesmo se não houver conversão de valor-para-valor.p
fosse um endereço de hardware que desencadearia alguma ação quando lida, mas não fosse declaradavolatile
, a declaração*p;
não seria necessária, mas teria permissão , para realmente ler esse endereço; a declaração&(*p);
, no entanto, seria proibida de fazê-lo. Se*p
fossevolatile
, a leitura seria necessária. Em qualquer um dos casos, se o ponteiro for inválido, não consigo ver como a primeira instrução não seria Comportamento indefinido, mas também não consigo ver por que a segunda instrução seria.Obviamente indefinido significa que não está definido , mas às vezes pode ser previsível. As informações que eu estou prestes a fornecer nunca devem ser consideradas como código de trabalho, pois certamente não são garantidas, mas podem ser úteis durante a depuração.
Você pode pensar que chamar uma função em um ponteiro de objeto desreferenciar o ponteiro e causar UB. Na prática, se a função não é virtual, o compilador vai ter convertido-lo para uma chamada de função simples passar o ponteiro como o primeiro parâmetro presente , ignorando o dereference e criando uma bomba relógio para a chamada função membro. Se a função de membro não fizer referência a nenhuma variável de membro ou função virtual, ela poderá ter êxito sem erro. Lembre-se de que o sucesso se enquadra no universo de "indefinido"!
A função MFC da Microsoft GetSafeHwnd, na verdade, depende desse comportamento. Eu não sei o que eles estavam fumando.
Se você estiver chamando uma função virtual, o ponteiro deve ser desreferenciado para chegar à tabela de dados e, com certeza, você obterá UB (provavelmente uma falha, mas lembre-se de que não há garantias).
fonte
GetSafeHwnd
, é possível que eles o tenham aprimorado desde então. E não esqueça que eles têm conhecimento interno sobre o funcionamento do compilador!