*** ВНИМАНИЕ: Блог переехал на другой адрес - demin.ws ***

четверг, 10 сентября 2009 г.

Виртуальные функции в конструкторе и деструкторе

Рассмотрим простой пример (virtual_funct_const.cpp):

#include <iostream>

class A {
public:
A() {
construct();
}

~A() {
destruct();
}

virtual void construct() {
std::cout << "A::construct()" << std::endl;
}

virtual void destruct() {
std::cout << "A::destruct()" << std::endl;
}
};

class B: public A {
public:
B() {
construct();
}

~B() {
destruct();
}

virtual void construct() {
std::cout << "B::construct()" << std::endl;
}

virtual void destruct() {
std::cout << "B::destruct()" << std::endl;
}
};

int main() {
B b;
return 0;
}
Что напечатает эта программа?

А вот что:
A::construct()
B::construct()
B::destruct()
A::destruct()
Получается, что конструкторы и деструкторы классов A и B при вызове объявленных виртуальными функций construct() и destruct() реально вызывали функции только своего класса.

В этом нет никакого секрета, а просто есть правило: виртуальная функция не является виртуальной, если вызывается из конструктора или деструктора.

Правило надо заучивать, что неудобно. Проще понять принцип. А принцип тут в краеугольном камне реализации наследования в C++: при создании объекта конструкторы в иерархии вызываются от базового класса к самому последнему унаследованному. Для деструкторов все наоборот.

Что получается: конструктор класса всегда работает в предположении, что его дочерние классы еще не созданы, поэтому он не имеет права вызывать функции, определенные в них. И для виртуальной функций ему ничего не остается, как только вызвать то, что определено в нем самом. Получается, что механизм виртуальных функций тут как-бы не работает. А он тут действительно не работает, так как таблица виртуальных функций дочернего класса еще не перекрыла текущую таблицу.

В деструкторе все наоборот. Деструктор знает, что во время его вызова все дочерние классы уже разрушены и вызывать у них ничего уже нельзя, поэтому он замещает адрес таблицы виртуальных функций на адрес своей собственной таблицы и благополучно вызывает версию виртуальной функции, определенной в нем самом.

Итак, виртуальная функция не является виртуальной, если вызывается из конструктора или деструктора.

7 комментариев:

  1. Насколько я помню, такие вызовы - это undefined behavior, поэтому "Что напечатает эта программа?" в общем неизвестно.

    ОтветитьУдалить
  2. Я как-то не нашел однозначно написанного в Стандарте или у Страуструпа, что так делать нельзя. И вроде с точки зрения логики тут нет особых противоречий.

    ОтветитьУдалить
  3. Что касается правил, то имхо лучше заучить правило Скотта Мейера по этому поводу: "Никогда не вызывайте виртуальные функции из конструктора или деструктора."

    http://www.artima.com/cppsource/nevercall.html

    ОтветитьУдалить
  4. Согласен, путаницы в коде будет порядочно. По понимать природу явления надо, так как тут нет особого "неопределенного поведения", так как ясно, что именно делает компилятор.

    ОтветитьУдалить
  5. "виртуальная функция не является виртуальной, если вызывается из конструктора или деструктора".
    Любопытная мнемоническая фигня. Все несколько проще: на момент конструирования/разрушения (сорри) базового класса вызов виртуальной функции осуществляется на общих основаниях. Просто в эти интимные моменты еще/уже не существует никакой перегруженной версии этой функции. Вот, скажем, так:

    struct A {
    virtual void v() = 0;
    void f() { v(); }
    virtual ~A() { f(); }
    };

    struct B : A {
    void v() {}
    };

    int main()
    {
    B b;
    }

    ОтветитьУдалить
  6. dmi3s: все верно, понимание реализации механизма виртуальных функции в С++ дает ответ на этот вопрос.

    ОтветитьУдалить
  7. вобще-то правило звучит : не запускать виртуальные функции во время конструкции или деструкции (а не конкретно из конструктора или деструктора)

    ОтветитьУдалить