De onde vêm os travamentos de “chamada de função virtual pura”?

106

Às vezes, noto programas que travam no meu computador com o erro: "chamada de função virtual pura".

Como esses programas compilam quando um objeto não pode ser criado de uma classe abstrata?

Brian R. Bondy
fonte

Respostas:

107

Eles podem resultar se você tentar fazer uma chamada de função virtual de um construtor ou destruidor. Uma vez que você não pode fazer uma chamada de função virtual de um construtor ou destruidor (o objeto da classe derivada não foi construído ou já foi destruído), ele chama a versão da classe base, que no caso de uma função virtual pura, não não existe.

(Veja a demonstração ao vivo aqui )

class Base
{
public:
    Base() { doIt(); }  // DON'T DO THIS
    virtual void doIt() = 0;
};

void Base::doIt()
{
    std::cout<<"Is it fine to call pure virtual function from constructor?";
}

class Derived : public Base
{
    void doIt() {}
};

int main(void)
{
    Derived d;  // This will cause "pure virtual function call" error
}
Adam Rosenfield
fonte
3
Alguma razão pela qual o compilador não conseguiu pegar isso, em geral?
Thomas,
21
No caso geral, não pode pegá-lo, pois o fluxo do ctor pode ir a qualquer lugar e qualquer lugar pode chamar a função virtual pura. Este é o problema de Halting 101.
shoosh
9
A resposta está um pouco errada: uma função virtual pura ainda pode ser definida, consulte Wikipedia para detalhes.
Frase
5
Acho que este exemplo é muito simplista: a doIt()chamada no construtor é facilmente desvirtualizada e enviada para Base::doIt()estaticamente, o que apenas causa um erro de vinculador. O que realmente precisamos é uma situação em que o tipo dinâmico durante um despacho dinâmico seja o tipo de base abstrata.
Kerrek SB
2
Isso pode ser acionado com MSVC se você adicionar um nível extra de indireção: Base::Basechamar um não virtual f()que, por sua vez, chama o doItmétodo virtual (puro) .
Frerich Raabe
64

Assim como o caso padrão de chamar uma função virtual do construtor ou destruidor de um objeto com funções virtuais puras, você também pode obter uma chamada de função virtual pura (pelo menos no MSVC) se chamar uma função virtual depois que o objeto foi destruído . Obviamente, isso é uma coisa muito ruim de se tentar, mas se você estiver trabalhando com classes abstratas como interfaces e bagunçar, então é algo que você pode ver. É possivelmente mais provável se você estiver usando interfaces de contagem referenciadas e tiver um bug de contagem de referência ou se tiver uma condição de corrida de uso / destruição de objeto em um programa multi-threaded ... O que há sobre esse tipo de chamada é que é muitas vezes é menos fácil entender o que está acontecendo, já que uma verificação dos 'suspeitos usuais' de chamadas virtuais em ctor e dtor aparecerá limpa.

Para ajudar a depurar esses tipos de problemas, você pode, em várias versões do MSVC, substituir o manipulador purecall da biblioteca de tempo de execução. Você faz isso fornecendo sua própria função com esta assinatura:

int __cdecl _purecall(void)

e vinculá-lo antes de vincular a biblioteca de tempo de execução. Isso dá a VOCÊ o controle do que acontece quando uma chamada direta é detectada. Depois de ter controle, você pode fazer algo mais útil do que o manipulador padrão. Eu tenho um manipulador que pode fornecer um rastreamento de pilha de onde a purecall aconteceu; veja aqui: http://www.lenholgate.com/blog/2006/01/purecall.html para mais detalhes.

(Observe que você também pode chamar _set_purecall_handler () para instalar seu manipulador em algumas versões do MSVC).

Len Holgate
fonte
1
Obrigado pela indicação sobre como obter uma invocação _purecall () em uma instância excluída; Eu não estava ciente disso, mas apenas provei para mim mesmo com um pequeno código de teste. Olhando para um despejo post-mortem no WinDbg, pensei que estava lidando com uma corrida em que outro thread estava tentando usar um objeto derivado antes de ter sido totalmente construído, mas isso traz uma nova luz sobre o problema e parece se adequar melhor às evidências.
Dave Ruske
1
Acrescentarei outra coisa: a _purecall()invocação que normalmente ocorre ao chamar um método de uma instância excluída não acontecerá se a classe base tiver sido declarada com a __declspec(novtable)otimização (específico da Microsoft). Com isso, é inteiramente possível chamar um método virtual sobrescrito após o objeto ter sido excluído, o que poderia mascarar o problema até que ele o morda de alguma outra forma. A _purecall()armadilha é sua amiga!
Dave Ruske
É útil conhecer Dave. Recentemente, vi algumas situações em que não estava recebendo ligações quando pensei que deveria. Talvez eu estivesse caindo em conflito com essa otimização.
Len Holgate,
1
@LenHolgate: Resposta extremamente valiosa. Este foi EXATAMENTE nosso caso de problema (contagem de referências errada causada por condições de corrida). Muito obrigado por nos apontar na direção certa (estávamos suspeitando de corrupção da tabela v e enlouquecendo ao tentar encontrar o código culpado)
BlueStrat
7

Normalmente, quando você chama uma função virtual por meio de um ponteiro pendente - provavelmente a instância já foi destruída.

Também pode haver razões mais "criativas": talvez você tenha conseguido cortar a parte do seu objeto onde a função virtual foi implementada. Mas geralmente é só que a instância já foi destruída.

Braden
fonte
4

Corri para o cenário que as funções virtuais puras são chamadas por causa de objetos destruídos, Len Holgatejá tenho uma resposta muito boa , gostaria de adicionar algumas cores com um exemplo:

  1. Um objeto derivado é criado, e o ponteiro (como classe base) é salvo em algum lugar
  2. O objeto derivado é excluído, mas de alguma forma o ponteiro ainda é referenciado
  3. O ponteiro que aponta para o objeto Derivado excluído é chamado

O destruidor da classe Derived redefine os pontos vptr para a classe vtable da base, que tem a função virtual pura, portanto, quando chamamos a função virtual, ele realmente chama as virutais puras.

Isso pode acontecer devido a um bug de código óbvio ou a um cenário complicado de condição de corrida em ambientes de multi-threading.

Aqui está um exemplo simples (compilação g ++ com otimização desativada - um programa simples pode ser facilmente otimizado):

 #include <iostream>
 using namespace std;

 char pool[256];

 struct Base
 {
     virtual void foo() = 0;
     virtual ~Base(){};
 };

 struct Derived: public Base
 {
     virtual void foo() override { cout <<"Derived::foo()" << endl;}
 };

 int main()
 {
     auto* pd = new (pool) Derived();
     Base* pb = pd;
     pd->~Derived();
     pb->foo();
 }

E o rastreamento de pilha se parece com:

#0  0x00007ffff7499428 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:54
#1  0x00007ffff749b02a in __GI_abort () at abort.c:89
#2  0x00007ffff7ad78f7 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#3  0x00007ffff7adda46 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#4  0x00007ffff7adda81 in std::terminate() () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#5  0x00007ffff7ade84f in __cxa_pure_virtual () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#6  0x0000000000400f82 in main () at purev.C:22

Realçar:

se o objeto for totalmente excluído, o que significa que o destruidor é chamado e o memroy é recuperado, podemos simplesmente obter um, Segmentation faultpois a memória voltou ao sistema operacional e o programa simplesmente não consegue acessá-lo. Portanto, esse cenário de "chamada de função virtual pura" geralmente ocorre quando o objeto é alocado no pool de memória, enquanto um objeto é excluído, a memória subjacente não é recuperada pelo SO, ela ainda está acessível ao processo.

Baiyan Huang
fonte
0

Eu acho que há um vtbl criado para a classe abstrata por algum motivo interno (pode ser necessário para algum tipo de informação de tipo de tempo de execução) e algo dá errado e um objeto real consegue. É um bug. Só isso já deveria dizer que algo que não pode acontecer é.

Especulação pura

editar: parece que estou errado no caso em questão. OTOH IIRC algumas linguagens permitem chamadas vtbl do destruidor do construtor.

BCS
fonte
Não é um bug no compilador, se é isso que você quer dizer.
Thomas,
Sua suspeita está certa - C # e Java permitem isso. Nessas linguagens, os bohjects em construção têm seu tipo final. Em C ++, os objetos mudam de tipo durante a construção e é por isso que e quando você pode ter objetos com um tipo abstrato.
MSalters
TODAS as classes abstratas e objetos reais criados derivados delas precisam de uma vtbl (tabela de função virtual), listando quais funções virtuais devem ser chamadas nela. Em C ++, um objeto é responsável por criar seus próprios membros, incluindo a tabela de função virtual. Os construtores são chamados da classe base para a derivada e os destruidores são chamados da classe derivada para a classe base, portanto, em uma classe base abstrata, a tabela de função virtual ainda não está disponível.
fuzzyTew
0

Eu uso o VS2010 e sempre que tento chamar o destrutor diretamente do método público, recebo um erro de "chamada de função virtual pura" durante o tempo de execução.

template <typename T>
class Foo {
public:
  Foo<T>() {};
  ~Foo<T>() {};

public:
  void SomeMethod1() { this->~Foo(); }; /* ERROR */
};

Mudei o que está dentro de ~ Foo () para separar o método privado e funcionou perfeitamente.

template <typename T>
class Foo {
public:
  Foo<T>() {};
  ~Foo<T>() {};

public:
  void _MethodThatDestructs() {};
  void SomeMethod1() { this->_MethodThatDestructs(); }; /* OK */
};
David Lee
fonte
0

Se você usa Borland / CodeGear / Embarcadero / Idera C ++ Builder, pode apenas implementar

extern "C" void _RTLENTRY _pure_error_()
{
    //_ErrorExit("Pure virtual function called");
    throw Exception("Pure virtual function called");
}

Durante a depuração, coloque um ponto de interrupção no código e veja a pilha de chamadas no IDE, caso contrário, registre a pilha de chamadas em seu manipulador de exceções (ou essa função) se você tiver as ferramentas apropriadas para isso. Eu pessoalmente uso MadExcept para isso.

PS. A chamada de função original está em [C ++ Builder] \ source \ cpprtl \ Source \ misc \ pureerr.cpp

Niki
fonte
-2

Aqui está uma maneira sorrateira de que isso aconteça. Isso essencialmente aconteceu comigo hoje.

class A
{
  A *pThis;
  public:
  A()
   : pThis(this)
  {
  }

  void callFoo()
  {
    pThis->foo(); // call through the pThis ptr which was initialized in the constructor
  }

  virtual void foo() = 0;
};

class B : public A
{
public:
  virtual void foo()
  {
  }
};

B b();
b.callFoo();
1800 INFORMAÇÕES
fonte
1
Pelo menos não pode ser reproduzido em meu vc2008, o vptr aponta para vtable de A quando inicializado pela primeira vez no contrutor de A, mas então quando B é totalmente inicializado, o vptr é alterado para apontar para vtable de B, o que está ok
Baiyan Huang
não foi possível reproduzi-lo com vs2010 / 12
makc
I had this essentially happen to me todayobviamente não é verdade, porque simplesmente errado: uma função virtual pura é chamada apenas quando callFoo()é chamada dentro de um construtor (ou destruidor), porque neste momento o objeto ainda está (ou já) em um estágio. Aqui está uma versão em execução do seu código sem o erro de sintaxe B b();- os parênteses a tornam uma declaração de função, você quer um objeto.
Wolf