← Назад к вопросам

Какие знаешь особенности вызова виртуальной функции из конструктора?

2.2 Middle🔥 121 комментариев
#ООП и проектирование#Язык C++

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI29 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Какие знаешь особенности вызова виртуальной функции из конструктора

Это классический вопрос на C++ собеседованиях и часто является причиной трудноуловимых ошибок. Ключевая особенность — во время выполнения конструктора базового класса виртуальные функции вызывают версию базового класса, а не производного, даже если объект является экземпляром производного класса.

Основная проблема: динамический тип vs статический тип

class Base {
public:
    Base() {
        // Во время конструктора Base, vtable указывает на Base
        doSomething();  // Вызывает Base::doSomething()
    }
    
    virtual void doSomething() {
        std::cout << "Base::doSomething()\n";
    }
    
    virtual ~Base() = default;
};

class Derived : public Base {
public:
    Derived() {
        // Во время конструктора Derived, уже можно вызвать Derived::doSomething()
        doSomething();  // Вызывает Derived::doSomething()
    }
    
    void doSomething() override {
        std::cout << "Derived::doSomething()\n";
    }
};

int main() {
    // Вывод:
    // Base::doSomething()      <- из конструктора Base
    // Derived::doSomething()   <- из конструктора Derived
    Derived d;
    
    // После конструктора работает нормально
    d.doSomething();  // Вывод: Derived::doSomething()
}

Почему это происходит: порядок конструирования

1. RTTI (Run-Time Type Information) инициализация

Язык C++ инициализирует vtable поэтапно:

class Base {
public:
    Base() {
        // На этом этапе:
        // vptr (virtual pointer) указывает на vtable BASE
        // Тип объекта считается Base (в RTTI)
        virtualFunc();  // Base::virtualFunc()
    }
    virtual void virtualFunc() { }
};

class Derived : public Base {
public:
    Derived() : Base() {  // Сначала конструктор Base
        // После конструктора Base:
        // vptr теперь указывает на vtable DERIVED
        virtualFunc();  // Derived::virtualFunc()
    }
    void virtualFunc() override { }
};

2. Порядок инициализации членов

class Complex : public Base {
public:
    Complex() : Base(), member(nullptr) {
        // 1. Конструктор Base() <- virtualFunc() зовёт Base версию
        // 2. Инициализируются member переменные (member = nullptr)
        // 3. Конструктор Complex
        // 4. Теперь vptr указывает на Complex::vtable
        virtualFunc();  // Complex::virtualFunc()
    }
    
    int* member;
    void virtualFunc() override {
        if (member == nullptr) {  // МОЖЕТ БЫТЬ ОШИБКА!
            // ...
        }
    }
};

Практическая проблема: доступ к неинициализированным членам

class Observer : public Base {
public:
    Observer() : data_(0) {
        // Base() уже вызвал virtualFunc()
        // data_ ещё не инициализирован!
    }
    
    void virtualFunc() override {
        // Опасно! data_ может быть случайным значением
        processData(data_);  // UNDEFINED BEHAVIOR
    }
    
private:
    int data_;
};

Observer o;  // Вывод: обработка random значения data_

Решение 1: Избегать виртуальных вызовов в конструкторе

class Base {
public:
    Base() {
        // Не вызываем виртуальные функции
        // init() будет вызвана явно после конструктора
    }
    
    virtual void initialize() { }
    
    virtual ~Base() = default;
};

class Derived : public Base {
public:
    Derived() : value_(0) { }
    
    void initialize() override {
        std::cout << "Value: " << value_ << "\n";
    }
    
private:
    int value_;
};

int main() {
    Derived d;
    d.initialize();  // Вызывается явно ПОСЛЕ конструктора
}

Решение 2: Template Method Pattern

class Base {
public:
    Base() {
        // Вызываем нелинейную функцию с известным поведением
        doInitialization();
    }
    
    // Финальная функция в Base, вызывает pure virtual
    void doInitialization() {
        setupResources();
        configureSettings();
    }
    
    // Pure virtual — должна быть реализована
    virtual void configureSettings() = 0;
    
private:
    void setupResources() {
        // Базовая инициализация
    }
};

class Derived : public Base {
public:
    Derived() : data_(0) { }
    
    void configureSettings() override {
        // Вызывается из doInitialization()
        // Но data_ ещё не инициализирована!
        // ОШИБКА остаётся
    }
    
private:
    int data_;
};

Решение 3: Двухэтапная инициализация (Two-Phase Init)

class Base {
public:
    Base() : initialized_(false) { }
    
    virtual ~Base() = default;
    
    void initialize() {
        if (!initialized_) {
            doInitialize();
            initialized_ = true;
        }
    }
    
    virtual void doInitialize() = 0;
    
protected:
    bool isInitialized() const { return initialized_; }
    
private:
    bool initialized_;
};

class Derived : public Base {
public:
    Derived() : data_(0) { }
    
    void doInitialize() override {
        // Теперь data_ полностью инициализирован
        processData(data_);
    }
    
private:
    int data_;
};

int main() {
    {
        Derived d;
        d.initialize();  // Безопасный вызов
    }
}

Решение 4: Factory Pattern

class Base {
protected:
    Base() { }
    
public:
    virtual ~Base() = default;
    virtual void configure() = 0;
};

class Derived : public Base {
private:
    Derived() : value_(0) { }
    
    friend class Factory;
    
public:
    void configure() override {
        // value_ полностью инициализирован
    }
    
private:
    int value_;
};

class Factory {
public:
    static std::unique_ptr<Base> create() {
        auto obj = std::unique_ptr<Base>(new Derived());
        obj->configure();  // Вызов ПОСЛЕ полного конструирования
        return obj;
    }
};

int main() {
    auto base = Factory::create();
}

Проверка в деструкторе

То же самое происходит и в деструкторе, но в обратном порядке:

class Base {
public:
    virtual ~Base() {
        doCleanup();  // Вызывает Base версию!
    }
    virtual void doCleanup() { }
};

class Derived : public Base {
public:
    ~Derived() override {
        // data_ ещё живой, но деструктор Base
        // вызовет Base::doCleanup()
    }
    
private:
    int data_;
};

Лучшие практики

  1. Не вызывай виртуальные функции в конструкторе
  2. Используй двухэтапную инициализацию для сложных объектов
  3. Помечай функции как final если они не должны перекрываться
  4. Используй noexcept в конструкторах для гарантированной безопасности
  5. Тестируй иерархии классов на этот баг
// Хороший паттерн
class SafeBase {
public:
    SafeBase() = default;
    
    // Инициализация вне конструктора
    virtual void init() { }
    
    virtual ~SafeBase() = default;
};

class SafeDerived : public SafeBase {
public:
    SafeDerived() : initialized_(false) { }
    
    void init() override {
        // Всё инициализировано к этому моменту
        initialized_ = true;
    }
    
private:
    bool initialized_;
};

Эта особенность одна из самых трудноуловимых в C++ и может привести к серьёзным bagам, поэтому её знание очень ценится на собеседованиях.

Какие знаешь особенности вызова виртуальной функции из конструктора? | PrepBro