Можно ли вызывать виртуальные функции в конструкторе и как это влияет на поведение программы?
Что проверяют
Знаете ли вы, каков динамический тип объекта во время конструирования. Следом почти всегда спрашивают про деструктор и про вызов чисто виртуальной функции.
Как отвечать
Вызывать можно, но полиморфизма не будет. Пока работает конструктор Base, объект считается объектом Base: часть Derived ещё не создана, её члены не инициализированы. Поэтому вызов виртуальной функции из конструктора Base уходит в Base::f(), а не в переопределение.
Механика в типичных реализациях: каждый конструктор после отработки базовых классов записывает в объект указатель на vtable своего класса. В теле конструктора Base vptr указывает на таблицу Base, конструктор Derived переписывает его на таблицу Derived. Если иерархия глубже, в конструкторе Derived вызов уйдёт в версию Derived, даже если строится MoreDerived. Так же ведут себя typeid и dynamic_cast.
Деструктор работает зеркально: к моменту тела ~Base часть Derived уже уничтожена, и вызов снова идёт в версию Base.
Отдельный случай: чисто виртуальная функция. Её вызов из конструктора или деструктора, в том числе через промежуточный метод, это неопределённое поведение. Обычно программа падает с сообщением pure virtual method called.
Что делать вместо этого:
- параметр конструктора базы: если базе нужно значение от наследника, передайте его явно;
- фабрика:
static std::unique_ptr<Derived> create()создаёт объект и потом вызывает виртуальную инициализацию; - CRTP: если полиморфизм нужен на этапе компиляции.
Правило C.82 из C++ Core Guidelines прямо запрещает такие вызовы, а clang-tidy ловит их проверкой clang-analyzer-optin.cplusplus.VirtualCall.
Где спотыкаются
Говорят, что вызовется версия Derived. Так ведут себя Java и C#: там переопределение вызывается из конструктора базы и видит поля наследника ещё со значениями по умолчанию. В C++ так сделано сознательно.
Думают, что такой вызов не скомпилируется. Компилятор его пропустит, в лучшем случае выдаст предупреждение.
Прячут вызов в обычный метод init(), который зовут из конструктора. Проблема от этого не исчезает, а для чисто виртуальной функции становится падением в рантайме.