Звідки беруться збої "чистого віртуального виклику функції"?


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
Будь-яка причина, чому компілятор не міг цього зрозуміти?
Томас

21
У загальному випадку це не вдається наздогнати, оскільки потік з ctor може йти куди завгодно, і будь-де можна викликати чисту віртуальну функцію. Це проблема зупинки 101.
shoosh

9
Відповідь трохи невірний: чиста віртуальна функція все ще може бути визначена, детальну інформацію див. У Вікіпедії. Правильне фразування: можливо, не існує
MSalters,

5
Я вважаю, що цей приклад є надто спрощеним: doIt()виклик у конструкторі легко деартуалізується та відправляється Base::doIt()статично, що просто спричиняє помилку лінкера. Нам дійсно потрібна ситуація, коли динамічний тип під час динамічної відправки є абстрактним базовим типом.
Керрек СБ

2
Це може бути спровоковано MSVC, якщо ви додасте додатковий рівень непрямості: Base::Baseвикличте невіртуальний, f()який у свою чергу викликає (чистий) віртуальний doItметод.
Фріріх Раабе

64

Крім стандартного випадку виклику віртуальної функції від конструктора або деструктора об'єкта з чисто віртуальними функціями, ви також можете отримати виклик чистої віртуальної функції (принаймні в MSVC), якщо ви викликаєте віртуальну функцію після знищення об'єкта. . Очевидно, що це дуже погано, щоб спробувати зробити, але якщо ви працюєте з абстрактними класами як інтерфейсами, і ви заплутаєтесь, то це щось, що ви можете побачити. Це, мабуть, більш ймовірно, якщо ви використовуєте посилання, що підраховуються, і у вас є помилка підрахунку посилань або якщо у вас багатопотокова програма має стан гонки використання об'єкта / знищення об'єкта ... Річ у цих видах purecall полягає в тому, що це часто менш легко зрозуміти, що відбувається, як перевірка на наявність "звичайних підозрюваних" віртуальних дзвінків в ctor і dtor вийде чистою.

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

int __cdecl _purecall(void)

і пов'язати його перед тим, як зв’язати бібліотеку виконання. Це дає ВАС контроль над тим, що відбувається при виявленні чистого дзвінка. Після контролю ви можете зробити щось більш корисне, ніж стандартний обробник. У мене є обробник, який може надати стек прослідкування того, де сталося purecall; дивіться тут: 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

Зазвичай, коли ви викликаєте віртуальну функцію через звисаючий покажчик - швидше за все, примірник вже знищений.

Можуть бути і більш "творчі" причини: можливо, вам вдалося відрізати частину вашого об'єкта, де реалізована віртуальна функція. Але зазвичай це просто те, що екземпляр вже знищений.


4

Я зіткнувся зі сценарієм, що чисті віртуальні функції викликаються через знищені об'єкти, Len Holgateвже є дуже приємна відповідь , я хотів би додати трохи прикладу:

  1. Створений об’єкт створений, а вказівник (як клас Base) зберігається десь
  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, створений для абстрактного класу з якихось внутрішніх причин (він може знадобитися для якоїсь інформації про тип часу запуску), і щось йде не так, і реальний об'єкт отримує це. Це помилка. Тільки одне повинно сказати, що щось не може трапитися.

Чисті міркування

редагувати: схоже, я помиляюся у розглянутому випадку. Деякі мови 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();

1
Принаймні, це не може бути відтворено на моєму vc2008, vptr дійсно вказує на vtable A при першому ініціалізації в кондукторі A, але тоді, коли B повністю ініціалізовано, vptr змінюється, щоб вказувати на
vtable V

відтворити його можна з vs2010 / 12
macc

I had this essentially happen to me todayочевидно, що не відповідає дійсності, тому що просто неправильно: чиста віртуальна функція викликається лише тоді, коли callFoo()викликається в конструкторі (або деструкторі), тому що в цей час об'єкт все ще (або вже є) на стадії A. Ось запущена версія вашого коду без синтаксичної помилки в B b();- круглі дужки роблять це декларацією функції, ви хочете об'єкт.
Вовк
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.