Всем привет! Перевод статьи подготовлен специально для студентов курса «Разработчик С++». Интересно развиваться в данном направлении? Приходите онлайн 13 декабря в 20:00 по мск. на мастер-класс «Практика использования Google Test Framework»!  В этой статье мы рассмотрим, как clang реализует vtables (таблицы виртуальных методов) и RTTI (идентификацию типа времени исполнения). В первой части мы начнем с базовых классов, а затем рассмотрим множественное и виртуальное наследование. Обратите внимание, что в этой статье нам предстоит покопаться в двоичном представлении, сгенерированном для различных частей нашего кода, с помощью gdb. Это довольно низкий уровень, но я сделаю всю тяжелую работу за вас. Я не думаю, что большинство будущих постов будут описывать детали такого низкого уровня.  отлично, тогда давайте начнем. Давайте рассмотрим следующий код: NonVirtualClass имеет размер 1 байт, потому что в C++ классы не могут иметь нулевой размер. Однако сейчас это не важно. Размер VirtualClass составляет 8 байт на 64-битной машине. Почему? Потому что внутри есть скрытый указатель, указывающий на vtable. vtables — это статические таблицы трансляции, создаваемые для каждого виртуального класса. Эта статья рассказывает об их содержании и о том, как они используются. Чтобы получить более глубокое понимание того, как выглядят vtables, давайте рассмотрим следующий код с помощью gdb, чтобы выяснить, как распределена память: Вот что мы узнали из вышеизложенного:
— Несмотря на то, что у классов нет членов данных, существует скрытый указатель на vtable;
— vtable для p1 и p2 одинаков. vtables — это статические данные для каждого типа;
— d1 и d2 наследуют vtable-указатель от Parent, который указывает на vtable Derived;
— Все vtables указывают на смещение 16 (0x10) байтов в vtable. Это мы также обсудим позже. Давайте продолжим нашу gdb-сессию, чтобы увидеть содержимое vtables. Я буду использовать команду x, которая выводит память на экран. Мы собираемся вывести 300 байтов в шестнадцатеричном формате, начиная с 0x400b40. Почему именно этот адрес? Потому что выше мы видели, что указатель vtable указывает на 0x400b50, а символ для этого адреса vtable for Derived+16 (16 == 0x10). Вот структура vtable Parent: Вот структура vtable Derived: 1: 2: 3: Помните, что указатель vtable в Derived указывал на смещение +16 байтов в vtable? Третий указатель является адресом указателя первого метода. Хотите третий метод? Нет проблем — добавьте 2  sizeof(void) к указателю vtable. Хотите запись typeinfo? перейдите к указателю перед ним. Двигаемся дальше — как насчет структуры записей typeinfo? Parent: А вот запись typeinfo Derived: 1: 2: 3: 4: Если вы хотите узнать больше о __si_class_type_info, вы можете найти некоторую информацию здесь, а также здесь. Это исчерпывает мои навыки с gdb, а также завершает эту часть. Я предполагаю, что некоторые люди сочтут это слишком низким уровнем, или, возможно, просто не имеющим практической ценности. Если это так, я бы порекомендовал пропустить части 2 и 3, перейдя прямо к части 4. Мир иерархий одиночного наследования проще для компилятора. Как мы видели в первой части, каждый дочерний класс расширяет родительский vtable, добавляя записи для каждого нового виртуального метода. Давайте рассмотрим множественное наследование, которое усложняет ситуацию, даже когда наследование реализуется только чисто от интерфейсов. Посмотрим на следующий фрагмент кода: Обратите внимание, что есть 2 указателя vtable. Интуитивно я бы ожидал 1 или 3 указателя (Mother, Father и Child). На самом деле невозможно иметь один указатель (подробнее об этом далее), и компилятор достаточно умен, чтобы объединять записи дочерней vtable Child как продолжение vtable Mother, сохраняя таким образом 1 указатель. Почему у Child не может быть одного указателя vtable для всех трех типов? Помните, что указатель Child может быть передан в функцию, принимающую указатель Mother или Father, и оба будут ожидать, что указатель this будет содержать правильные данные в правильных смещениях. Эти функции не обязательно должны знать о Child, и определенно не следует предполагать, что Child — это действительно то, что находится под указателем Mother/Father, которым они оперируют. (1) Не имеет отношения к этой теме, но, тем не менее, интересно, что child_data фактически помещается в заполнении Father. Это называется «tail padding» и может быть темой будущего поста. Вот структура vtable: В этом примере экземпляр Child будет иметь тот же указатель при приведении к указателю Mother. Но при приведении к указателю Father компилятор вычисляет смещение указателя this, чтобы указать на _vptr$Father часть Child (3-е поле в структуре Child, см. таблицу выше). Другими словами, для данного Child c;: (void)&c != (void)static_cast<Father*>(&c). Некоторые люди не ожидают этого, и, возможно, однажды эта информация сэкономит вам время на отладку.  Я находил это полезным не один раз. Но подождите, это еще не все. Что, если Child решил переопределить один из методов Father? Рассмотрим этот код: Ситуация становится сложнее. Функция может принимать аргумент Father* и вызывать FatherFoo() для него. Но если вы передадите экземпляр Child, ожидается, что он вызовет переопределенный метод Child с правильным указателем this. Тем не менее, вызывающий не знает, что он действительно содержит Child. Он имеет указатель на смещение Child, где находится расположение Father. Кто-то должен выполнить смещение указателя this, но как это сделать? Какую магию выполняет компилятор, чтобы заставить это работать? Прежде чем мы ответим на это, обратите внимание, что переопределение одного из методов Mother не очень мудрено, так как указатель this одинаков. Child знает, что нужно читать после vtable Mother, и ожидает, что методы Child будут сразу после нее. Вот решение: компилятор создает метод ”переходник” (thunk), который исправляет указатель this, а затем вызывает “настоящий” метод. Адрес метода-переходника будет находиться под vtable Father, в то время как “настоящий” метод будет под vtable Child. Вот vtable Child: Что означает: Объяснение: как мы видели ранее, у Child есть 2 vtables — одна используется для Mother и Child, а другая для Father. В vtable Father, FatherFoo() указывает на “переходник”, а в vtable Child указывает непосредственно на Child::FatherFoo(). А что в этом “переходнике”, спросите вы? Как мы уже обсуждали — смещения this и вызов FatherFoo(). И на сколько мы должны сместить this, чтобы получить Child? top_offset! Обратите внимание, что я лично считаю, что имя non-virtual thunk чрезвычайно запутанно, поскольку это запись в виртуальной таблице для виртуальной функции. Я не уверен, что тут не виртуального, но это только мое мнение. Пока на этом всё, в ближайшем будущем переведем 3 и 4 части. Следите за новостями! CRM/Email-маркетолог