Во-первых, у полиморфного класса есть хотя бы одна виртуальная функция, поэтому он имеет vptr:
struct A {
virtual void foo();
};
компилируется в:
struct A__vtable { // vtable for objects of declared type A
void (*foo__ptr) (A *__this); // pointer to foo() virtual function
};
void A__foo (A *__this); // A::foo ()
// vtable for objects of real (dynamic) type A
const A__vtable A__real = { // vtable is never modified
/*foo__ptr =*/ A__foo
};
struct A {
A__vtable const *__vptr; // ptr to const not const ptr
// vptr is modified at runtime
};
// default constructor for class A (implicitly declared)
void A__ctor (A *__that) {
__that->__vptr = &A__real;
}
Примечание: C ++ может быть скомпилирован на другой язык высокого уровня, например C (как это делал cfront) или даже на подмножество C ++ (здесь C ++ без virtual
). Я поместил __
в имена, сгенерированные компилятором.
Обратите внимание, что это упрощенная модель, в которой RTTI не поддерживается; Реальные компиляторы добавят данные в vtable для поддержки typeid
.
Теперь простой производный класс:
struct Der : A {
override void foo();
virtual void bar();
};
Не-виртуальные (*) подобъекты базового класса являются подобъектами, подобными подобъектам-членам, но в то время как подобъекты-члены являются полными объектами, т.е. их реальный (динамический) тип является их объявленным типом, подобъекты базового класса не завершены, и их реальный тип изменяется во время построения.
(*) виртуальные базы сильно отличаются, как функции виртуальных членов отличаются от не виртуальных элементов
struct Der__vtable { // vtable for objects of declared type Der
A__vtable __primary_base; // first position
void (*bar__ptr) (Der *__this);
};
// overriding of a virtual function in A:
void Der__foo (A *__this); // Der::foo ()
// new virtual function in Der:
void Der__bar (Der *__this); // Der::bar ()
// vtable for objects of real (dynamic) type Der
const Der__vtable Der__real = {
{ /*foo__ptr =*/ Der__foo },
/*foo__ptr =*/ Der__bar
};
struct Der { // no additional vptr
A __primary_base; // first position
};
Здесь «первая позиция» означает, что элемент должен быть первым (другие элементы могут быть переупорядочены): они расположены с нулевым смещением, поэтому мы можем reinterpret_cast
указатели, типы совместимы; при ненулевом смещении мы должны были бы сделать корректировки указателя с арифметикой на char*
.
Отсутствие настройки может показаться не таким уж большим событием в плане сгенерированного кода (только некоторые добавляют непосредственные инструкции asm), но это означает гораздо больше, это означает, что такие указатели можно рассматривать как имеющие разные типы: объект типа A__vtable*
может содержать указатель на Der__vtable
и рассматриваться как Der__vtable*
или A__vtable*
. Этот же объект-указатель служит указателем на A__vtable
в функциях, работающих с объектами типа A
, и указателем на Der__vtable
в функциях, работающих с объектами типа Der
.
.
// default constructor for class Der (implicitly declared)
void Der__ctor (Der *__this) {
A__ctor (reinterpret_cast<A*> (__this));
__this->__vptr = reinterpret_cast<A__vtable const*> (&Der__real);
}
Вы видите, что динамический тип, как определено в vptr, изменяется во время построения, когда мы присваиваем vptr новое значение (в данном конкретном случае вызов конструктора базового класса не делает ничего полезного и может быть оптимизирован, но дело обстоит не так с нетривиальными конструкторами).
С множественным наследованием:
struct C : A, B {};
Экземпляр C
будет содержать A
и B
, например:
struct C {
A base__A; // primary base
B base__B;
};
Обратите внимание, что только один из этих подобъектов базового класса может иметь привилегию сидеть со смещением ноль; это важно во многих отношениях:
преобразование указателей на другие базовые классы (апкасты) потребует
регулировка; и наоборот, для повышений нужны противоположные корректировки;
это означает, что при выполнении виртуального вызова с базовым классом
указатель, this
имеет правильное значение для записи в производной
Переопределение класса.
Итак, следующий код:
void B::printaddr() {
printf ("%p", this);
}
void C::printaddr () { // overrides B::printaddr()
printf ("%p", this);
}
можно скомпилировать в
void B__printaddr (B *__this) {
printf ("%p", __this);
}
// proper C::printaddr taking a this of type C* (new vtable entry in C)
void C__printaddr (C *__this) {
printf ("%p", __this);
}
// C::printaddr overrider for B::printaddr
// needed for compatibility in vtable
void C__B__printaddr (B *__this) {
C__printaddr (reinterpret_cast<C*>(reinterpret_cast<char*> (__this) - offset__C__B));
}
Мы видим, что объявленный тип C__B__printaddr
и семантика совместимы с B__printaddr
, поэтому мы можем использовать &C__B__printaddr
в vtable таблицы B
; C__printaddr
несовместим, но может использоваться для вызовов, включающих объекты C
или классы, производные от C
.
Не виртуальная функция-член похожа на бесплатную функцию, которая имеет доступ к внутренним материалам. Функция виртуального члена - это «точка гибкости», которую можно настроить путем переопределения. Объявление функции виртуального члена играет особую роль в определении класса: подобно другим членам они являются частью договора с внешним миром, но в то же время они являются частью договора с производным классом.
Не виртуальный базовый класс подобен объекту-члену, где мы можем улучшить поведение с помощью переопределения (также мы можем получить доступ к защищенным членам). Для внешнего мира наследование A
в Der
подразумевает, что для указателей будут существовать неявные преобразования из производных в базовые, что A&
может быть связано со значением Der
l и т. Д. (получено из Der
), это также означает, что виртуальные функции A
наследуются в Der
: виртуальные функции в A
могут быть переопределены в других производных классах.
Когда класс наследуется, скажем, Der2
получен из Der
, неявное преобразование указателей типа Der2*
в A*
выполняется семантически на шаге: сначала проверяется преобразование в Der*
(контроль доступа к отношению наследования Der2
от Der
проверяется с помощью обычных правил public / protected / private / friend), затем контроль доступа от Der
до A
. Невиртуальное отношение наследования не может быть уточнено или переопределено в производных классах.
Функции, не являющиеся виртуальными членами, могут вызываться напрямую, а виртуальные члены должны вызываться косвенно через vtable (если только фактический тип объекта не известен компилятору), поэтому ключевое слово virtual
добавляет косвенное обращение к функциям доступа членов. Как и для членов-функций, ключевое слово virtual
добавляет косвенное отношение к доступу к базовому объекту; Как и для функций, виртуальные базовые классы обеспечивают гибкость в наследовании.
При выполнении не виртуального, многократного наследования:
struct Top { int i; };
struct Left : Top { };
struct Right : Top { };
struct Bottom : Left, Right { };
В Bottom
(Left::i
и Right::i
) есть только два Top::i
подобъекта, как и с объектами-членами:
struct Top { int i; };
struct mLeft { Top t; };
struct mRight { mTop t; };
struct mBottom { mLeft l; mRight r; }
Никто не удивляется, что есть два int
подчиненных (l.t.i
и r.t.i
).
С виртуальными функциями:
struct Top { virtual void foo(); };
struct Left : Top { }; // could override foo
struct Right : Top { }; // could override foo
struct Bottom : Left, Right { }; // could override foo (both)
это означает, что есть две разные (не связанные) виртуальные функции, называемые foo
, с разными записями vtable (обе имеют одинаковую сигнатуру и могут иметь общую переопределение).
Семантика не виртуальных базовых классов вытекает из того факта, что базовое, не виртуальное наследование является исключительным отношением: отношение наследования, установленное между Left и Top, не может быть изменено дальнейшим производным, поэтому существует тот же факт между Right
и Top
не может повлиять на это отношение. В частности, это означает, что Left::Top::foo()
может быть переопределено в Left
и в Bottom
, но Right
, который не имеет отношения наследования с Left::Top
, не может установить эту точку настройки.
Виртуальные базовые классы отличаются: виртуальное наследование - это общее отношение, которое можно настроить в производных классах:
struct Top { int i; virtual void foo(); };
struct vLeft : virtual Top { };
struct vRight : virtual Top { };
struct vBottom : vLeft, vRight { };
Здесь это только один подобъект базового класса Top
, только один int
член.
Реализация:
Пространство для не виртуальных базовых классов выделяется на основе статической компоновки с фиксированными смещениями в производном классе. Обратите внимание, что компоновка производного класса включена в компоновку более производного класса, поэтому точное положение подобъектов не зависит от реального (динамического) типа объекта (так же, как адрес не виртуальной функции является константой). ). OTOH, положение подобъектов в классе с виртуальным наследованием определяется динамическим типом (точно так же, как адрес реализации виртуальной функции известен только тогда, когда известен динамический тип).
Местоположение подобъекта будет определено во время выполнения с помощью vptr и vtable (повторное использование существующего vptr подразумевает меньшие затраты пространства) или прямым внутренним указателем на подобъект (больше служебных данных, меньше косвенных ссылок).
Поскольку смещение виртуального базового класса определяется только для полного объекта и не может быть известно для данного объявленного типа, виртуальное основание не может быть выделено с нулевым смещением и никогда не является первичной базой, Производный класс никогда не будет повторно использовать vptr виртуальной базы как свой собственный vptr.
В терминах возможного перевода:
struct vLeft__vtable {
int Top__offset; // relative vLeft-Top offset
void (*foo__ptr) (vLeft *__this);
// additional virtual member function go here
};
// this is what a subobject of type vLeft looks like
struct vLeft__subobject {
vLeft__vtable const *__vptr;
// data members go here
};
void vLeft__subobject__ctor (vLeft__subobject *__this) {
// initialise data members
}
// this is a complete object of type vLeft
struct vLeft__complete {
vLeft__subobject __sub;
Top Top__base;
};
// non virtual calls to vLeft::foo
void vLeft__real__foo (vLeft__complete *__this);
// virtual function implementation: call via base class
// layout is vLeft__complete
void Top__in__vLeft__foo (Top *__this) {
// inverse .Top__base member access
char *cp = reinterpret_cast<char*> (__this);
cp -= offsetof (vLeft__complete,Top__base);
vLeft__complete *__real = reinterpret_cast<vLeft__complete*> (cp);
vLeft__real__foo (__real);
}
void vLeft__foo (vLeft *__this) {
vLeft__real__foo (reinterpret_cast<vLeft__complete*> (__this));
}
// Top vtable for objects of real type vLeft
const Top__vtable Top__in__vLeft__real = {
/*foo__ptr =*/ Top__in__vLeft__foo
};
// vLeft vtable for objects of real type vLeft
const vLeft__vtable vLeft__real = {
/*Top__offset=*/ offsetof(vLeft__complete, Top__base),
/*foo__ptr =*/ vLeft__foo
};
void vLeft__complete__ctor (vLeft__complete *__this) {
// construct virtual bases first
Top__ctor (&__this->Top__base);
// construct non virtual bases:
// change dynamic type to vLeft
// adjust both virtual base class vptr and current vptr
__this->Top__base.__vptr = &Top__in__vLeft__real;
__this->__vptr = &vLeft__real;
vLeft__subobject__ctor (&__this->__sub);
}
Для объекта известного типа доступ к базовому классу осуществляется через vLeft__complete
:
struct a_vLeft {
vLeft m;
};
void f(a_vLeft &r) {
Top &t = r.m; // upcast
printf ("%p", &t);
}
переводится на:
struct a_vLeft {
vLeft__complete m;
};
void f(a_vLeft &r) {
Top &t = r.m.Top__base;
printf ("%p", &t);
}
Здесь известен действительный (динамический) тип r.m
, а также относительная позиция подобъекта известна во время компиляции. Но здесь:
void f(vLeft &r) {
Top &t = r; // upcast
printf ("%p", &t);
}
реальный (динамический) тип r
неизвестен, поэтому доступ осуществляется через vptr:
void f(vLeft &r) {
int off = r.__vptr->Top__offset;
char *p = reinterpret_cast<char*> (&r) + off;
printf ("%p", p);
}
Эта функция может принимать любой производный класс с другим макетом:
// this is what a subobject of type vBottom looks like
struct vBottom__subobject {
vLeft__subobject vLeft__base; // primary base
vRight__subobject vRight__base;
// data members go here
};
// this is a complete object of type vBottom
struct vBottom__complete {
vBottom__subobject __sub;
// virtual base classes follow:
Top Top__base;
};
Обратите внимание, что базовый класс vLeft
находится в фиксированном месте в vBottom__subobject
, поэтому vBottom__subobject.__ptr
используется в качестве vptr для всего vBottom
.
Семантика:
Отношение наследования является общим для всех производных классов; это означает, что право на переопределение является общим, поэтому vRight
может переопределить vLeft::foo
. Это создает распределение обязанностей: vLeft
и vRight
должны согласовать, как они настраивают Top
:
struct Top { virtual void foo(); };
struct vLeft : virtual Top {
override void foo(); // I want to customise Top
};
struct vRight : virtual Top {
override void foo(); // I want to customise Top
};
struct vBottom : vLeft, vRight { }; // error
Здесь мы видим конфликт: vLeft
и vRight
пытаются определить поведение единственной виртуальной функции foo, а определение vBottom
ошибочно из-за отсутствия общей переопределения.
struct vBottom : vLeft, vRight {
override void foo(); // reconcile vLeft and vRight
// with a common overrider
};
Реализация:
Конструирование класса с не виртуальными базовыми классами с не виртуальными базовыми классами включает вызов конструкторов базового класса в том же порядке, что и для переменных-членов, меняя динамический тип каждый раз, когда мы вводим ctor. Во время построения подобъекты базового класса действительно действуют так, как если бы они были законченными объектами (это даже верно для невозможных завершенных подобъектов базового класса: они являются объектами с неопределенными (чистыми) виртуальными функциями). Виртуальные функции и RTTI могут вызываться во время построения (кроме, конечно, чисто виртуальных функций).
Построение класса с не виртуальными базовыми классами с виртуальными базами более сложное : во время построения динамический тип является типом базового класса, но макет виртуальной базы по-прежнему является макетом большинство производных типов, которые еще не созданы, поэтому нам нужно больше vtables для описания этого состояния:
// vtable for construction of vLeft subobject of future type vBottom
const vLeft__vtable vLeft__ctor__vBottom = {
/*Top__offset=*/ offsetof(vBottom__complete, Top__base),
/*foo__ptr =*/ vLeft__foo
};
Виртуальные функции - это функции vLeft
(во время конструирования время жизни объекта vBottom еще не началось), в то время как виртуальные базовые местоположения соответствуют vBottom
(как определено в vBottom__complete
переведенном возражении). 1207 *
Семантика:
Во время инициализации очевидно, что мы должны быть осторожны, чтобы не использовать объект до его инициализации. Поскольку C ++ дает нам имя до полной инициализации объекта, это легко сделать:
int foo (int *p) { return *pi; }
int i = foo(&i);
или с указателем this в конструкторе:
struct silly {
int i;
std::string s;
static int foo (bad *p) {
p->s.empty(); // s is not even constructed!
return p->i; // i is not set!
}
silly () : i(foo(this)) { }
};
Совершенно очевидно, что любое использование this
в ctor-init-list должно быть тщательно проверено. После инициализации всех членов this
может быть передано другим функциям и зарегистрировано в некотором наборе (пока не начнется уничтожение).
Что менее очевидно, так это то, что при построении класса, включающего общие виртуальные базы, субобъекты перестают создаваться: во время построения vBottom
:
сначала создаются виртуальные базы: когда Top
создается, он создается как обычный субъект (Top
даже не знает, что это виртуальная база)
, тогда базовые классы создаются в порядке слева направо: подобъект vLeft
создается и становится функциональным как обычный vLeft
(но с макетом vBottom
), поэтому база Top
подобъект класса теперь имеет vLeft
динамический тип;
начинается создание подобъекта vRight
, и динамический тип базового класса меняется на vRight; но vRight
не является производным от vLeft
, ничего не знает о vLeft
, поэтому база vLeft
теперь сломана;
когда начинается тело конструктора Bottom
, типы всех подобъектов стабилизировались и vLeft
снова функционирует.