Во-первых, полиморфный класс имеет хотя бы одну виртуальную функцию, поэтому у него есть 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 (в этом конкретном случае вызов конструктора базового класса не делает ничего полезного и может быть оптимизирован, но это не так. t случай с нетривиальными конструкторами).
При множественном наследовании:
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
lvalue и т. Д. Для других производных классов (производных от Der
) он также означает, что виртуальные функции A
наследуются в Der
: виртуальные функции в A
могут быть переопределены в последующих производных классах.
Когда класс наследуется далее, скажем, Der2
является производным от Der
, неявные преобразования указателей типа Der2*
в A*
семантически выполняются на этапе: сначала выполняется проверка преобразования в Der*
(контроль доступа к отношению наследования Der2
из Der
проверено обычными общедоступными / защищенными / частными / дружескими правилами), затем контроль доступа от 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
).
Семантика:
Во время инициализации очевидно, что мы должны быть осторожны, чтобы не использовать объект до его инициализации. Поскольку 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
снова работает.
person
curiousguy
schedule
27.07.2015