Я иногда замечаю программы, которые сбой на моем компьютере с ошибкой: "чистый вызов виртуальной функции".
Как эти программы даже компилируются, когда объект не может быть создан из абстрактного класса?
Я иногда замечаю программы, которые сбой на моем компьютере с ошибкой: "чистый вызов виртуальной функции".
Как эти программы даже компилируются, когда объект не может быть создан из абстрактного класса?
Они могут возникнуть, если вы попытаетесь сделать вызов виртуальной функции от конструктора или деструктора. Поскольку вы не можете сделать вызов виртуальной функции от конструктора или деструктора (объект производного класса не был создан или уже был уничтожен), он вызывает версию базового класса, которая в случае чистой виртуальной функции doesn 't существует.
(см. живое демо здесь)
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
уже есть очень хороший ответ, я хотел бы
добавить немного цвета с примером:
Деструктор производного класса сбрасывает точки 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 ([email protected]=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
Highlight:
если объект полностью удален, что означает, что вызывается деструктор, а память восстанавливается, мы можем просто получить 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();