Откуда берутся сбои при “чисто виртуальном вызове функции”?

StackOverflow https://stackoverflow.com/questions/99552

Вопрос

Иногда я замечаю программы, которые выходят из строя на моем компьютере с ошибкой:"чистый вызов виртуальной функции".

Как эти программы вообще компилируются, если объект не может быть создан из абстрактного класса?

Это было полезно?

Решение

Они могут возникнуть, если вы попытаетесь вызвать виртуальную функцию из конструктора или деструктора.Поскольку вы не можете выполнить вызов виртуальной функции из конструктора или деструктора (объект производного класса не был создан или уже был уничтожен), он вызывает версию базового класса, которая в случае чисто виртуальной функции не существует.

(Смотрите живую демонстрацию здесь)

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
}

Другие советы

Помимо стандартного случая вызова виртуальной функции из конструктора или деструктора объекта с помощью чисто виртуальных функций, вы также можете получить чисто виртуальный вызов функции (по крайней мере, в MSVC), если вы вызываете виртуальную функцию после уничтожения объекта.Очевидно, что пытаться делать это довольно плохо, но если вы работаете с абстрактными классами в качестве интерфейсов и у вас что-то не получается, то это то, что вы можете увидеть.Возможно, это более вероятно, если вы используете интерфейсы с подсчетом ссылок и у вас ошибка с подсчетом ссылок или если у вас есть условие гонки использования объекта / уничтожения объекта в многопоточной программе...Особенность такого рода purecall в том, что часто бывает не так просто понять, что происходит, поскольку проверка на наличие "обычных подозреваемых" в виртуальных вызовах в ctor и dtor даст чистый результат.

Чтобы помочь в отладке подобных проблем, вы можете в различных версиях MSVC заменить обработчик purecall библиотеки времени выполнения.Вы делаете это, предоставляя своей собственной функции эту подпись:

int __cdecl _purecall(void)

и свяжите его перед тем, как вы свяжете библиотеку среды выполнения.Это дает ВАМ контроль над тем, что происходит при обнаружении purecall.Получив контроль, вы можете сделать что-то более полезное, чем стандартный обработчик.У меня есть обработчик, который может предоставить трассировку стека того места, где произошел purecall;смотрите здесь: http://www.lenholgate.com/blog/2006/01/purecall.html для получения более подробной информации.

(Обратите внимание, что вы также можете вызвать _set_purecall_handler() для установки вашего обработчика в некоторых версиях MSVC).

Обычно, когда вы вызываете виртуальную функцию через висячий указатель, скорее всего, экземпляр уже был уничтожен.

Могут быть и более "творческие" причины:возможно, вам удалось отрезать ту часть вашего объекта, где была реализована виртуальная функция.Но обычно дело просто в том, что экземпляр уже был уничтожен.

Я бы предположил, что есть vtbl, созданный для абстрактного класса по какой-то внутренней причине (это может понадобиться для какой-то информации о типе времени выполнения), и что-то идет не так, и реальный объект получает это.Это ошибка.Одно это должно говорить о том, что происходит нечто такое, чего произойти не может.

Чистая спекуляция

Редактировать: похоже, в данном случае я ошибаюсь.OTOH IIRC некоторые языки допускают вызовы vtbl из конструктора-деструктора.

Я использую VS2010, и всякий раз, когда я пытаюсь вызвать деструктор непосредственно из общедоступного метода, я получаю ошибку "чисто виртуального вызова функции" во время выполнения.

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

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

Поэтому я переместил то, что находится внутри ~Foo(), в отдельный закрытый метод, и тогда это сработало как по волшебству.

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

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

Если вы используете Borland / CodeGear / Embarcadero / Idera C ++ Builder, вы можете просто реализовать

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

Во время отладки поместите точку останова в код и просмотрите стек вызовов в IDE, в противном случае зарегистрируйте стек вызовов в вашем обработчике исключений (или этой функции), если у вас есть для этого соответствующие инструменты.Лично я использую MadExcept для этого.

PS.Исходный вызов функции находится в [C ++ Builder]\source\cpprtl\Source\misc\pureerr.cpp

Я столкнулся со сценарием, когда чисто виртуальные функции вызываются из-за уничтоженных объектов, Len Holgate у меня уже есть очень хороший ответ, я хотел бы добавить немного цвета с примером:

  1. Создается производный объект, а указатель (как базовый класс) где-то сохраняется
  2. Производный объект удаляется, но почему-то указатель еще ссылка
  3. Вызывается указатель, указывающий на удаленный производный объект

Деструктор производного класса сбрасывает vptr, указывающий на базовый класс vtable, который имеет чисто виртуальную функцию, поэтому, когда мы вызываем виртуальную функцию, она фактически вызывает чисто виртуальные функции.

Это может произойти из-за очевидной ошибки кода или сложного сценария состояния гонки в многопоточных средах.

Вот простой пример (компиляция g ++ с отключенной оптимизацией - простая программа может быть легко оптимизирована):

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

И трассировка стека выглядит следующим образом:

#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

Выделите:

если объект полностью удален, то есть вызывается деструктор, а memroy восстанавливается, мы можем просто получить Segmentation fault поскольку память вернулась в операционную систему, и программа просто не может получить к ней доступ.Таким образом, этот сценарий "чисто виртуального вызова функции" обычно происходит, когда объект выделяется в пуле памяти, в то время как объект удаляется, базовая память фактически не восстанавливается операционной системой, она все еще доступна процессу.

Вот хитрый способ, как это может произойти.По сути, это случилось со мной сегодня.

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();
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top