Ответ 1
Final в объявлении функции X::f()
подразумевает, что объявление не может быть переопределено, поэтому все вызовы, которые называют это объявление, могут быть связаны рано (не те вызовы, которые называют объявление в базовом классе): если виртуальная функция является конечной в ABI созданные vtables могут быть несовместимы с производимыми практически того же класса без final: вызов виртуальных функций, объявления имен которых помечены как final, можно считать прямым: попытка использовать запись vtable (которая должна существовать в final- меньше ABI) незаконно.
Компилятор может использовать окончательную гарантию, чтобы сократить размер таблиц vtables (которые могут иногда сильно увеличиваться), не добавляя новую запись, которая обычно добавляется и которая должна соответствовать ABI для не окончательного объявления.
Записи добавляются для объявления, переопределяющего функцию, а не (по сути, всегда) первичную базу или для нетривиально ковариантного возвращаемого типа (ковариант возвращаемого типа на неосновной основе).
По своей сути первичный базовый класс: простейший случай полиморфного наследования
Простой случай полиморфного наследования, производный класс, наследующий не виртуально от одного полиморфного базового класса, является типичным случаем всегда первичной базы: подобъект полиморфной базы находится в начале, адрес производного объекта совпадает с адресом из базового подобъекта виртуальные вызовы могут быть сделаны напрямую с указателем на любой из них, все просто.
Эти свойства имеют значение true, независимо от того, является ли производный класс полным объектом (объектом, не являющимся подобъектом), наиболее производным объектом или базовым классом. (Это классовые инварианты, гарантированные на уровне ABI для указателей неизвестного происхождения.)
Рассматривая случай, когда возвращаемый тип не ковариантен; или же:
Тривиальная ковариация
Пример: случай, когда он ковариантен с тем же типом, что и *this
; как в:
struct B { virtual B *f(); };
struct D : B { virtual D *f(); }; // trivial covariance
Здесь B
изначально является неизменным основным в D
: во всех когда-либо созданных D
(под) объектах B
находится по одному адресу: преобразование D*
в B*
тривиально, поэтому ковариация также тривиальна: это проблема статической типизации,
Всякий раз, когда это имеет место (тривиальное повышение), ковариация исчезает на уровне генерации кода.
Заключение
В этих случаях тип объявления переопределяющей функции тривиально отличается от типа базы:
- все параметры почти одинаковы (только с разницей в типе
this
) - тип возвращаемого значения почти одинаков (с возможной разницей в типе возвращаемого типа указателя (*))
(*), поскольку возврат ссылки точно такой же, как возврат указателя на уровне ABI, ссылки конкретно не обсуждаются
Таким образом, запись vtable для производного объявления не добавляется.
(Таким образом, сделать финал класса не было бы приемлемым упрощением.)
Никогда первичная база
Очевидно, что класс может иметь только один подобъект, содержащий конкретный элемент скалярных данных (например, vptr (*)), со смещением 0. Другие базовые классы с членами-скалярными данными будут иметь нетривиальное смещение, что требует нетривиальных производных для базовых преобразований. указателей. Таким образом, множественное интересное (**) наследование создаст неосновные базы
(*) Vptr не является обычным элементом данных на уровне пользователя; но в сгенерированном коде это в значительной степени обычный скалярный член данных, известный компилятору. (**) Компоновка неполиморфных баз здесь не интересна: для целей vtable ABI неполиморфная база обрабатывается как подобъект-член, так как она никак не влияет на vtables.
Концептуально простейший интересный пример неосновного и нетривиального преобразования указателей:
struct B1 { virtual void f(); };
struct B2 { virtual void f(); };
struct D : B1, B2 { };
Каждая база имеет свой собственный скалярный член vptr, и эти vptr имеют разные цели:
-
B1::vptr
указывает на структуруB1_vtable
-
B2::vptr
указывает на структуруB2_vtable
и они имеют идентичный макет (поскольку определения классов являются суперпозиционными, ABI должен генерировать суперпозиционные макеты); и они строго несовместимы, потому что
-
Vtables имеют различные записи:
-
B1_vtable.f_ptr
указывает на окончательное переопределение дляB1::f()
-
B2_vtable.f_ptr
указывает на окончательный переопределение дляB2::f()
-
-
B1_vtable.f_ptr
должен быть в том же смещении, что иB2_vtable.f_ptr
(из их соответствующих членов данныхB2_vtable.f_ptr
вB1
иB2
) - Конечные переопределения
B1::f()
иB2::f()
по своей природе (всегда, неизменно) не эквивалентны (*): они могут иметь различные конечные переопределения, которые делают разные вещи. (***)
(*) Две вызываемые функции времени выполнения (**) эквивалентны, если они имеют одинаковое наблюдаемое поведение на уровне ABI. (Эквивалентные вызываемые функции могут не иметь одно и то же объявление или типы C++.)
(**) Вызываемая функция времени выполнения - это любая точка входа: любой адрес, по которому можно вызвать/перейти; это может быть обычный код функции, блок/батут, конкретная запись в функции множественного ввода. Вызываемые во время выполнения функции часто не имеют возможных объявлений C++, например, "конечный переопределитель вызывается с указателем базового класса".
(***) Иногда они имеют одинаковое окончательное переопределение в следующем производном классе:
struct DD : D { void f(); }
не полезно с целью определения ABI D
Итак, мы видим, что D
несомненно, нуждается в не первичной полиморфной основе; условно это будет D2
; первая назначенная полиморфная основа (B1
) становится первичной.
Таким образом, B2
должен иметь нетривиальное смещение, а преобразование D
в B2
нетривиально: для этого требуется сгенерированный код.
Таким образом, параметры функции-члена D
не могут быть эквивалентны параметрам функции-члена B2
, так как неявное this
не является тривиально конвертируемым; так:
-
D
должно быть два разных vtables: vtable, соответствующийB1_vtable
и один сB2_vtable
(на практике они объединены в один большой vtable дляD
но концептуально они представляют собой две различные структуры). - запись виртуальной
D_B2_vtable
виртуального членаB2::g
которая переопределена вD
требует двух записей: одна вD_B2_vtable
(которая является просто компоновкойB2_vtable
с другими значениями) и одна вD_B1_vtable
которая является расширеннойB1_vtable
: плюсB1_vtable
записи для новых функций времени выполненияD
Поскольку D_B1_vtable
из B1_vtable
, указатель на D_B1_vtable
является тривиальным указателем на B1_vtable
, и значение B1_vtable
одинаково.
Обратите внимание, что в теории можно было бы опустить запись для D::g()
в D_B1_vtable
если бремя выполнения всех виртуальных вызовов D::g()
через базу B2
, что, если нет нетривиальной ковариации используется (#), также возможно.
(#) или если происходит нетривиальная ковариация, "виртуальная ковариация" (ковариация в производном отношении к основному, включающему виртуальное наследование) не используется
Не по своей сути первичная база
Обычное (не виртуальное) наследование просто как членство:
- не-виртуальный базовый подобъект - это прямая база ровно одного объекта (что подразумевает, что всегда существует только один конечный переопределитель любой виртуальной функции, когда виртуальное наследование не используется);
- фиксированное размещение не виртуальной базы;
- Базовый подобъект, который не имеет виртуальных базовых подобъектов, как член данных, создается точно так же, как завершенные объекты (у них есть ровно один код функции конструктора времени выполнения для каждого определенного конструктора C++).
Более тонкий случай наследования - это виртуальное наследование: виртуальный базовый подобъект может быть прямой основой многих подобъектов базового класса. Это означает, что расположение виртуальных баз определяется только на уровне самого производного класса: смещение виртуальной базы в наиболее производном объекте хорошо известно и постоянная времени компиляции; в произвольном объекте производного класса (который может быть или не быть самым производным объектом) это значение вычисляется во время выполнения.
Это смещение никогда не может быть известно, потому что C++ поддерживает как унифицирующее, так и дублирующее наследование:
- виртуальное наследование объединяет: все виртуальные базы данного типа в наиболее производном объекте являются одним и тем же подобъектом;
-
не виртуальное наследование дублирует: все косвенные не виртуальные базы семантически различны, поскольку их виртуальные члены не должны иметь общих окончательных переопределений (в отличие от Java, где это невозможно (AFAIK)):
struct B {виртуальная пустота f(); }; struct D1: B {виртуальная пустота f(); }; //окончательный переопределитель struct D2: B {virtual void f(); }; // финальная структура переопределения DD: D1, D2 {};
Здесь DD
имеет два различных окончательных переопределения B::f()
:
-
DD::D1::f()
является окончательным переопределением дляDD::D1::B::f()
-
DD::D2::f()
является окончательным переопределением дляDD::D2::B::f()
в двух разных записях.
Дублирование наследования, когда вы косвенно производите несколько раз из данного класса, подразумевает множество vptr, vtables и, возможно, отдельный конечный код vtable (конечная цель использования записи vtable: высокоуровневая семантика вызова виртуальной функции, а не точки входа),
Не только C++ поддерживает оба, но допустимы комбинации фактов: дублирование наследования класса, который использует унифицирующее наследование:
struct VB { virtual void f(); };
struct D : virtual VB { virtual void g(); int dummy; };
struct DD1 : D { void g(); };
struct DD2 : D { void g(); };
struct DDD : DD1, DD2 { };
Существует только один DDD::VB
но есть два различные Наблюдаемая D
субобъектов в DDD
с различным конечным overriders для D::g()
. Независимо от того, [или нет] C++ -подобный язык (который поддерживает виртуальное и не виртуальное семантическое наследование) гарантирует, что разные подобъекты имеют разные адреса, адрес DDD::DD1::D
не может совпадать с адресом DDD::DD2::D
Таким образом, смещение VB
в D
не может быть зафиксировано (на любом языке, который поддерживает унификацию и дублирование баз).
В этом конкретном примере реальный объект VB
(объект во время выполнения) не имеет конкретного элемента данных, кроме vptr, а vptr является специальным скалярным элементом, так как он является общим элементом типа "инвариант" (не const): он зафиксирован на конструктор (инвариант после завершения построения) и его семантика разделяются между базами и производными классами. Поскольку у VB
нет скалярного члена, который не является инвариантом типа, то в DDD
подобъект VB
может быть наложением на DDD::DD1::D
, если виртуальная таблица D
совпадает с виртуальной таблицей VB
.
Это, однако, не может иметь место для виртуальных баз, которые имеют неинвариантные скалярные элементы, то есть обычные элементы данных с идентичностью, то есть элементы, занимающие различный диапазон байтов: эти "реальные" элементы данных не могут быть наложены ни на что другое. Таким образом, виртуальный базовый подобъект с элементами данных (элементы с адресом, который гарантированно будет отличаться от C++ или любым другим C++ -подобным языком, который вы реализуете) должен быть размещен в отдельном месте: виртуальные базы с элементами данных обычно (##) имеют по своей сути нетривиальные смещения.
(##) с потенциально очень узким частным случаем с производным классом без элемента данных с виртуальной базой с некоторыми элементами данных
Итак, мы видим, что "почти пустые" классы (классы без элемента данных, но с vptr) являются особыми случаями, когда они используются в качестве виртуальных базовых классов: эти виртуальные базы являются кандидатами для наложения на производные классы, они являются потенциальными основными цветами, но не являются примитивными основными цветами:
- смещение, в котором они находятся, будет определяться только в самом производном классе;
- смещение может быть или не быть нулевым;
- нулевое смещение подразумевает наложение базы, поэтому виртуальная таблица каждого непосредственно полученного класса должна совпадать с виртуальной таблицей базы;
- ненулевое смещение подразумевает нетривиальные преобразования, поэтому записи в vtables должны обрабатывать преобразование указателей на виртуальную базу как нуждающееся в преобразовании во время выполнения (кроме случаев, когда они накладываются явно, так как в этом нет необходимости, что это невозможно).
Это означает, что при переопределении виртуальной функции в виртуальной базе всегда предполагается, что корректировка потенциально необходима, но в некоторых случаях корректировка не потребуется.
Морально виртуальная база - это отношение базового класса, которое включает в себя виртуальное наследование (возможно, плюс не виртуальное наследование). Выполнение преобразования производного в базовое, в частности, преобразование указателя d
в производное D
, в базовое B
, преобразование в...
-
... неморальная виртуальная база по своей сути обратима в каждом случае:
- существует взаимно-однозначное соотношение между идентичностью подобъекта
B
вD
иD
(который может быть самим подобъектом); - обратная операция может быть выполнена с помощью
static_cast<D*>
:static_cast<D*>((B*)d)
равноd
;
- существует взаимно-однозначное соотношение между идентичностью подобъекта
-
(в любом C++ подобном языке с полной поддержкой унификации и дублирования наследования)... морально виртуальная база по своей сути необратима в общем случае (хотя она обратима в общем случае с простыми иерархиями). Обратите внимание, что:
-
static_cast<D*>((B*)d)
плохо сформирован; -
dynamic_cast<D*>((B*)d)
будет работать для простых случаев.
-
Итак, пусть называется виртуальной ковариацией тот случай, когда ковариация возвращаемого типа основана на морально-виртуальной основе. При переопределении с виртуальной ковариацией соглашение о вызовах не может предполагать, что основание будет с известным смещением. Таким образом, новая запись vtable необходима для виртуальной ковариации, независимо от того, является ли переопределенное объявление первичным:
struct VB { virtual void f(); }; // almost empty
struct D : virtual VB { }; // VB is potential primary
struct Ba { virtual VB * g(); };
struct Da : Ba { // non virtual base, so Ba is inherent primary
D * g(); // virtually covariant: D->VB is morally virtual
};
Здесь VB
может иметь нулевое смещение в D
и никакая корректировка может не потребоваться (например, для полного объекта типа D
), но это не всегда так в подобъекте D
: при работе с указателями на D
невозможно знать будь то так.
Когда Da::g()
переопределяет Ba::g()
с помощью виртуальной ковариации, следует предполагать общий случай, поэтому для Da::g()
строго необходима новая запись vtable, поскольку невозможно преобразование нисходящего указателя из VB
в D
который обращает преобразование указателя D
в VB
в общем случае.
Ba
является неотъемлемой частью Da
поэтому семантика Ba::vptr
является общей/улучшенной:
- на этом скалярном члене есть дополнительные гарантии/инварианты, и vtable расширяется;
- Для
Da
нет нового vptr.
Таким образом, Da_vtable
(изначально совместимый с Ba_vtable
) требует двух разных записей для виртуальных вызовов g()
:
- в части
Ba_vtable
vtable:Ba::g()
vtable: вызывает окончательный переопределительBa::g()
с неявным параметром thisBa*
и возвращает значениеVB*
. - в части новых членов записи vtable:
Da::g()
vtable: вызывает окончательный переопределениеDa::g()
(которое по своей сути совпадает с окончательным переопределениемBa::g()
в C++) с неявным этим параметромDa*
и возвращает значениеD*
.
Обратите внимание, что на самом деле здесь нет никакой свободы ABI: основы дизайна vptr/vtable и их внутренние свойства подразумевают наличие этих нескольких записей для уникальной виртуальной функции на высоком уровне языка.
Обратите внимание, что создание тела виртуальной функции встроенным и видимым для ABI (чтобы ABI с помощью классов с различными определениями встроенных функций можно было сделать несовместимыми, что позволило бы получить больше информации для информирования о расположении памяти), возможно, не поможет, так как встроенный код будет только определить, что делает вызов не переопределенной виртуальной функции: нельзя основывать решения ABI на вариантах, которые могут быть переопределены в производных классах.
[Пример виртуальной ковариации, которая оказывается только тривиально ковариантной, так как в полном D
смещение для VB
является тривиальным, и в этом случае не потребовался бы никакой корректирующий код:
struct Da : Ba { // non virtual base, so inherent primary
D * g() { return new D; } // VB really is primary in complete D
// so conversion to VB* is trivial here
};
Обратите внимание, что в этом коде некорректная генерация кода для виртуального вызова с ошибочным компилятором, который будет использовать запись Ba_vtable
для вызова g()
, на самом деле будет работать, потому что ковариация оказывается тривиальной, так как VB
является первичным в полном D
Соглашение о вызовах относится к общему случаю, и такая генерация кода не удалась бы с кодом, который возвращает объект другого класса.
--end пример]
Но если Da::g()
является окончательным в ABI, только виртуальные вызовы могут быть сделаны через VB * g();
объявление: ковариация сделана чисто статической, преобразование производного в базовое выполняется во время компиляции как последний шаг виртуального блока, как если бы виртуальная ковариация никогда не использовалась.
Возможное продление финала
В C++ есть два типа виртуальности: функции-члены (соответствующие сигнатуре функции) и наследование (соответствие по имени класса). Если final перестает переопределять виртуальную функцию, может ли она применяться к базовым классам на языке, подобном C++?
Сначала нам нужно определить, что переопределяет наследование виртуальной базы:
"Почти прямое" отношение подобъекта означает, что косвенный подобъект контролируется почти как прямой подобъект:
- почти прямой подобъект может быть инициализирован как прямой подобъект;
- контроль доступа никогда не является действительно препятствием для доступа (недоступные частные почти прямые субобъекты могут быть доступны по усмотрению).
Виртуальное наследование обеспечивает практически прямой доступ:
- конструктор для каждой виртуальной базы должен вызываться ctor-init-list конструктора самого производного класса;
- когда виртуальный базовый класс недоступен из-за того, что объявлен как частный в базовом классе, или публично унаследован в частном базовом классе базового класса, производный класс может по своему усмотрению снова объявить виртуальную базу как виртуальную базу, сделав ее доступной.
Способ формализации переопределения виртуальной базы состоит в том, чтобы создать в каждом производном классе объявление мнимого наследования, которое переопределяет объявления виртуального наследования базового класса:
struct VB { virtual void f(); };
struct D : virtual VB { };
struct DD : D
// , virtual VB // imaginary overrider of D inheritance of VB
{
// DD () : VB() { } // implicit definition
};
Теперь варианты C++, которые поддерживают обе формы наследования, не должны иметь семантику C++ почти прямого доступа во всех производных классах:
struct VB { virtual void f(); };
struct D : virtual VB { };
struct DD : D, virtual final VB {
// DD () : VB() { } // implicit definition
};
Здесь виртуальность базы VB
заморожена и не может использоваться в дальнейших производных классах; виртуальность делается невидимой и недоступной для производных классов, а местоположение VB
фиксируется.
struct DDD : DD {
DD () :
VB() // error: not an almost direct subobject
{ }
};
struct DD2 : D, virtual final VB {
// DD2 () : VB() { } // implicit definition
};
struct Diamond : DD, DD2 // error: no unique final overrider
{ // for ": virtual VB"
};
Замораживание виртуальности делает незаконным объединение Diamond::DD::VB
и Diamond::DD2::VB
но виртуальность VB
требует объединения, что делает Diamond
противоречивым, недопустимым определением класса: ни один класс не может быть производным от обоих DD
и DD2
[аналог/пример: точно так же, как никакой полезный класс не может быть напрямую получен из A1
и A2
:
struct A1 {
virtual int f() = 0;
};
struct A2 {
virtual unsigned f() = 0;
};
struct UselessAbstract : A1, A2 {
// no possible declaration of f() here
// none of the inherited virtual functions can be overridden
// in UselessAbstract or any derived class
};
Здесь UselessAbstract
является абстрактным и не является производным классом, что делает этот ABC (абстрактный базовый класс) чрезвычайно глупым, поскольку любой указатель на UselessAbstract
доказуемо является нулевым указателем.
- конец аналога/пример]
Это обеспечило бы способ заморозить виртуальное наследование, обеспечить значимое частное наследование классов с виртуальной базой (без этого производные классы могут узурпировать отношения между классом и его частным базовым классом).
Такое использование final, конечно, замораживает местоположение виртуальной базы в производном классе и его дальнейших производных классах, избегая дополнительных записей vtable, которые необходимы только потому, что местоположение виртуальной базы не является фиксированным.