Ответ 1
Эта часть стандарта просто говорит вам, что когда вы создаете какой-то "большой" объект J
, чья иерархия базового класса включает в себя множественное наследование, а вы сейчас сидите внутри конструктора некоторого базового подобъекта H
, тогда вам разрешено использовать полиморфизм H
и его прямых и косвенных базовых подобъектов. Вам не разрешено использовать какой-либо полиморфизм вне этой иерархии.
Например, рассмотрим эту диаграмму наследования (стрелки указывают из производных классов на базовые классы)
Скажем, мы строим "большой" объект типа J
. И в настоящее время мы выполняем конструктор класса H
. Внутри конструктора H
вам разрешено пользоваться типичным ограниченным конструктором полиморфизмом суб-иерархии внутри красного овала. Например, вы можете вызывать виртуальные функции базового подобъекта типа B
, а полиморфное поведение будет работать, как и ожидалось, внутри обведенной иерархии ( "как ожидалось" ) означает, что полиморфное поведение будет меньше, чем H
в иерархии, но не ниже). Вы также можете вызвать виртуальные функции A
, E
, X
и другие подобъекты, попадающие в красный овал.
Однако, если вы каким-то образом получаете доступ к иерархии вне овала и пытаетесь использовать там полиморфизм, поведение становится undefined. Например, если вы каким-то образом получаете доступ к подобъекту G
из конструктора H
и пытаетесь вызвать виртуальную функцию G
- поведение undefined. То же самое можно сказать о вызове виртуальных функций D
и I
из конструктора H
.
Единственный способ получить такой доступ к "внешней" суб-иерархии - это если кто-то каким-то образом передал указатель/ссылку на подобъект G
в конструктор H
. Следовательно, ссылка на "явный доступ к членам класса" в стандартном тексте (хотя это кажется чрезмерным).
Стандарт включает в себя виртуальное наследование в примере, чтобы продемонстрировать, насколько это включено это правило. В приведенной выше диаграмме базовый подобъект X
разделяется как суб-иерархией внутри овала, так и суб-иерархией вне овала. В стандарте говорится, что нормально вызвать виртуальные функции субъекта X
из конструктора H
.
Обратите внимание, что это ограничение применяется, даже если конструкция субъектов D
, G
и I
закончена до начала построения H
.
Корни этой спецификации приводят к практическому рассмотрению внедрения полиморфного механизма. В практических реализациях указатель VMT вводится как поле данных в макет объекта самых основных полиморфных классов в иерархии. Производные классы не представляют своих собственных указателей VMT, они просто предоставляют свои собственные значения для указателей, введенных базовыми классами (и, возможно, более длительными VMT).
Взгляните на пример из стандарта. Класс A
выводится из класса V
. Это означает, что указатель VMT A
физически относится к подобъекту V
. Все вызовы виртуальных функций, введенные V
, отправляются через указатель VMT, введенный V
. То есть когда вы вызываете
pointer_to_A->f();
он фактически переведен в
V *v_subobject = (V *) pointer_to_A; // go to V
vmt = v_subobject->vmt_ptr; // retrieve the table
vmt[index_for_f](); // call through the table
Однако в примере из стандарта тот же самый подобъект V
также встроен в B
. Чтобы сделать корректный полиморфизм, ограниченный конструктором, компилятор поместит указатель на B
VMT в указатель VMT, хранящийся в V
(поскольку в то время как конструктор B
активен V
подобъект должен действовать как часть B
).
Если в этот момент вы как-то пытаетесь вызвать
a->f(); // as in the example
приведенный выше алгоритм найдет B
указатель VMT, сохраненный в подобъекте V
, и попытается вызвать f()
через этот VMT. Это явно не имеет никакого смысла. То есть с виртуальными методами A
, отправленными через B
VMT, нет смысла. Поведение undefined.
Это довольно просто проверить с помощью практического эксперимента. Пусть добавьте свою собственную версию f
в B
и сделайте это
#include <iostream>
struct V {
virtual void f() { std::cout << "V" << std::endl; }
};
struct A : virtual V {
virtual void f() { std::cout << "A" << std::endl; }
};
struct B : virtual V {
virtual void f() { std::cout << "B" << std::endl; }
B(V*, A*);
};
struct D : A, B {
virtual void f() {}
D() : B((A*)this, this) { }
};
B::B(V* v, A* a) {
a->f(); // What `f()` is called here???
}
int main() {
D d;
}
Вы ожидаете, что здесь будет вызываться A::f
? Я попробовал несколько компиляторов, все они на самом деле называют B::f
! Между тем, значение указателя this
B::f
, принимаемое в таком вызове, является полностью фиктивным.
Это происходит именно по причинам, описанным выше (большинство компиляторов реализуют полиморфизм так, как я описал выше). Именно по этой причине язык описывает такие вызовы, как undefined.
Можно заметить, что в этом конкретном примере фактически виртуальное наследование приводит к этому необычному поведению. Да, это происходит именно потому, что подобъект V
делится между подобъектами A
и B
. Вполне возможно, что без виртуального наследования поведение было бы гораздо более предсказуемым. Однако спецификация языка, по-видимому, решила просто нарисовать линию так, как она нарисована на моей диаграмме: когда вы строите H
, вам не удастся выйти из "песочницы" суб-иерархии H
, независимо от того, какой тип наследования используется.