Я иногда замечаю программы, которые вылетают на моем компьютере с ошибкой: «вызов чистой виртуальной функции».
Как эти программы вообще компилируются, если объект не может быть создан из абстрактного класса?
Я иногда замечаю программы, которые вылетают на моем компьютере с ошибкой: «вызов чистой виртуальной функции».
Как эти программы вообще компилируются, если объект не может быть создан из абстрактного класса?
Ответы:
Они могут возникнуть, если вы попытаетесь вызвать виртуальную функцию из конструктора или деструктора. Поскольку вы не можете вызвать виртуальную функцию из конструктора или деструктора (объект производного класса не был создан или уже был уничтожен), он вызывает версию базового класса, которая в случае чистой виртуальной функции не не существует.
(Смотрите живую демонстрацию здесь )
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
}
doIt()
вызов в конструкторе легко девиртуализируется и передается Base::doIt()
статически, что вызывает ошибку компоновщика. Что нам действительно нужно, так это ситуация, в которой динамический тип во время динамической отправки является абстрактным базовым типом.
Base::Base
вызовите не виртуальный, f()
который, в свою очередь, вызывает (чистый) виртуальный doIt
метод.
Помимо стандартного случая вызова виртуальной функции из конструктора или деструктора объекта с чистыми виртуальными функциями, вы также можете получить вызов чистой виртуальной функции (по крайней мере, на MSVC), если вы вызываете виртуальную функцию после того, как объект был уничтожен. . Очевидно, это довольно плохая идея, но если вы работаете с абстрактными классами в качестве интерфейсов и ошибаетесь, то вы можете это увидеть. Вероятно, это более вероятно, если вы используете интерфейсы с подсчетом ссылок и у вас есть ошибка подсчета ссылок или если у вас есть состояние гонки использования / уничтожения объекта в многопоточной программе ... Суть этих видов чистых вызовов заключается в том, что они Часто бывает труднее понять, что происходит, так как проверка «обычных подозреваемых» виртуальных вызовов в ctor и dtor оказывается чистой.
Чтобы помочь с отладкой такого рода проблем, вы можете в различных версиях MSVC заменить обработчик purecall библиотеки времени выполнения. Вы делаете это, предоставляя свою собственную функцию с этой подписью:
int __cdecl _purecall(void)
и связать его перед тем, как связать библиотеку времени выполнения. Это дает ВАМ контроль над тем, что происходит при обнаружении чистого вызова. Получив контроль, вы можете делать что-то более полезное, чем стандартный обработчик. У меня есть обработчик, который может предоставить трассировку стека того, где произошел чистый вызов; посмотреть здесь: http://www.lenholgate.com/blog/2006/01/purecall.html для получения дополнительных сведений.
(Обратите внимание, что вы также можете вызвать _set_purecall_handler (), чтобы установить обработчик в некоторых версиях MSVC).
_purecall()
вызов, который обычно происходит при вызове метода удаленного экземпляра, не произойдет, если базовый класс был объявлен с __declspec(novtable)
оптимизацией (специфично для Microsoft). При этом вполне возможно вызвать переопределенный виртуальный метод после удаления объекта, что может замаскировать проблему, пока она не укусит вас в какой-либо другой форме. _purecall()
Ловушка является вашим другом!
Обычно, когда вы вызываете виртуальную функцию через висящий указатель - скорее всего, экземпляр уже был уничтожен.
Могут быть и более «творческие» причины: возможно, вам удалось отрезать часть вашего объекта, где была реализована виртуальная функция. Но обычно просто экземпляр уже уничтожен.
Я столкнулся со сценарием, когда чистые виртуальные функции вызываются из-за уничтоженных объектов, Len Holgate
уже есть очень хороший ответ , я хотел бы добавить немного цвета с примером:
Деструктор производного класса сбрасывает точки 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
поскольку память вернулась в операционную систему, и программа просто не может получить к ней доступ. Таким образом, этот сценарий «вызова чистой виртуальной функции» обычно происходит, когда объект выделяется в пуле памяти, в то время как объект удаляется, базовая память фактически не используется ОС, она все еще доступна для процесса.
Я предполагаю, что для абстрактного класса по какой-то внутренней причине создан vtbl (он может понадобиться для какой-то информации о типе времени выполнения), и что-то идет не так, и реальный объект получает это. Это ошибка. Уже одно это должно сказать, что то, чего не может случиться, есть.
Чистая спекуляция
edit: похоже, что я ошибаюсь в рассматриваемом случае. 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
Вот хитрый способ, чтобы это произошло. По сути, это случилось со мной сегодня.
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();
I had this essentially happen to me today
очевидно, неверно, потому что просто неправильно: чистая виртуальная функция вызывается только тогда, когда callFoo()
вызывается в конструкторе (или деструкторе), потому что в это время объект все еще (или уже) находится на стадии A. Вот работающая версия вашего кода без синтаксической ошибки B b();
- круглые скобки означают объявление функции, вам нужен объект.