Виртуальные функции
Стоит помнить, что традиционной и очевидной реализацией вызова виртуальной функции является просто косвенный вызов функции...
Это, вообще говоря, неверно. При применении множественного наследования "просто косвенного вызова" оказывается недостаточно. Рассмотрим следующую программу: #include <stdio.h>
struct B1 { int b1; // непустая virtual ~B1() { } };
struct B2 { int b2; // непустая virtual void vfun() { } };
struct D : B1, B2 { // множественное наследование от непустых классов virtual void vfun() { printf("D::vfun(): this=%p\n", this); } };
int main() { D d;
D* dptr=&d; printf("dptr\t%p\n", dptr); dptr->vfun();
B2* b2ptr=&d; printf("b2ptr\t%p\n", b2ptr); b2ptr->vfun(); }
На своей машине я получил следующие результаты: dptr 0x283fee8 D::vfun(): this=0x283fee8 b2ptr 0x283feec
D::vfun(): this=0x283fee8
Т.е. при вызове через указатель на производный класс dptr, внутри D::vfun() мы получим this=0x283fee8. Но несмотря на то, что после преобразования исходного указателя в указатель на (второй) базовый класс b2ptr, его значение (очевидно) изменилось, внутри D::vfun() мы все равно видим исходное значение, что полностью соответствует ожиданиям D::vfun() относительно типа и значения своего this.
Что же все это означает? А означает это то, что если бы вызов виртуальной функции struct D : B1, B2 { virtual void vfun(D *const this) // псевдокод { // ... } };
через указатель ptr->vfun() всегда сводился бы к вызову (*vtbl[index_of_vfun])(ptr), то в нашей программе мы бы получили b2ptr==0x283feec==this!=0x283fee8.
Вопрос номер два: как они это делают? Суть проблемы в том, что одна и та же замещенная виртуальная функция (D::vfun() в нашем случае) может быть вызвана как через указатель на производный класс (ptr==0x283fee8) так и через указатель на один из базовых классов (ptr==0x283feec), чьи значения не совпадают, в то время как переданное значение this должно быть одним и тем же (this==0x283fee8) в обоих случаях.
К счастью, vtbl содержит разные записи для каждого из вариантов вызова, так что решение, очевидно, есть. На практике, чаще всего, используется один из следующих способов:
В таблицу vtbl добавляется дополнительная колонка -- vdelta. Тогда в процессе вызова виртуальной функции кроме адреса функции из vtbl извлекается и дельта, чье значение добавляется к ptr: addr=vtbl[index].vaddr; // извлекаем адрес функции vfun delta=vtbl[index].vdelta; // извлекаем дельту, зависящую от способа вызова vfun (*addr)(ptr+delta); // вызываем vfun
Существенным недостатком данного способа является заметное увеличение размеров vtbl и значительные накладные расходы времени выполнения: дело в том, что абсолютное большинство вызовов виртуальных функций не требует коррекции значения ptr, так что соответствующие им значения vdelta будут нулевыми. Достоинством -- возможность вызова виртуальной функции из ANSI C кода, что важно для C++ -> C трансляторов.
Более эффективным решением является создание нескольких точек входа для одной и той же виртуальной функции, каждая из которых соответствующим образом корректирует значение ptr (если это вообще нужно): vfun_entry_0: // ... // собственно код vfun // ... return;
vfun_entry_1: ptr+=delta_1; // корректируем значение ptr goto vfun_entry_0; // и переходим к телу vfun
В этом случае vtbl содержит только адреса соответствующих точек входа и никаких напрасных вычислений не требуется. Специфическим недостатком данного способа является невозможность его реализации средствами ANSI C.
Интересное и достаточно подробное описание представления объектов и реализации механизма вызова виртуальных функций можно найти в статье C++: Under the Hood. Она описывает реализацию, использованную разработчиками MSVC.
void f1(B* ptr) { ptr->ackf(3, 5); // 42438 вызовов! }
void f2(B* ptr) { ptr->vackf(3, 5); // 42438 вызовов! }
int B::ackf(int x, int y) { if (x==0) return y+1; else if (y==0) return ackf(x-1, 1); else return ackf(x-1, ackf(x, y-1)); }
int B::vackf(int x, int y) { return 0; }
int D::vackf(int x, int y) { if (x==0) return y+1; else if (y==0) return vackf(x-1, 1); else return vackf(x-1, vackf(x, y-1)); }
long Count;
int main(int argc,char** argv) { if (argc>1) Count=atol(argv[1]);
clock_t c1,c2;
D d; { c1=clock();
for (long i=0; i<Count; i++) for (long j=0; j<1000; j++) f1(&d);
c2=clock(); printf("f1(): %ld ths calls per %.1f sec\n", Count, double(c2-c1)/CLK_TCK); } { c1=clock();
for (long i=0; i<Count; i++) for (long j=0; j<1000; j++) f2(&d);
c2=clock(); printf("f2(): %ld ths calls per %.1f sec\n", Count, double(c2-c1)/CLK_TCK); } }
показывает заметно другие результаты, существенно уменьшая относительную разность времени выполнения.