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

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

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

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
110
0
100 144
8
Перейти к ответу Данный вопрос помечен как решенный

Ответы 8

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

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

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

Это не ошибка компилятора, если вы это имеете в виду.

Thomas 19.09.2008 08:20

Ваше подозрение верно - C# и Java это позволяют. На этих языках у строящихся проектов есть свой окончательный тип. В C++ объекты меняют тип во время создания, поэтому и когда у вас могут быть объекты с абстрактным типом.

MSalters 19.09.2008 14:42
ВСЕ abstract classes, and real objects created derived from them, need a vtbl (virtual function table), listing which virtual functions should be called on it. In C++ an object is responsible for creating its own members, including the virtual function table. Constructors are called from base class to derived, and destructors are called from derived to base class, so in an abstract base class the virtual function table is not yet available.
fuzzyTew 17.07.2009 15:00
Ответ принят как подходящий

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

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

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
}

Есть ли причина, по которой компилятор не мог это уловить?

Thomas 19.09.2008 08:15

Я не вижу никаких технических причин, по которым компилятор не смог это уловить.

Brian R. Bondy 19.09.2008 08:16

GCC дает мне только предупреждение: test.cpp: В конструкторе 'Base :: Base ()': test.cpp: 4: warning: abstract virtual 'virtual void Base :: doIt ()' вызывается из конструктора Но он не работает по ссылке время.

Thomas 19.09.2008 08:17

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

1800 INFORMATION 19.09.2008 08:19

vC++ также не работает во время компоновки ...> test.obj: ошибка LNK2001: неразрешенный внешний символ "public: virtual void __thiscall Base :: doIt (void)" (? doIt @ Base @@ UAEXXZ)

Brian R. Bondy 19.09.2008 08:19

В общем случае не могу его поймать, так как поток от ctor может идти куда угодно и куда угодно может вызывать чистую виртуальную функцию. Это проблема остановки 101.

shoosh 19.09.2008 08:20

Ответ немного неверен: чистая виртуальная функция все еще может быть определена, подробности см. В Википедии. Правильная формулировка: мог бы не существует

MSalters 19.09.2008 14:46

Я думаю, что этот пример слишком упрощен: вызов doIt() в конструкторе легко девиртуализируется и статически отправляется на Base::doIt(), что вызывает ошибку компоновщика. Что нам действительно нужно, так это ситуация, в которой динамический тип во время динамической отправки является абстрактным базовым типом.

Kerrek SB 05.04.2012 13:41

Это можно запустить с помощью MSVC, если вы добавите дополнительный уровень косвенности: пусть Base::Base вызывает невиртуальный f(), который, в свою очередь, вызывает (чистый) виртуальный метод doIt.

Frerich Raabe 05.03.2014 18:20

Когда это аварийное завершение произошло со мной, я отследил его с помощью вложенных деструкторов / удалений, пока не нашел нереализованный деструктор.

Paulo Carvalho 08.08.2016 17:08

Дополняя свой предыдущий комментарий: я действительно нашел двойное освобождение в деструкторе суперкласса, который не является чисто виртуальным. В этом случае сообщение об ошибке времени выполнения вводило в заблуждение (компилятор MinGW).

Paulo Carvalho 08.08.2016 17:18

Совершенно нормально вызывать нечистые виртуальные функции из конструкторов. Стандарт явно позволяет это. Конечно, вызывать чистую виртуальную функцию нельзя (это не имеет смысла).

rustyx 08.09.2016 14:43

Я просто скопировал код в VS2015, и я не получаю никаких ошибок ни в Release, ни в Debug.

Romeno 16.04.2017 17:46

Кроме того, я попытался собрать тот же код, используя cygwin gcc в Windows, и он не разбился и полностью съел инструкцию Derived d;.

Romeno 16.04.2017 18:26

@Romeno: Undefined Behavior - вызвать виртуальную функцию чистого члена (прямо или косвенно) из конструктора или деструктора абстрактного класса. И «сбои с ошибкой« вызова чистой виртуальной функции »», и «отсутствие наблюдаемого эффекта» являются допустимым поведением для компилятора в этом сценарии именно потому, что поведение не определено стандартом языка.

Adam Rosenfield 19.04.2017 21:03

@AdamRosenfield Меня интересует, как это точно указано, можно процитировать или дать ссылку на место / фразу в стандарте, спасибо?

Romeno 20.04.2017 15:45

@Romeno: §10.4 / 6 [class.abstract] в стандартах языка C++ 03 или C++ 11: «Функции-члены могут быть вызваны из конструктора (или деструктора) абстрактного класса; эффект создания виртуальный вызов (10.3) чистой виртуальной функции прямо или косвенно для объекта, создаваемого (или уничтожаемого) из такого конструктора (или деструктора), не определен ».

Adam Rosenfield 21.04.2017 00:09

Как бы вы тогда вызывали производный переопределенный метод из базового конструктора?

Post Self 10.05.2018 11:39

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

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

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

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();

По крайней мере, это не может быть воспроизведено на моем vc2008, vptr действительно указывает на vtable A при первой инициализации в конструкторе A, но затем, когда B полностью инициализирован, vptr изменяется, чтобы указывать на vtable B, что нормально

baye 07.03.2010 05:45

не смог воспроизвести его ни с vs2010 / 12

makc 06.01.2013 13:11
I had this essentially happen to me today obviously not true, because simply wrong: a pure virtual function is called only when callFoo() is called within a constructor (or destructor), because at this time the object is still (or already) at A stage. Вот работающая версия of your code without the syntax error in B b(); - the parentheses make it a function declaration, you want an object.
Wolf 11.06.2015 14:26

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

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

int __cdecl _purecall(void)

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

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

Спасибо за указатель на получение вызова _purecall () на удаленном экземпляре; Я не знал об этом, но просто доказал это себе с помощью небольшого тестового кода. Глядя на посмертный дамп в WinDbg, я думал, что имею дело с гонкой, когда другой поток пытался использовать производный объект до того, как он был полностью построен, но это проливает новый свет на проблему и, кажется, лучше соответствует свидетельствам.

Dave Ruske 06.03.2015 19:37

Еще кое-что я добавлю: вызов _purecall(), который обычно происходит при вызове метода удаленного экземпляра, будет нет, если базовый класс был объявлен с оптимизацией __declspec(novtable) (специфично для Microsoft). При этом вполне возможно вызвать переопределенный виртуальный метод после удаления объекта, что может замаскировать проблему, пока она не укусит вас в какой-либо другой форме. Ловушка _purecall() - ваш друг!

Dave Ruske 06.03.2015 19:52

Это полезно знать, Дэйв, недавно я видел несколько ситуаций, когда я не получал чистых звонков, когда думал, что должен. Возможно, мне не нравилась эта оптимизация.

Len Holgate 06.03.2015 21:22

@LenHolgate: Чрезвычайно ценный ответ. Это ИМЕННО наш проблемный случай (неправильный счетчик ссылок, вызванный условиями гонки). Большое спасибо за то, что указали нам в правильном направлении (вместо этого мы подозревали повреждение v-таблицы и сходили с ума, пытаясь найти код виновника)

BlueStrat 08.03.2016 02:41

Я использую 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

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

Другие вопросы по теме