Откуда берутся сбои «вызова чисто виртуальной функции»?

106

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

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

Брайан Р. Бонди
источник

Ответы:

107

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

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

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
}
Адам Розенфилд
источник
3
По какой причине компилятор не смог это уловить?
Thomas
21
В общем случае его не уловить, так как поток от ctor может идти куда угодно и куда угодно может вызывать чистую виртуальную функцию. Это проблема остановки 101.
shoosh
9
Ответ немного неверен: чистая виртуальная функция все еще может быть определена, подробности см. В Википедии. Правильная формулировка: может не существовать
MSalters
5
Я думаю, что этот пример слишком упрощен: doIt()вызов в конструкторе легко девиртуализируется и передается Base::doIt()статически, что вызывает ошибку компоновщика. Что нам действительно нужно, так это ситуация, в которой динамический тип во время динамической отправки является абстрактным базовым типом.
Kerrek SB
2
Это можно запустить с помощью MSVC, если вы добавите дополнительный уровень косвенности: Base::Baseвызовите не виртуальный, f()который, в свою очередь, вызывает (чистый) виртуальный doItметод.
Frerich Raabe 05
64

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

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

int __cdecl _purecall(void)

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

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

Лен Холгейт
источник
1
Спасибо за указатель на получение вызова _purecall () на удаленном экземпляре; Я не знал об этом, но просто доказал это себе с помощью небольшого тестового кода. Глядя на посмертный дамп в WinDbg, я думал, что имею дело с гонкой, когда другой поток пытался использовать производный объект до того, как он был полностью построен, но это проливает новый свет на проблему и, кажется, лучше соответствует свидетельствам.
Дэйв Руск,
1
Еще кое-что я добавлю: _purecall()вызов, который обычно происходит при вызове метода удаленного экземпляра, не произойдет, если базовый класс был объявлен с __declspec(novtable)оптимизацией (специфично для Microsoft). При этом вполне возможно вызвать переопределенный виртуальный метод после удаления объекта, что может замаскировать проблему, пока она не укусит вас в какой-либо другой форме. _purecall()Ловушка является вашим другом!
Дэйв Руск,
Это полезно знать, Дэйв, недавно я видел несколько ситуаций, когда я не получал чистых звонков, когда думал, что должен. Возможно, мне не нравилась эта оптимизация.
Лен Холгейт
1
@LenHolgate: Чрезвычайно ценный ответ. Это ИМЕННО наш проблемный случай (неправильный счетчик ссылок, вызванный условиями гонки). Большое спасибо за то, что указали нам правильное направление (вместо этого мы подозревали повреждение v-table и сходили с ума, пытаясь найти виновный код)
BlueStrat
7

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

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

Braden
источник
4

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

Байян Хуанг
источник
0

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

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

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

БКС
источник
Это не ошибка компилятора, если вы это имеете в виду.
Томас
Ваше подозрение верно - C # и Java это позволяют. На этих языках у строящихся проектов есть свой окончательный тип. В C ++ объекты меняют тип во время построения, поэтому и когда у вас могут быть объекты с абстрактным типом.
MSalters
ВСЕ абстрактные классы и реальные объекты, созданные на их основе, нуждаются в vtbl (таблице виртуальных функций), в которой перечислены виртуальные функции, которые должны быть вызваны в ней. В C ++ объект отвечает за создание своих членов, включая таблицу виртуальных функций. Конструкторы вызываются из базового класса в производный, а деструкторы вызываются из производного класса в базовый, поэтому в абстрактном базовом классе таблица виртуальных функций еще недоступна.
fuzzyTew
0

Я использую 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 */
};
Дэвид Ли
источник
0

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

Ники
источник
-2

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

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 ИНФОРМАЦИЯ
источник
1
По крайней мере, это не может быть воспроизведено на моем vc2008, vptr действительно указывает на vtable A при первой инициализации в конструкторе A, но затем, когда B полностью инициализирован, vptr изменяется, чтобы указывать на vtable B, что нормально
Байян Хуанг
не могу воспроизвести это либо с vs2010 / 12
makc
I had this essentially happen to me todayочевидно, неверно, потому что просто неправильно: чистая виртуальная функция вызывается только тогда, когда callFoo()вызывается в конструкторе (или деструкторе), потому что в это время объект все еще (или уже) находится на стадии A. Вот работающая версия вашего кода без синтаксической ошибки B b();- круглые скобки означают объявление функции, вам нужен объект.
Wolf