"Самая распространенная ошибка, с которой я столкнулся, - это вызов виртуальной функции из конструктора или деструктора базового класса."
Когда объект создается, указатель на таблицу виртуальной диспетчеризации изначально нацелен на самый высокий суперкласс, и он обновляется только после завершения построения промежуточных классов. Таким образом, вы можете случайно вызвать чисто виртуальную реализацию до того момента, пока подкласс - с его собственной реализацией переопределяющей функции - не завершит конструирование. Это может быть самый производный подкласс, или где-то между ними.
Это может произойти, если вы будете следовать указателю на частично сконструированный объект (например, в состоянии гонки из-за асинхронных или потоковых операций).
Если у компилятора есть основания полагать, что он знает реальный тип, на который указывает указатель на базовый класс, он может разумно обойти виртуальную диспетчеризацию. Вы можете запутать это, делая что-то с неопределенным поведением, например, переосмыслением приведения.
Во время уничтожения виртуальная диспетчерская таблица должна быть возвращена, так как производные классы уничтожены, поэтому снова может быть вызвана чистая виртуальная реализация.
После уничтожения дальнейшее использование объекта с помощью «висящих» указателей или ссылок может вызывать чисто виртуальную функцию, но в таких ситуациях нет определенного поведения.