Я іноді помічаю програми, які виходять з ладу на моєму комп’ютері з помилкою: "чистий виклик віртуальної функції".
Як ці програми навіть компілюються, коли об’єкт не може бути створений абстрактним класом?
Я іноді помічаю програми, які виходять з ладу на моєму комп’ютері з помилкою: "чистий виклик віртуальної функції".
Як ці програми навіть компілюються, коли об’єкт не може бути створений абстрактним класом?
Відповіді:
Вони можуть мати результат, якщо ви спробуєте здійснити виклик віртуальної функції від конструктора чи деструктора. Оскільки ви не можете зробити виклик віртуальної функції від конструктора чи деструктора (похідний об'єкт класу не побудований або вже знищений), він викликає версію базового класу, яка у випадку чистої віртуальної функції не робить не існує.
(Дивіться демо-версію тут )
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), якщо ви викликаєте віртуальну функцію після знищення об'єкта. . Очевидно, що це дуже погано, щоб спробувати зробити, але якщо ви працюєте з абстрактними класами як інтерфейсами, і ви заплутаєтесь, то це щось, що ви можете побачити. Це, мабуть, більш ймовірно, якщо ви використовуєте посилання, що підраховуються, і у вас є помилка підрахунку посилань або якщо у вас багатопотокова програма має стан гонки використання об'єкта / знищення об'єкта ... Річ у цих видах purecall полягає в тому, що це часто менш легко зрозуміти, що відбувається, як перевірка на наявність "звичайних підозрюваних" віртуальних дзвінків в ctor і dtor вийде чистою.
Щоб допомогти з налагодженням подібних проблем, ви можете в різних версіях MSVC замінити обробник purecall бібліотеки часу виконання. Ви робите це, надаючи власну функцію цим підписом:
int __cdecl _purecall(void)
і пов'язати його перед тим, як зв’язати бібліотеку виконання. Це дає ВАС контроль над тим, що відбувається при виявленні чистого дзвінка. Після контролю ви можете зробити щось більш корисне, ніж стандартний обробник. У мене є обробник, який може надати стек прослідкування того, де сталося purecall; дивіться тут: 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, створений для абстрактного класу з якихось внутрішніх причин (він може знадобитися для якоїсь інформації про тип часу запуску), і щось йде не так, і реальний об'єкт отримує це. Це помилка. Тільки одне повинно сказати, що щось не може трапитися.
Чисті міркування
редагувати: схоже, я помиляюся у розглянутому випадку. Деякі мови 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();
- круглі дужки роблять це декларацією функції, ви хочете об'єкт.