01 мая 2012

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

... или «Да я функции вызывал, когда ты был ещё только в планах»

Виртуальные функции — штука хорошая. Однако приходится считаться с механизмами её реализации.
При вызове виртуальных функций в конструкторе базового класса произойдёт не совсем то, чего вы ожидаете.
Рассмотрим пример:
Классы Drvd1 и Drvd2 наследуются от класса Base. В классе Base определён метод
 virtual void init(int _i) = 0; 

Допустим, он обязывает объекты производных классов инициализироваться по-своему, в зависимости от полученного целочисленного параметра. Для пущей убедительности сделаем его чисто виртуальным. Дело за малым: вызвать эти методы.
...

На ум приходит вызвать эту функцию из конструктора базового класса, чтобы при создании объекта он сразу же инициализировался:

Base::Base(int _i)
  : m_i(_i) {
  init(m_i);
}

Здесь-то нас и поджидает подвох. Вы знаете уже, надеюсь, что даже чисто виртуальные методы можно определить в базовом классе. И программа не скопмонуется, пока вы не сделаете это с методом init(), поскольку в конструкторе Base будет всегда вызываться версия Base::init().
Почему так? Дело в том, что когда конструируется объект Drvd1 или Drvd2, в его списке инициализации вызывается (явно или не явно) конструктор базового класса — Base. На этом этапе производный класс отдаёт управление конструктору базового, чтобы он создал свою часть объекта. После выполнения списка инициализации, конструктор класса Base выполняет своё тело, завершающее создание объекта класса Base, где сказано вызвать метод init. И, хоть тот и виртуальный, компилятор подставит в это место вызов Base::init() (а не Drvd1::init() или Drvd2::init()), поскольку из таблицы виртуальных функций будет получен указатель на функцию того полиморфного объекта, который на самом деле запросил эту функцию. А в этот момент объекта производного класса ещё нет и в помине. Виртуальность метода будет работать так, как ожидается только после завершения работы конструктора производного класса. Посему, вызывать init() нужно уже после полного создания объектов.

Base* pb1 = new Drvd1;
Base* pb2 = new Drvd2;

{
  int iCourse = getCourse();
  pb1->init(iCourse);
  pb2->init(iCourse);
}

  #неожиданное поведение  

Об этом говорят специалисты: