Во время конструирования производного объекта базовый конструктор вызывает виртуальный метод. Какой override фактически будет выбран?
Будет выбран override, принадлежащий текущему конструируемому классу, а не наиболее производному классу объекта. Поэтому из конструктора базового класса виртуальный вызов не попадёт в переопределение производного класса: на этом этапе производная часть ещё не сконструирована.
Аналогичное правило действует при разрушении объекта: из деструктора производного класса виртуальный вызов не направляется в ещё более производный класс, а из деструктора базового класса — в производные переопределения.
Виртуальные функции были введены для динамического полиморфизма: код, работающий через интерфейс базового класса, может вызывать реализацию фактического производного типа. Однако во время конструирования и разрушения объект проходит последовательность состояний, в которых его части существуют не одновременно.
Если бы конструктор базового класса мог вызвать метод самого производного класса, такой метод получил бы доступ к полям и инвариантам, которые ещё не инициализированы. Правило ограниченной виртуальной диспетчеризации предотвращает это некорректное использование незавершённого объекта.
Производный объект сначала конструирует базовую часть, затем собственные поля и тело производного конструктора. Когда выполняется базовый конструктор, производственная часть ещё не готова, поэтому вызов переопределённого метода производного класса может прочитать неинициализированное состояние или нарушить его предположения.
Неверное ожидание динамической диспетчеризации приводит к трудно обнаружимым ошибкам: вызову логики в неправильном порядке, обращению к ещё не созданным ресурсам и различиям между поведением конструктора и обычного метода после полного создания объекта.
При входе в конструктор базового класса объект рассматривается как объект этого базового класса. Вызов виртуальной функции выбирает реализацию текущего уровня конструирования, поэтому выполняется версия базового класса, если она переопределена там. После завершения базовой инициализации начинается конструирование производной части, и только затем виртуальные вызовы из обычных методов объекта могут достигать производного override.
Упрощённо это можно представить так: виртуальная диспетчеризация учитывает не только объявленный тип ссылки или указателя, но и текущую фазу жизни объекта. Термин «указатель на таблицу виртуальных функций» полезен как модель реализации, но стандарт формулирует правило через поведение виртуальных вызовов, а не через обязательное устройство компилятора.
При создании object из конструктора Base будет напечатано Base, несмотря на то что объявленный тип объекта — Derived. После завершения конструирования вызов report через интерфейс базового класса уже выбрал бы Derived::report.
В конструкторах и деструкторах лучше не вызывать виртуальные методы для реализации полиморфного поведения. Если виртуальная функция на текущем уровне является чисто виртуальной, такой вызов имеет неопределённое поведение; квалифицированный вызов конкретной реализации базового класса — отдельный механизм и не является виртуальным dispatch.
Практическое решение — выполнять действия, зависящие от производного состояния, после полного конструирования: через фабричную функцию, отдельный метод инициализации с явным контрактом или вызов из конструктора производного класса. Это добавляет этап жизненного цикла, зато делает зависимости и порядок подготовки состояния явными.
Есть базовый класс, конструктор которого вызывает виртуальный метод регистрации объекта. Производный класс переопределяет этот метод и регистрирует собственные поля в глобальном реестре. При создании объекта вызывается базовая реализация, поэтому производные поля не регистрируются, хотя разработчик ожидает полиморфное поведение.
Вариант с виртуальным вызовом из конструктора прост, но неверен: производная часть ещё не готова. Вариант с вызовом виртуального метода после конструктора через отдельный публичный метод работает, однако объект можно забыть инициализировать. Фабричная функция, которая сначала создаёт полностью готовый объект, а затем выполняет регистрацию, лучше защищает порядок действий, но требует запретить или ограничить прямое создание объекта.
Выбирают фабричный путь либо явный метод initialize, если создание и дополнительная настройка действительно должны быть разными этапами. В результате регистрация выполняется только после инициализации всех полей, а конструкторы остаются безопасными и предсказуемыми.
Нет. Тип указателя сам по себе не делает вызов особым: в базовом конструкторе виртуальный вызов всё равно ограничен текущей конструируемой базовой частью. Даже если указатель указывает на память будущего производного объекта, производный override не выбирается.
Во время выполнения деструктора производного класса производная часть ещё считается текущим уровнем, поэтому вызов может выбрать override этого класса, но не класс, который был бы ещё более производным. После завершения деструктора производного класса объект переходит к разрушению базовой части, и виртуальные вызовы из базового деструктора выбирают базовую реализацию.
Да, квалифицированный вызов конкретной функции базового класса обращается непосредственно к указанной реализации и не использует виртуальную диспетчеризацию. Однако это безопасно только при соблюдении контракта базового класса: базовая функция не должна полагаться на ещё не инициализированные поля производного класса. Такой вызов не позволяет получить полиморфное поведение производного типа.