Откуда происходят "чисто виртуальные вызовы функций"?

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

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

7 ответов

Решение

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

(Смотрите демо-версию здесь)

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), если вы вызываете виртуальную функцию после уничтожения объекта, Очевидно, что это довольно неудачная попытка, но если вы работаете с абстрактными классами в качестве интерфейсов, и вы все перепутали, то это то, что вы можете увидеть. Вероятно, это более вероятно, если вы используете подсчитанные интерфейсы, на которые ссылаются, и у вас есть ошибка подсчета ссылок или если у вас есть условие гонки объекта / уничтожения объекта в многопоточной программе... Суть этих видов чистого вызова в том, что это часто не так просто понять, что происходит, поскольку проверка "обычных подозреваемых" виртуальных вызовов в ctor и dtor будет чистой.

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

int __cdecl _purecall(void)

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

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

Я столкнулся со сценарием, когда чистые виртуальные функции вызываются из-за уничтоженных объектов, 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поскольку память вернулась в операционную систему, и программа просто не может получить к ней доступ. Таким образом, этот сценарий "вызова чистой виртуальной функции" обычно происходит, когда объект выделяется в пуле памяти, в то время как объект удаляется, базовая память фактически не используется ОС, она все еще доступна для процесса.

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

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

Если вы используете 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

Я предполагаю, что существует 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 */
};

Вот хитрый способ, чтобы это произошло. У меня было это по сути со мной сегодня.

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();
Другие вопросы по тегам