Ответ 1
Страница "Заметки о множественном наследовании в компиляторе GCC С++ v4.0.1" теперь находится в автономном режиме и http://web.archive.org не архивировал его. Итак, я нашел копию текста в tinydrblog, который заархивирован в веб-архиве.
Существует полный текст оригинальных заметок, опубликованных в Интернете в разделе "Семинар по докторантуре: внутренние языки GCC "(осень 2005 г.) выпускник Morgan Deters "в лаборатории распределенных вычислений объектов в отделе компьютерных наук в Вашингтонском университете в Сент-Луисе."
Его (заархивированная) домашняя страница:
THIS IS THE TEXT by Morgan Deters and NOT CC-licensed. PART1:
Основы: одиночное наследование
Как мы обсуждали в классе, одиночное наследование приводит к компоновке объекта с данными базового класса, выложенными перед данными производного класса. Поэтому, если классы
A
иB
определяются следующим образом:class A { public: int a;
};
class B : public A { public: int b; };
тогда объекты типа
B
выкладываются следующим образом (где "b" является указателем на такой объект):b --> +-----------+ | a | +-----------+ | b | +-----------+
Если у вас есть виртуальные методы:
class A { public: int a; virtual void v(); }; class B : public A { public: int b; };
то у вас также будет указатель vtable:
+-----------------------+ | 0 (top_offset) | +-----------------------+ b --> +----------+ | ptr to typeinfo for B | | vtable |-------> +-----------------------+ +----------+ | A::v() | | a | +-----------------------+ +----------+ | b | +----------+
то есть
top_offset
и указатель типаinfo живут над местом, в которое указывает указатель vtable.Простое множественное наследование
Теперь рассмотрим множественное наследование:
class A { public: int a; virtual void v(); }; class B { public: int b; virtual void w(); }; class C : public A, public B { public: int c; };
В этом случае объекты типа C выкладываются следующим образом:
+-----------------------+ | 0 (top_offset) | +-----------------------+ c --> +----------+ | ptr to typeinfo for C | | vtable |-------> +-----------------------+ +----------+ | A::v() | | a | +-----------------------+ +----------+ | -8 (top_offset) | | vtable |---+ +-----------------------+ +----------+ | | ptr to typeinfo for C | | b | +---> +-----------------------+ +----------+ | B::w() | | c | +-----------------------+ +----------+
... но почему? Почему два vtables в одном? Ну, подумайте о замене типа. Если у меня есть указатель на C, я могу передать его функции, которая ожидает от указателя к-A или функции, которая ожидает от указателя к-B. Если функция ожидает указатель-на-A, и я хочу передать ему значение моей переменной c (типа pointer-to-C), я уже настроен. Вызовы
A::v()
могут быть выполнены с помощью (первой) таблицы vtable, и вызываемая функция может получить доступ к элементу a через указатель, который я передаю, таким же образом, как он может через любой указатель-на-A.Однако, если я передам значение моей переменной-указателю
c
функции, которая ожидает указателя-на-B, нам также понадобится подобъект типа B в нашем C для ссылки на него. Вот почему у нас есть второй указатель vtable. Мы можем передать значение указателя (c + 8 байтов) функции, ожидающей от указателя к-B, и все это задано: он может совершать вызовыB::w()
через (v) указатель (второй) vtable и обращаться к члену b через указатель мы проходим так же, как и через любой указатель-на-B.Обратите внимание, что эта "коррекция указателя" также должна возникать для вызываемых методов. Класс
c
наследуетB::w()
в этом случае. Когдаw()
вызывается через указатель-на-C, указатель (который становится этим указателем внутриw()
) должен быть скорректирован. Это часто называют этой корректировкой указателя.В некоторых случаях компилятор будет генерировать thunk для исправления адреса. Рассмотрим тот же код, что и выше, но на этот раз
c
переопределяетB
функцию-членw()
:class A { public: int a; virtual void v(); }; class B { public: int b; virtual void w(); }; class C : public A, public B { public: int c; void w(); };
c
layout объекта и vtable теперь выглядят следующим образом:+-----------------------+ | 0 (top_offset) | +-----------------------+ c --> +----------+ | ptr to typeinfo for C | | vtable |-------> +-----------------------+ +----------+ | A::v() | | a | +-----------------------+ +----------+ | C::w() | | vtable |---+ +-----------------------+ +----------+ | | -8 (top_offset) | | b | | +-----------------------+ +----------+ | | ptr to typeinfo for C | | c | +---> +-----------------------+ +----------+ | thunk to C::w() | +-----------------------+
Теперь, когда
w()
вызывается в экземпляреc
через указатель-на-B, вызывается thunk. Что делает тнк? Разберите его (здесь, сgdb
):0x0804860c <_ZThn8_N1C1wEv+0>: addl $0xfffffff8,0x4(%esp) 0x08048611 <_ZThn8_N1C1wEv+5>: jmp 0x804853c <_ZN1C1wEv>
Поэтому он просто настраивает указатель
this
и переходит наC::w()
. Все хорошо.Но не означает ли это, что
B
vtable всегда указывает на этотC::w()
thunk? Я имею в виду, если у нас есть указатель-на-B, который является законнымB
(а неc
), мы не хотим вызывать thunk, правильно?Right. Вышеприведенная встроенная таблица vtable для
B
вc
является специальной для случая B-in-C. B обычный vtable является нормальным и непосредственно указывает наB::w()
.Алмаз: множественные копии базовых классов (не виртуальное наследование)
Хорошо. Теперь нужно заняться действительно тяжелым делом. Вспомним обычную проблему множественных копий базовых классов при формировании алмаза наследования:
class A { public: int a; virtual void v(); }; class B : public A { public: int b; virtual void w(); }; class C : public A { public: int c; virtual void x(); }; class D : public B, public C { public: int d; virtual void y(); };
Обратите внимание, что
D
наследует как отB
, так иc
, аB
иc
оба наследуются отA
. Это означает, чтоD
имеет две копииA
. Макет объекта и встраивание vtable - это то, чего мы ожидаем от предыдущих разделов:+-----------------------+ | 0 (top_offset) | +-----------------------+ d --> +----------+ | ptr to typeinfo for D | | vtable |-------> +-----------------------+ +----------+ | A::v() | | a | +-----------------------+ +----------+ | B::w() | | b | +-----------------------+ +----------+ | D::y() | | vtable |---+ +-----------------------+ +----------+ | | -12 (top_offset) | | a | | +-----------------------+ +----------+ | | ptr to typeinfo for D | | c | +---> +-----------------------+ +----------+ | A::v() | | d | +-----------------------+ +----------+ | C::x() | +-----------------------+
Конечно, мы ожидаем, что данные
A
(членA
) будут существовать дважды в макете объектовD
(и это так), и мы ожидаем, чтоA
виртуальные функции-члены будут представлены дважды в таблице vtable (иA::v()
действительно существует). Ладно, здесь ничего нового.Алмаз: одиночные копии виртуальных баз
Но что, если мы будем применять виртуальное наследование? Виртуальное наследование С++ позволяет нам указывать иерархию алмазов, но гарантируется только одна копия фактически унаследованных баз. Поэтому напишите наш код следующим образом:
class A { public: int a; virtual void v(); }; class B : public virtual A { public: int b; virtual void w(); }; class C : public virtual A { public: int c; virtual void x(); }; class D : public B, public C { public: int d; virtual void y(); };
Внезапно все становится намного сложнее. Если в нашем представлении
D
мы можем иметь только одну копиюA
, то мы уже не можем уйти от нашего "трюка" вложения ac
вD
(и вложение vtable дляc
частьD
вD
vtable). Но как мы можем обрабатывать обычную подстановку типов, если мы не можем это сделать?Попробуйте установить схему:
+-----------------------+ | 20 (vbase_offset) | +-----------------------+ | 0 (top_offset) | +-----------------------+ | ptr to typeinfo for D | +----------> +-----------------------+ d --> +----------+ | | B::w() | | vtable |----+ +-----------------------+ +----------+ | D::y() | | b | +-----------------------+ +----------+ | 12 (vbase_offset) | | vtable |---------+ +-----------------------+ +----------+ | | -8 (top_offset) | | c | | +-----------------------+ +----------+ | | ptr to typeinfo for D | | d | +-----> +-----------------------+ +----------+ | C::x() | | vtable |----+ +-----------------------+ +----------+ | | 0 (vbase_offset) | | a | | +-----------------------+ +----------+ | | -20 (top_offset) | | +-----------------------+ | | ptr to typeinfo for D | +----------> +-----------------------+ | A::v() | +-----------------------+
Хорошо. Итак, вы видите, что
A
теперь встроен вD
по существу так же, как и другие базы. Но он встроен в D, а не inits непосредственно производные классы.