Quando a chamada de uma função de membro em uma instância nula resulta em um comportamento indefinido?

120

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 xpara o ponteiro nulo. Na prática, (a)não falha porque o thisponteiro nunca é usado.

Como (b)desreferencia o thisponteiro ( (*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?

GManNickG
fonte
Se ambas as funções são estáticas , como x pode ser referido dentro do baz ? (x é uma variável membro não-estático)
legends2k
4
@ legends2k: Fingir xtambém ficar estático. :)
GManNickG
Certamente, mas para o caso (a) funciona da mesma forma em todos os casos, ou seja, a função é invocada. No entanto, substituindo o valor do ponteiro de 0 para 1 (digamos, através de reinterpret_cast), ele quase sempre falha sempre. A alocação de valor 0 e, portanto, NULL, como no caso a, representa algo especial para o compilador? Por que ele sempre falha com qualquer outro valor alocado a ele?
Siddharth Shankaran
5
Interessante: na próxima revisão do C ++, não haverá mais desreferenciamento de ponteiros. Vamos agora executar indiretamente através de ponteiros. Para saber mais, por favor executar indireto através deste link: N3362
James McNellis
3
Invocar uma função de membro em um ponteiro nulo é sempre um comportamento indefinido. Só de olhar o seu código, já consigo sentir o comportamento indefinido subindo lentamente pelo meu pescoço!
Fredoverflow

Respostas:

113

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:

Um valor l refere-se a um objeto ou função.

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

Se o objeto ao qual o lvalue se refere não for um objeto do tipo T e não for um objeto de um tipo derivado de T ou se o objeto não for inicializado, um programa que necessite dessa conversão terá um comportamento indefinido.

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, se thisfor nulo, independentemente do conteúdo da função, o resultado será indefinido.

Isto segue do §5.2.5 / 3:

Se E1tiver o tipo "ponteiro para a classe X", a expressão E1->E2será convertida para a forma equivalente(*(E1)).E2;

*(E1)resultará em comportamento indefinido com uma interpretação estrita e o .E2converterá 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):

Se uma função de membro não estática de uma classe X for chamada para um objeto que não seja do tipo X ou de um tipo derivado de X, o comportamento será indefinido.


Com funções estáticas, a interpretação estrita versus fraca faz a diferença. A rigor, é indefinido:

Um membro estático pode ser referido usando a sintaxe de acesso de membro da classe; nesse caso, a expressão do objeto é avaliada.

Ou seja, é avaliado como se não fosse estático e, mais uma vez, desreferenciamos um ponteiro nulo (*(E1)).E2.

No entanto, como E1nã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.

GManNickG
fonte
5
+1. Continuando o pedantismo, sob a "definição fraca", a função membro não estática não foi chamada "para um objeto que não é do tipo X". Foi chamado para um lvalue que não é um objeto. Portanto, a solução proposta adiciona o texto "ou se o lvalue for um lvalue vazio" à cláusula citada.
Steve Jessop
Você poderia esclarecer um pouco? Em particular, com os links "problema encerrado" e "problema ativo", quais são os números dos problemas? Além disso, se esse é um problema fechado, qual é exatamente a resposta sim / não para funções estáticas? Sinto que estou perdendo o passo final na tentativa de entender sua resposta.
Brooks Moses
4
Não acho que o defeito 315 do CWG esteja tão "fechado" quanto a presença na página "problemas fechados". A lógica diz que isso deve ser permitido porque " *pnão é um erro quando pé 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.
James McNellis
1
@JamesMcNellis: Pelo meu entendimento, se pfosse um endereço de hardware que desencadearia alguma ação quando lida, mas não fosse declarada volatile, 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 *pfosse volatile, 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.
Supercat
1
".E2 converte-o em um rvalue" - - Uh, não, não é
MM
30

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

Mark Ransom
fonte
1
GetSafeHwnd primeiro faz uma verificação! E, se verdadeiro, retorna NULL. Em seguida, inicia um quadro SEH e desreferencia o ponteiro. se houver violação de acesso à memória (0xc0000005), isso será capturado e NULL será retornado ao chamador :) Caso contrário, o HWND será retornado.
Петър Петров
@ Se já faz alguns anos desde que eu olhei o código 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!
Mark Ransom
Eu estou declarando uma amostra possível implementação que têm o mesmo efeito, o que isso realmente fazer é ser engenharia reversa usando um depurador :)
Петър Петров
1
"eles têm conhecimento interno sobre o funcionamento do compilador!" - a causa do eterno problema para projetos como o MinGW que tentam permitir que o g ++ compile código que chama a API do Windows
MM
@ MM Acho que todos concordamos que isso é injusto. E por causa disso, também acho que há uma lei sobre compatibilidade que torna um pouco ilegal mantê-lo assim.
v.oddou