SeniorДебаггингИногдаЕщё не отвечали
Почему виртуальный вызов внутри конструктора не диспетчеризуется к переопределению производного класса?
Автор хочет, чтобы конструктор Widget вызвал переопределённый render() из Button, но программа печатает Widget::render, а не Button::render.
#include <iostream>
struct Widget {
Widget() {
render(); // intended: Button::render
}
virtual void render() const {
std::cout << "Widget::render\n";
}
};
struct Button : Widget {
std::string label_ = "OK";
void render() const override {
std::cout << "Button::render label=" << label_ << "\n";
}
};
int main() {
Button b;
}
Найдите ошибку и объясните причину.
На каждой стадии конструирования vptr объекта указывает на vtable именно этого класса, потому что более производных частей ещё не существует. virtual-вызов из конструктора базового класса разрешается в версию базы. Вызов чисто виртуальной функции так — неопределённое поведение.
- ✗Вызывать виртуальные хуки из конструктора базы, ожидая выполнения поведения производного класса
- ✗Вызывать чисто виртуальную функцию из конструктора или деструктора — UB, а не просто версия базы
- ✗Пытаться исправить это через
dynamic_cast<Derived*>(this)внутри конструктора базы
- →Действует ли то же правило для виртуальных вызовов внутри деструктора, и почему?
- →Как двухфазная инициализация или фабричная функция чисто решают эту проблему?
Оглавление
Проблема
#include <iostream>
struct Widget {
Widget() {
// намерение: вызвать переопределённый render() из Button
render(); // (1) диспетчеризуется к Widget::render
}
virtual void render() const {
std::cout << "Widget::render\n";
}
};
struct Button : Widget {
std::string label_ = "OK";
void render() const override {
std::cout << "Button::render label=" << label_ << "\n";
}
};
int main() {
Button b; // печатает "Widget::render", а не Button::render
}
В точке (1) объект ещё имеет динамический тип Widget: vptr указывает на vtable Widget, под-объект Button (включая label_) ещё не существует. Если бы render() была чисто виртуальной, вызов был бы неопределённым поведением, а не просто обращением к базовой версии.
Решение — двухфазная инициализация
struct Widget {
Widget() = default; // конструктор не вызывает virtual
virtual void render() const { std::cout << "Widget::render\n"; }
virtual ~Widget() = default;
};
struct Button : Widget {
std::string label_ = "OK";
void render() const override {
std::cout << "Button::render label=" << label_ << "\n";
}
};
template <class T, class... Args>
T make() { // фабрика: объект сконструирован полностью
T obj(std::forward<Args>(args)...); // прежде чем вызывается render()
obj.render(); // теперь динамический тип окончателен
return obj;
}Оглавление