Что такое VTT для класса?
Недавно натолкнулся на ошибку компоновщика C++, которая была для меня новой.
libfoo.so: undefined reference to `VTT for Foo'
libfoo.so: undefined reference to `vtable for Foo'
Я обнаружил ошибку и исправил свою проблему, но у меня все еще есть вопрос: что такое VTT?
Кроме того: Для тех, кто заинтересован, проблема возникает, когда вы забыли определить первую виртуальную функцию, объявленную в классе. Vtable входит в модуль компиляции первой виртуальной функции класса. Если вы забудете определить эту функцию, вы получите ошибку компоновщика, из-за которой он не может найти vtable, а гораздо более дружественный для разработчика не может найти функцию.
6 ответов
Страница "Примечания о множественном наследовании в GCC C++ Compiler v4.0.1" теперь недоступна, и http://web.archive.org/ не заархивировал ее. Итак, я нашел копию текста в tinydrblog, который заархивирован в веб-архиве.
Полный текст оригинальных Заметок опубликован онлайн как часть " Семинара по докторскому языку программирования: GCC Internals " (осень 2005 г.) выпускника Моргана Детерса "в Лаборатории вычислительных распределенных объектов на факультете компьютерных наук Вашингтонского университета в Сент-Луисе."
Его (заархивированная) домашняя страница:
THIS IS THE TEXT by Morgan Deters and NOT CC-licensed.
Морган Детерс веб-страницы:
- http://web.archive.org/web/20060908050947/http://www.cse.wustl.edu/~mdeters/,
- Его биография http://web.archive.org/web/20060910122623/http://www.cse.wustl.edu/~mdeters/bio/,
- Профиль Google Scholar: https://scholar.google.com/citations?user=DsD8SDYAAAAJ&hl=en&oi=sra
- Более свежая веб-страница https://cs.nyu.edu/~mdeters/
- Некролог от 2015 года: http://www.sent-trib.com/obituaries/dr-morgan-g-deters/article_70a9b22a-a307-11e4-ba08-476415c7fb1c.html " Доктор Морган Г. Детерс, доктор философии, 35 лет, ранее Боулинг-Грин и совсем недавно в Бруклине, штат Нью-Йорк, умерли в субботу 17 января 2015 года в Тобаго, Тринидад ".
- http://cvc4.cs.stanford.edu/web/in-memoriam-morgan-deters/
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
и указатель typeinfo находится над тем местом, на которое указывает указатель 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()
через (второй) указатель vtable и доступ к члену b через указатель, который мы передаем так же, как и через любой указатель на B.Обратите внимание, что это "исправление указателя" должно происходить и для вызываемых методов. Учебный класс
C
наследуетсяB::w()
в этом случае. когдаw()
вызывается через указатель на C, указатель (который становится указателем this внутри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
Теперь объектный макет и 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. Что делает Thunk? Давайте разберем его (здесь, сgdb
):0x0804860c <_ZThn8_N1C1wEv+0>: addl $0xfffffff8,0x4(%esp) 0x08048611 <_ZThn8_N1C1wEv+5>: jmp 0x804853c <_ZN1C1wEv>
Так что это просто корректирует
this
указатель и прыгает наC::w()
, Все хорошо.Но не означает ли это, что
B
Vtable всегда указывает на этоC::w()
санк? Я имею в виду, если у нас есть указатель на B, который на законных основанияхB
(неC
), мы не хотим вызывать Thunk, верно?Правильно. Вышеуказанный встроенный vtable для
B
вC
является особенным для случая B-in-C. Обычный vtable B нормален и указывает на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()
действительно есть). Ладно, ничего нового здесь.Бриллиант: отдельные копии виртуальных баз
Но что, если мы применяем виртуальное наследование? Виртуальное наследование C++ позволяет нам определять алмазную иерархию, но гарантируется только одна копия виртуально унаследованных баз. Итак, давайте напишем наш код следующим образом:
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(); };
Внезапно все становится намного сложнее. Если мы можем иметь только одну копию
A
в нашем представленииD
, тогда мы больше не можем сойти с нашего "трюка" встраиванияC
в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, а не в начальные классы.
THIS IS THE TEXT by Morgan Deters and NOT CC-licensed.
Морган Детерс веб-страницы:
- http://web.archive.org/web/20060908050947/http://www.cse.wustl.edu/~mdeters/,
- Его биография http://web.archive.org/web/20060910122623/http://www.cse.wustl.edu/~mdeters/bio/,
- Профиль Google Scholar: https://scholar.google.com/citations?user=DsD8SDYAAAAJ&hl=en&oi=sra
- Более свежая веб-страница https://cs.nyu.edu/~mdeters/
- Некролог от 2015 года: http://www.sent-trib.com/obituaries/dr-morgan-g-deters/article_70a9b22a-a307-11e4-ba08-476415c7fb1c.html "Доктор Морган Г. Детерс, доктор философии, 35 лет, ранее Боулинг-Грин и совсем недавно в Бруклине, штат Нью-Йорк, умерли в субботу 17 января 2015 года в Тобаго, Тринидад".
- http://cvc4.cs.stanford.edu/web/in-memoriam-morgan-deters/
ЧАСТЬ 2:
Строительство / Разрушение в присутствии множественного наследования
Как вышеуказанный объект создается в памяти, когда создается сам объект? И как мы можем гарантировать, что частично сконструированный объект (и его vtable) безопасны для работы конструкторов?
К счастью, для нас все это очень осторожно. Скажем, мы создаем новый объект типа
D
(например, черезnew D
). Во-первых, память для объекта выделяется в куче и возвращается указатель.D
вызывается конструктор, но перед выполнением любогоD
-специфическая конструкция это называетсяA
конструктор объекта (после настройкиthis
указатель, конечно!).A
конструктор заполняетA
частьD
объект, как если бы это был экземплярA
,d --> +----------+ | | +----------+ | | +----------+ | | +----------+ | | +-----------------------+ +----------+ | 0 (top_offset) | | | +-----------------------+ +----------+ | ptr to typeinfo for A | | vtable |-----> +-----------------------+ +----------+ | A::v() | | a | +-----------------------+ +----------+
Контроль возвращается
D
конструктор, который вызываетB
конструктор. (Настройка указателя здесь не нужна.) КогдаB
конструктор готов, объект выглядит так:B-in-D +-----------------------+ | 20 (vbase_offset) | +-----------------------+ | 0 (top_offset) | +-----------------------+ d --> +----------+ | ptr to typeinfo for B | | vtable |------> +-----------------------+ +----------+ | B::w() | | b | +-----------------------+ +----------+ | 0 (vbase_offset) | | | +-----------------------+ +----------+ | -20 (top_offset) | | | +-----------------------+ +----------+ | ptr to typeinfo for B | | | +--> +-----------------------+ +----------+ | | A::v() | | vtable |---+ +-----------------------+ +----------+ | a | +----------+
Но ждать...
B
конструктор модифицировалA
часть объекта, изменив его указатель vtable! Как он узнал, что нужно отличать этот вид B-in-D от B-in-что-то еще (или отдельногоB
в этом отношении)? Просто. Стол виртуальной таблицы сказал это сделать. Эта структура, сокращенно VTT, представляет собой таблицу таблиц, используемых в строительстве. В нашем случае VTT дляD
выглядит так:B-in-D +-----------------------+ | 20 (vbase_offset) | VTT for D +-----------------------+ +-------------------+ | 0 (top_offset) | | vtable for D |-------------+ +-----------------------+ +-------------------+ | | ptr to typeinfo for B | | vtable for B-in-D |-------------|----------> +-----------------------+ +-------------------+ | | B::w() | | vtable for B-in-D |-------------|--------+ +-----------------------+ +-------------------+ | | | 0 (vbase_offset) | | vtable for C-in-D |-------------|-----+ | +-----------------------+ +-------------------+ | | | | -20 (top_offset) | | vtable for C-in-D |-------------|--+ | | +-----------------------+ +-------------------+ | | | | | ptr to typeinfo for B | | vtable for D |----------+ | | | +-> +-----------------------+ +-------------------+ | | | | | A::v() | | vtable for D |-------+ | | | | +-----------------------+ +-------------------+ | | | | | | | | | | C-in-D | | | | | +-----------------------+ | | | | | | 12 (vbase_offset) | | | | | | +-----------------------+ | | | | | | 0 (top_offset) | | | | | | +-----------------------+ | | | | | | ptr to typeinfo for C | | | | | +----> +-----------------------+ | | | | | C::x() | | | | | +-----------------------+ | | | | | 0 (vbase_offset) | | | | | +-----------------------+ | | | | | -12 (top_offset) | | | | | +-----------------------+ | | | | | ptr to typeinfo for C | | | | +-------> +-----------------------+ | | | | A::v() | | | | +-----------------------+ | | | | | | D | | | +-----------------------+ | | | | 20 (vbase_offset) | | | | +-----------------------+ | | | | 0 (top_offset) | | | | +-----------------------+ | | | | ptr to typeinfo for D | | | +----------> +-----------------------+ | | | B::w() | | | +-----------------------+ | | | D::y() | | | +-----------------------+ | | | 12 (vbase_offset) | | | +-----------------------+ | | | -8 (top_offset) | | | +-----------------------+ | | | ptr to typeinfo for D | +----------------> +-----------------------+ | | C::x() | | +-----------------------+ | | 0 (vbase_offset) | | +-----------------------+ | | -20 (top_offset) | | +-----------------------+ | | ptr to typeinfo for D | +-------------> +-----------------------+ | A::v() | +-----------------------+
Конструктор D передает указатель в VTT D на конструктор B (в этом случае он передает адрес первой записи B-in-D). И действительно, vtable, который использовался для макета объекта выше, является специальным vtable, используемым только для построения B-in-D.
Элемент управления возвращается конструктору D и вызывает конструктор C (с параметром адреса VTT, указывающим на запись "C-in-D+12"). Когда конструктор C сделан с объектом, он выглядит так:
B-in-D +-----------------------+ | 20 (vbase_offset) | +-----------------------+ | 0 (top_offset) | +-----------------------+ | ptr to typeinfo for B | +---------------------------------> +-----------------------+ | | B::w() | | +-----------------------+ | C-in-D | 0 (vbase_offset) | | +-----------------------+ +-----------------------+ d --> +----------+ | | 12 (vbase_offset) | | -20 (top_offset) | | vtable |--+ +-----------------------+ +-----------------------+ +----------+ | 0 (top_offset) | | ptr to typeinfo for B | | b | +-----------------------+ +-----------------------+ +----------+ | ptr to typeinfo for C | | A::v() | | vtable |--------> +-----------------------+ +-----------------------+ +----------+ | C::x() | | c | +-----------------------+ +----------+ | 0 (vbase_offset) | | | +-----------------------+ +----------+ | -12 (top_offset) | | vtable |--+ +-----------------------+ +----------+ | | ptr to typeinfo for C | | a | +-----> +-----------------------+ +----------+ | A::v() | +-----------------------+
Как вы видите, конструктор C снова изменил указатель vtable встроенного A-объекта. Встраиваемые объекты C и A теперь используют специальную конструкцию C-in-D vtable, а встроенный объект B использует специальную конструкцию B-in-D vtable. Наконец, конструктор D завершает работу, и мы получаем ту же диаграмму, что и раньше:
+-----------------------+ | 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() | +-----------------------+
Разрушение происходит таким же образом, но в обратном порядке. Деструктор D вызывается. После запуска кода уничтожения пользователя деструктор вызывает деструктор C и предписывает ему использовать соответствующую часть VTT D. Деструктор Си манипулирует указателями vtable так же, как и во время конструирования; то есть соответствующие указатели vtable теперь указывают на v-конструкцию C-in-D. Затем он запускает код уничтожения пользователя для C и возвращает управление деструктору D, который затем вызывает деструктор B со ссылкой на VTT D. Деструктор B устанавливает соответствующие части объекта для ссылки на виртуальную таблицу конструкции B-in-D. Он запускает код уничтожения пользователя для B и возвращает управление деструктору D, который, наконец, вызывает деструктор A. Деструктор A изменяет виртуальную таблицу для части A объекта, чтобы она ссылалась на виртуальную таблицу для A. Наконец, управление возвращается к деструктору D, и уничтожение объекта завершено. Память, однажды использованная объектом, возвращается в систему.
Теперь, на самом деле, история несколько сложнее. Вы когда-нибудь видели эти "ответственные" и "не отвечающие" спецификации конструкторов и деструкторов в сообщениях о предупреждениях и ошибках, создаваемых GCC, или в двоичных файлах, создаваемых GCC? Дело в том, что может быть две реализации конструктора и до трех реализаций деструктора.
Конструктор "ответственный" (или полный объект) - это конструктор виртуальных баз, а конструктор "не отвечающий" (или базовый объект) - это конструктор, который этого не делает. Рассмотрим наш пример выше. Если B создается, его конструктор должен вызвать конструктор A, чтобы построить его. Точно так же конструктор C должен построить A. Однако, если B и C построены как часть конструкции D, их конструкторы не должны конструировать A, потому что A является виртуальной базой, а конструктор D позаботится о ее построении ровно один раз. для случая D. Рассмотрим случаи:
Если вы делаете новый A, вызывается "ответственный" конструктор A, чтобы создать A. Когда вы делаете новый B, вызывается "ответственный" конструктор B. Он вызовет конструктор "не отвечает" для А.
новый C похож на новый B.
Новый D вызывает "ответственный" конструктор D. Прошелся по этому примеру. "Ответственный" конструктор D вызывает "незавершенные" версии конструкторов A, B и C (в том же порядке).
"Ответственный" деструктор является аналогом "ответственного" конструктора - он отвечает за разрушение виртуальных баз. Точно так же деструктор "не отвечает" генерируется. Но есть и третий. Деструктор "ответственного за удаление" - это деструктор, который освобождает память, а также разрушает объект. Итак, когда один называется предпочтением другого?
Ну, есть два вида объектов, которые могут быть уничтожены - те, которые расположены в стеке, и те, которые размещены в куче. Рассмотрим этот код (учитывая нашу алмазную иерархию с виртуальным наследованием ранее):
D d; // allocates a D on the stack and constructs it D *pd = new D; // allocates a D in the heap and constructs it /* ... */ delete pd; // calls "in-charge deleting" destructor for D return; // calls "in-charge" destructor for stack-allocated D
Мы видим, что фактический оператор удаления вызывается не кодом, выполняющим удаление, а скорее деструктором удаления ответственного за удаляемый объект. Почему это так? Почему бы не вызвать вызывающего вызывающего деструктора, а затем удалить объект? Тогда у вас будет только две копии реализации деструктора вместо трех...
Что ж, компилятор может сделать такую вещь, но это будет более сложным по другим причинам. Рассмотрим этот код (в предположении виртуального деструктора, который вы всегда используете, верно?... правильно?!?):
D *pd = new D; // allocates a D in the heap and constructs it C *pc = d; // we have a pointer-to-C that points to our heap-allocated D /* ... */ delete pc; // call destructor thunk through vtable, but what about delete?
Если у вас не было варианта "ответственного удаления" деструктора D, то операция удаления должна была бы настроить указатель так же, как это делает деструктор thunk. Помните, что объект C встроен в D, и поэтому наш указатель на C выше настроен так, чтобы указывать на середину объекта D. Мы не можем просто удалить этот указатель, поскольку это не указатель, который был вернулся
malloc()
когда мы построили это.Таким образом, если бы у нас не было ответственного за удаление деструктора, нам бы пришлось иметь дело с оператором удаления (и представлять их в наших таблицах) или что-то подобное.
Thunks, виртуальные и не виртуальные
Этот раздел еще не написан.
Множественное наследование с виртуальными методами на одной стороне
Хорошо. Последнее упражнение. Что если у нас есть иерархия наследования алмазов с виртуальным наследованием, как и раньше, но с виртуальными методами только по одной ее стороне? Так:
class A { public: int a; }; class B : public virtual A { public: int b; virtual void w(); }; class C : public virtual A { public: int c; }; class D : public B, public C { public: int d; virtual void y(); };
В этом случае макет объекта выглядит следующим образом:
+-----------------------+ | 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 | +-----> +-----------------------+ +----------+ | a | +----------+
Таким образом, вы можете видеть, что подобъект C, у которого нет виртуальных методов, все еще имеет vtable (хотя и пустой). Действительно, все экземпляры C имеют пустой vtable.
Спасибо, Морган Детерс!!
Таблица виртуальных таблиц (VTT). Это позволяет безопасно создавать / деконструировать объекты при множественном наследовании.
для объяснения см.: http://www.cse.wustl.edu/~mdeters/seminar/fall2005/mi.html
VTT = таблица виртуальных таблиц.
Это виртуальная (функциональная) таблица и таблица виртуальных типов, которая обрабатывает множественное наследование.
сравни:
http://www.codesourcery.com/archives/cxx-abi-dev/msg00077.html http://www.cse.wustl.edu/~mdeters/seminar/fall2005/mi.html
Таблица виртуальных таблиц, сокращенно VTT, представляет собой таблицу виртуальных таблиц, используемых в конструкции, где используется множественное наследование.
Более подробная информация здесь в этой интересной статье: Примечания о множественном наследовании в GCC C++ Compiler v4.0.1