Правила программирования на Си и Си++

       

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


Это не столько правило, сколько констатация факта, хотя она и будет для многих неожиданностью. Базовые классы инициализируются перед производными классами. К тому же, по-видимому, функции производного класса имеют доступ к данным этого класса; в ином случае не было бы смысла в помещении этих функций в производный класс. Если бы конструктор базового класса мог вызывать функцию производного класса через механизм виртуальных функций, то эта функция могла бы с пользой использовать инициализированные поля данных производного класса.

Чтобы сделать суть кристально ясной, давайте взглянем на то, что происходит под капотом. Механизм виртуальных функций реализован посредством таблицы указателей на функции. Когда вы объявляете класс, подобный следующему:

class storable

{

   int stuff;

public:

   storable( void );

   virtual void print( void              );

   virtual void virtf( void              );

   virtual int  cmp  ( const storable r ) = 0;

   int nonvirtual( void );

};

storable::storable  ( void



) { stuff = 0;                       }

void

storable::print( void ) { /* материал для отладки print */ }

void

storable::virtf( void ) { /* делай что-нибудь */           }

int  storable::nonvirtual( void ) {                             }

Лежащее в основе определение класса (сгенерированное компилятором) может выглядеть подобно этому:

int _storable__print          ( storable *this ) { /* ... */ }

int _storable__virtf          ( storable *this ) { /* ... */ }

int _storable__nonvirtual     ( storable *this ) { /* ... */ }

typedef void

(*_vtab[])(...); // массив указателей на функции

_vtab _storable__vtab

{

   _storable__print,

   _storable__virtf,

   NULL             // метка-заполнитель для функции сравнения

};

typedef struct

storable

{

   _storable__vtab *_vtable;

   int stuff;

}

storable;

_storable__ctor( void )      // конструктор

{

   _vtable = _storable__vtable; // Эту строку добавляет


                                //
компилятор.
   stuff = 0;                   // Эта строка из исходного кода.
}
Когда вы вызываете невиртуальную функцию, используя такой код,
как:
storable *p;
p-nonvirtual();
то компилятор в действительности генерирует:
_storable__nonvirtual( p )
Если вы вызываете виртуальную функцию, подобную этой:
p-print();
то получаете нечто совершенно отличное:
( p-_vtable[0] )( p );
Вот таким-то окольным путем, посредством этой таблицы и работают виртуальные функции. Когда вы вызываете функцию производного класса при помощи указателя базового класса, то компилятор даже не знает, что он обращается к функции производного класса. Например, вот определение производного класса на уровне исходного кода:
class employee : public storable
{
   int
derived_stuff;
   // ...
public:
   virtual
int cmp( const storable r );
};
/* виртуальный */ int
employee::print( const storable r ) { }
/* виртуальный */ int employee::cmp  ( const storable r ) { }
А вот что сделает с ним компилятор:
int _employee__print( employee *this          ) { /* ... */ }
int _employee__cmp  ( employee *this, const
storable *ref_r )
{ /* ... */ }
_vtab _employee_vtable =
{
   _employee__print,
   _storable_virtf,  // Тут нет замещения в производном классе,
                     // поэтому
используется указатель на
                     // функцию базового класса.
   _employee_cmp
};
typedef struct
employee
{
   _vtab *_vtable;    // Генерируемое компилятором поле данных.
   int stuff;         // Поле базового класса.
   int
derived_stuff; // Поле, добавленное в объявлении
                      // производного класса.
}
employee;
_employee__ctor( employee *this ) // Конструктор по умолчанию,
{                                 // генерируемый
компилятором.
   _storable_ctor();        // Базовые классы
инициализируются
                            // в первую очередь.


_vtable = _employee_vtable; // Создается таблица виртуальных
}                           // функций.
Компилятор переписал те ячейки в таблице виртуальных функций, которые содержат замещенные в производном классе виртуальные функции. Виртуальная функция (virtf), которая не была замещена в производном классе, остается инициализированной функцией базового класса.
Когда вы создаете во время выполнения объект таким образом:
storable *p = new employee();
то компилятор на самом деле генерирует:
storable *p;
p = (storable *)malloc( sizeof(employee) );
_employee_ctor( p );
Вызов _employee_ctor()
сначала инициализирует компонент базового класса посредством вызова _sortable_ctor(), которая добавляет таблицу этой виртуальной функции к своей таблице и выполняется. Затем управление передается обратно к _employee_ctor() и указатель в таблице виртуальной функции переписывается так, чтобы он указывал на таблицу производного класса.
Отметьте, что, хотя p теперь указывает на employee, код p-print()
генерирует точно такой же код, как и раньше:
( p-_vtable[0] )( p );
Несмотря на это, теперь p
указывает на объект производного класса, поэтому вызывается версия print()
из производного класса (так как _vtable в объекте производного класса указывает на таблицу производного класса). Крайне необходимо, чтобы эти две функции print()
располагались в одной и той же ячейке своих таблиц смешений, но это обеспечивается компилятором.
Возвращаясь к основному смыслу данного правила, отметим, что при рассмотрении того, как работает конструктор, важен порядок инициализации. Конструктор производного класса перед тем, как он что-либо сделает, вызывает конструктор базового класса. Так как _vtable в конструкторе базового класса указывает на таблицу виртуальных функций базового класса, то вы лишаетесь доступа к виртуальным функциям базового класса после того, как вызвали их. Вызов print в конструкторе базового класса все так же дает:
( this-_vtable[0] )( p );
но _vtable
указывает на таблицу базового класса и _vtable[0]
указывает на функцию базового класса. Тот же самый вызов в конструкторе производного класса даст версию print()
производного класса, потому что _vtable
будет перекрыта указателем на таблицу производного класса к тому времени, когда была вызвана print().
Хотя я и не показывал этого прежде, то же самое происходит в деструкторе. Первое, что делает деструктор, — это помещает в _vtable
указатель на таблицу своего собственного класса. Только после этого он выполняет написанный вами код. Деструктор производного класса вызывает деструктор базового класса на выходе (в самом конце — после того, как выполнен написанный пользователем код).

Содержание раздела