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





Я предполагаю, что для абстрактного класса по какой-то внутренней причине создан vtbl (он может понадобиться для какой-то информации о типе времени выполнения), и что-то идет не так, и реальный объект получает это. Это ошибка. Уже одно это должно сказать, что то, чего не может случиться, есть.
Чистая спекуляция
редактировать: похоже, что я ошибаюсь в рассматриваемом случае. OTOH IIRC некоторые языки действительно разрешают вызовы vtbl из деструктора конструктора.
Ваше подозрение верно - C# и Java это позволяют. На этих языках у строящихся проектов есть свой окончательный тип. В C++ объекты меняют тип во время создания, поэтому и когда у вас могут быть объекты с абстрактным типом.
Они могут возникнуть, если вы попытаетесь вызвать виртуальную функцию из конструктора или деструктора. Поскольку вы не можете вызвать виртуальную функцию из конструктора или деструктора (объект производного класса не был создан или уже был уничтожен), он вызывает версию базового класса, которая в случае чистой виртуальной функции не не существует.
(См. Живую демонстрацию здесь)
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
}
Есть ли причина, по которой компилятор не мог это уловить?
Я не вижу никаких технических причин, по которым компилятор не смог это уловить.
GCC дает мне только предупреждение: test.cpp: В конструкторе 'Base :: Base ()': test.cpp: 4: warning: abstract virtual 'virtual void Base :: doIt ()' вызывается из конструктора Но он не работает по ссылке время.
По иронии судьбы, именно это случилось со мной сегодня. Немного более хитрым способом.
vC++ также не работает во время компоновки ...> test.obj: ошибка LNK2001: неразрешенный внешний символ "public: virtual void __thiscall Base :: doIt (void)" (? doIt @ Base @@ UAEXXZ)
В общем случае не могу его поймать, так как поток от ctor может идти куда угодно и куда угодно может вызывать чистую виртуальную функцию. Это проблема остановки 101.
Ответ немного неверен: чистая виртуальная функция все еще может быть определена, подробности см. В Википедии. Правильная формулировка: мог бы не существует
Я думаю, что этот пример слишком упрощен: вызов doIt() в конструкторе легко девиртуализируется и статически отправляется на Base::doIt(), что вызывает ошибку компоновщика. Что нам действительно нужно, так это ситуация, в которой динамический тип во время динамической отправки является абстрактным базовым типом.
Это можно запустить с помощью MSVC, если вы добавите дополнительный уровень косвенности: пусть Base::Base вызывает невиртуальный f(), который, в свою очередь, вызывает (чистый) виртуальный метод doIt.
Когда это аварийное завершение произошло со мной, я отследил его с помощью вложенных деструкторов / удалений, пока не нашел нереализованный деструктор.
Дополняя свой предыдущий комментарий: я действительно нашел двойное освобождение в деструкторе суперкласса, который не является чисто виртуальным. В этом случае сообщение об ошибке времени выполнения вводило в заблуждение (компилятор MinGW).
Совершенно нормально вызывать нечистые виртуальные функции из конструкторов. Стандарт явно позволяет это. Конечно, вызывать чистую виртуальную функцию нельзя (это не имеет смысла).
Я просто скопировал код в VS2015, и я не получаю никаких ошибок ни в Release, ни в Debug.
Кроме того, я попытался собрать тот же код, используя cygwin gcc в Windows, и он не разбился и полностью съел инструкцию Derived d;.
@Romeno: Undefined Behavior - вызвать виртуальную функцию чистого члена (прямо или косвенно) из конструктора или деструктора абстрактного класса. И «сбои с ошибкой« вызова чистой виртуальной функции »», и «отсутствие наблюдаемого эффекта» являются допустимым поведением для компилятора в этом сценарии именно потому, что поведение не определено стандартом языка.
@AdamRosenfield Меня интересует, как это точно указано, можно процитировать или дать ссылку на место / фразу в стандарте, спасибо?
@Romeno: §10.4 / 6 [class.abstract] в стандартах языка C++ 03 или C++ 11: «Функции-члены могут быть вызваны из конструктора (или деструктора) абстрактного класса; эффект создания виртуальный вызов (10.3) чистой виртуальной функции прямо или косвенно для объекта, создаваемого (или уничтожаемого) из такого конструктора (или деструктора), не определен ».
Как бы вы тогда вызывали производный переопределенный метод из базового конструктора?
Обычно, когда вы вызываете виртуальную функцию через висящий указатель - скорее всего, экземпляр уже был уничтожен.
Могут быть и более «творческие» причины: возможно, вам удалось отрезать часть вашего объекта, где была реализована виртуальная функция. Но обычно просто экземпляр уже уничтожен.
Вот хитрый способ, чтобы это произошло. По сути, это случилось со мной сегодня.
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, что нормально
не смог воспроизвести его ни с vs2010 / 12
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.
Помимо стандартного случая вызова виртуальной функции из конструктора или деструктора объекта с чистыми виртуальными функциями, вы также можете получить вызов чистой виртуальной функции (по крайней мере, на MSVC), если вы вызываете виртуальную функцию после того, как объект был уничтожен. . Очевидно, это довольно плохая идея, но если вы работаете с абстрактными классами в качестве интерфейсов и ошибаетесь, то это то, что вы можете увидеть. Вероятно, это более вероятно, если вы используете интерфейсы с подсчетом ссылок и у вас есть ошибка счетчика ссылок или если у вас есть условие гонки использования / уничтожения объекта в многопоточной программе ... Суть этих видов чистых вызовов заключается в том, что они Часто бывает труднее понять, что происходит, так как проверка «обычных подозреваемых» виртуальных вызовов в ctor и dtor оказывается чистой.
Чтобы помочь с отладкой такого рода проблем, вы можете в различных версиях MSVC заменить обработчик purecall библиотеки времени выполнения. Вы делаете это, предоставляя свою собственную функцию с этой подписью:
int __cdecl _purecall(void)
и связать его перед тем, как связать библиотеку времени выполнения. Это дает ВАМ контроль над тем, что происходит при обнаружении чистого вызова. Получив контроль, вы можете делать что-то более полезное, чем стандартный обработчик. У меня есть обработчик, который может предоставить трассировку стека того, где произошел чистый вызов; подробнее см. здесь: http://www.lenholgate.com/blog/2006/01/purecall.html.
(Обратите внимание, что вы также можете вызвать _set_purecall_handler (), чтобы установить обработчик в некоторых версиях MSVC).
Спасибо за указатель на получение вызова _purecall () на удаленном экземпляре; Я не знал об этом, но просто доказал это себе с помощью небольшого тестового кода. Глядя на посмертный дамп в WinDbg, я думал, что имею дело с гонкой, когда другой поток пытался использовать производный объект до того, как он был полностью построен, но это проливает новый свет на проблему и, кажется, лучше соответствует свидетельствам.
Еще кое-что я добавлю: вызов _purecall(), который обычно происходит при вызове метода удаленного экземпляра, будет нет, если базовый класс был объявлен с оптимизацией __declspec(novtable) (специфично для Microsoft). При этом вполне возможно вызвать переопределенный виртуальный метод после удаления объекта, что может замаскировать проблему, пока она не укусит вас в какой-либо другой форме. Ловушка _purecall() - ваш друг!
Это полезно знать, Дэйв, недавно я видел несколько ситуаций, когда я не получал чистых звонков, когда думал, что должен. Возможно, мне не нравилась эта оптимизация.
@LenHolgate: Чрезвычайно ценный ответ. Это ИМЕННО наш проблемный случай (неправильный счетчик ссылок, вызванный условиями гонки). Большое спасибо за то, что указали нам в правильном направлении (вместо этого мы подозревали повреждение v-таблицы и сходили с ума, пытаясь найти код виновника)
Я использую 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 уже имеет очень хороший отвечать, я бы хотел
чтобы добавить немного цвета на примере:
Деструктор производного класса сбрасывает точки 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, поскольку память вернулась в операционную систему, а программа просто не может получить к ней доступ. Таким образом, этот сценарий «вызова чистой виртуальной функции» обычно происходит, когда объект выделяется в пуле памяти, в то время как объект удаляется, базовая память фактически не освобождается ОС, она все еще доступна для процесса.
Это не ошибка компилятора, если вы это имеете в виду.