Что такое виртуальный базовый класс в C ++?

403

Я хочу знать, что такое " виртуальный базовый класс" » и что это значит.

Позвольте мне показать пример:

class Foo
{
public:
    void DoSomething() { /* ... */ }
};

class Bar : public virtual Foo
{
public:
    void DoSpecific() { /* ... */ }
};
popopome
источник
мы должны использовать виртуальные базовые классы в «множественном наследовании», потому что, если класс A имеет переменную-член int a, а класс B также имеет член int a, а класс c наследует класс A и B, как мы решаем, какой «a» использовать?
Намит Синха
2
@NamitSinha нет, виртуальное наследование не решает эту проблему. В любом случае член А был бы неоднозначным
Ichthyo
@NamitSinha Виртуальное наследование не является волшебным инструментом для устранения неоднозначностей, связанных с множественным наследованием. Это «решает» «проблему» наличия косвенной базы более одного раза. Что является проблемой только в том случае, если оно предназначено для совместного использования (часто, но не всегда).
любопытный парень

Ответы:

533

Виртуальные базовые классы, используемые в виртуальном наследовании, являются способом предотвращения появления нескольких «экземпляров» данного класса в иерархии наследования при использовании множественного наследования.

Рассмотрим следующий сценарий:

class A { public: void Foo() {} };
class B : public A {};
class C : public A {};
class D : public B, public C {};

Приведенная выше иерархия классов приводит к «страшному алмазу», который выглядит следующим образом:

  A
 / \
B   C
 \ /
  D

Экземпляр D будет состоять из B, который включает в себя A, и C, который также включает в себя A. Таким образом, у вас есть два «экземпляра» (для лучшего выражения) из A.

Когда у вас есть этот сценарий, у вас есть возможность двусмысленности. Что происходит, когда вы делаете это:

D d;
d.Foo(); // is this B's Foo() or C's Foo() ??

Виртуальное наследование призвано решить эту проблему. Когда вы указываете virtual при наследовании ваших классов, вы сообщаете компилятору, что вам нужен только один экземпляр.

class A { public: void Foo() {} };
class B : public virtual A {};
class C : public virtual A {};
class D : public B, public C {};

Это означает, что в иерархию включен только один «экземпляр» A. следовательно

D d;
d.Foo(); // no longer ambiguous

Это мини-резюме. Для получения дополнительной информации, прочитайте это и это . Хороший пример также доступен здесь .

OJ.
источник
7
@ Bohdan нет, это не так :)
OJ.
6
@OJ. почему бы нет? Они веселые :)
Богдан
15
@Bohdan использует виртуальное ключевое слово столько же, сколько и меньше, потому что когда мы используем виртуальное ключевое слово, применяется механизм большого веса. Таким образом, эффективность вашей программы будет снижена.
Сагар
73
Ваша диаграмма «страшный алмаз» сбивает с толку, хотя, кажется, она широко используется. На самом деле это диаграмма, показывающая отношения наследования классов, а не макет объекта. Заблуждение заключается в том, что если мы используем virtual, то расположение объекта выглядит как ромб; и если мы не используем, virtualто расположение объекта выглядит как древовидная структура, которая содержит два As
ММ
5
Я должен понизить этот ответ по причине, изложенной ММ - диаграмма выражает противоположность посту.
Дэвид Стоун
251

О расположении памяти

В качестве примечания, проблема с Dreaded Diamond заключается в том, что базовый класс присутствует несколько раз. Таким образом, с регулярным наследованием, вы полагаете, что имеете:

  A
 / \
B   C
 \ /
  D

Но в макете памяти у вас есть:

A   A
|   |
B   C
 \ /
  D

Это объясняет, почему при звонке у D::foo()вас возникает проблема неоднозначности. Но настоящая проблема возникает, когда вы хотите использовать переменную-член A. Например, скажем, у нас есть:

class A
{
    public :
       foo() ;
       int m_iValue ;
} ;

Когда вы будете пытаться получить доступ m_iValueиз Dкомпилятор будет протестовать, потому что в иерархии, то увидит два m_iValue, а не один. И если вы измените один, скажем, B::m_iValue(это A::m_iValueродительB ), C::m_iValueне будет изменен (это A::m_iValueродительC ).

Вот где виртуальное наследование становится полезным, так как с его помощью вы вернетесь к истинному алмазному макету, а не только одному foo() метод, но также один и только одинm_iValue .

Что может пойти не так?

Представить:

  • A имеет некоторые основные функции.
  • B добавляет к этому какой-то классный массив данных (например)
  • C добавляет к этому некоторые интересные функции, такие как шаблон наблюдателя (например, на m_iValue ).
  • Dнаследует от Bи C, следовательно, от A.

С нормальным наследованием, изменяя m_iValueизD является неоднозначным, и это должно быть решено. Даже если это так, есть два m_iValuesвнутри D, так что лучше помнить , что и обновлять два одновременно.

С виртуальным наследованием, изменение m_iValueс Dв порядке ... Но ... Допустим, у вас есть D. Через его Cинтерфейс вы прикрепили наблюдателя. И через егоB интерфейс вы обновляете классный массив, побочный эффект которого заключается в прямом изменении m_iValue...

Поскольку изменение m_iValueвыполняется напрямую (без использования метода виртуального метода доступа), «прослушивание» наблюдателя Cвызываться не будет, поскольку код, реализующий прослушивание, находится вC и Bне знает об этом ...

Вывод

Если у вас есть ромб в вашей иерархии, это означает, что у вас есть 95% вероятность, что что-то не так с этой иерархией.

paercebal
источник
Ваше «что может пойти не так» связано с прямым доступом к базовому члену, а не с множественным наследованием. Избавьтесь от «B», и у вас возникнет та же проблема. Основное правило: «если оно не является частным, оно должно быть виртуальным», то устраняет проблему. M_iValue не является виртуальным и поэтому должно быть приватным
Chris Dodd
4
@ Крис Додд: Не совсем. То, что происходит с m_iValue, произошло бы с любым символом ( например, typedef, переменная-член, функция-член, приведение к базовому классу и т . Д. ). Это действительно проблема множественного наследования, проблема, которую пользователи должны знать, чтобы правильно использовать множественное наследование, вместо того, чтобы идти по пути Java, и сделать вывод: «Многократное наследование - это 100% зло, давайте сделаем это с интерфейсами».
paercebal
Привет, Когда мы используем виртуальное ключевое слово, будет только одна копия А. Мой вопрос: как мы узнаем, идет ли оно от B или C? Мой вопрос действителен вообще?
user875036
@ user875036: A приходит как из B, так и из C. Действительно, виртуальность меняет несколько вещей (например, D вызовет конструктор A, а не B или C). И B, и C (и D) имеют указатель на A.
paercebal
3
FWIW, если кому-то интересно, переменные-члены не могут быть виртуальными - виртуальный является спецификатором для функций . SO ссылка: stackoverflow.com/questions/3698831/…
rholmes
34

Для объяснения множественного наследования с помощью виртуальных баз требуется знание объектной модели C ++. И объяснение темы лучше всего делать в статье, а не в поле для комментариев.

Наилучшим понятным объяснением, которое я нашел и которое разрешило все мои сомнения по этому вопросу, была эта статья: http://www.phpcompiler.org/articles/virtualinheritance.html.

Вам действительно не нужно будет читать что-либо еще по этой теме (если вы не писатель компилятора) после прочтения этого ...

lenkite
источник
10

Виртуальный базовый класс - это класс, создание которого невозможно: из него нельзя создать прямой объект.

Я думаю, что вы путаете две совершенно разные вещи. Виртуальное наследование - это не то же самое, что абстрактный класс. Виртуальное наследование изменяет поведение вызовов функций; иногда он разрешает вызовы функций, которые в противном случае были бы неоднозначными, иногда он переносит обработку вызовов функций на класс, отличный от того, который можно ожидать в невиртуальном наследовании.

wilhelmtell
источник
7

Я хотел бы добавить к добрым разъяснениям OJ.

Виртуальное наследство не обходится без цены. Как и со всеми виртуальными вещами, вы получаете удар по производительности. Этот хит производительности может быть менее элегантным.

Вместо того, чтобы разбивать алмаз путем виртуального извлечения, вы можете добавить еще один слой к алмазу, чтобы получить что-то вроде этого:

   B
  / \
D11 D12
 |   |
D21 D22
 \   /
  DD

Ни один из классов не наследует виртуально, все наследуют публично. Классы D21 и D22 будут затем скрывать виртуальную функцию f (), которая неоднозначна для DD, возможно, объявив функцию частной. Каждый из них определил бы функцию-оболочку, f1 () и f2 () соответственно, каждый из которых вызывал бы локальный класс (private) f (), таким образом разрешая конфликты. Класс DD вызывает f1 (), если он хочет D11 :: f () и f2 (), если он хочет D12 :: f (). Если вы определите встроенные оболочки, вы, вероятно, получите около нуля накладных расходов.

Конечно, если вы можете изменить D11 и D12, вы можете сделать один и тот же трюк внутри этих классов, но часто это не так.

wilhelmtell
источник
2
Это не вопрос более-менее элегантного или разрешения неясностей (для этого всегда можно использовать явные спецификации xxx ::). При использовании невиртуального наследования у каждого экземпляра класса DD есть два независимых экземпляра B. Как только у класса есть один не статический член данных, виртуальное и не виртуальное наследование отличаются не только синтаксисом.
user3489112
@ user3489112 Как только ... ничего. Виртуальное и не виртуальное наследование отличаются семантически, точка.
любопытный парень
1

Ты немного запутался. Я не знаю, смешиваете ли вы некоторые понятия.

У вас нет виртуального базового класса в вашем OP. У вас просто есть базовый класс.

Вы сделали виртуальное наследование. Это обычно используется в множественном наследовании, так что несколько производных классов используют члены базового класса, не воспроизводя их.

Базовый класс с чисто виртуальной функцией не создается. для этого требуется синтаксис, который использует Пол. Обычно он используется для того, чтобы производные классы определяли эти функции.

Я не хочу больше об этом объяснять, потому что я не совсем понимаю, что вы спрашиваете.

Baltimark
источник
1
«Базовый класс», который используется в виртуальном наследовании, становится «виртуальным базовым классом» (в контексте этого точного наследования).
Люк Эрмитт
1

Это означает, что вызов виртуальной функции будет перенаправлен в «правильный» класс.

C ++ FAQ Lite FTW.

Короче говоря, он часто используется в сценариях множественного наследования, где формируется «алмазная» иерархия. Виртуальное наследование разрушит неоднозначность, созданную в нижнем классе, когда вы вызываете функцию в этом классе, и функция должна быть разрешена либо в класс D1, либо в D2 выше этого нижнего класса. См. Пункт часто задаваемых вопросов для диаграммы и деталей.

Он также используется в сестринском делегировании , мощная функция (хотя и не для слабонервных). Смотрите этот FAQ.

Также см. Пункт 40 в Effective C ++ 3-е издание (43 в 2-е издание).

wilhelmtell
источник
1

Пример использования бриллиантового наследования

В этом примере показано, как использовать виртуальный базовый класс в типичном сценарии: для решения проблемы наследования алмазов.

#include <cassert>

class A {
    public:
        A(){}
        A(int i) : i(i) {}
        int i;
        virtual int f() = 0;
        virtual int g() = 0;
        virtual int h() = 0;
};

class B : public virtual A {
    public:
        B(int j) : j(j) {}
        int j;
        virtual int f() { return this->i + this->j; }
};

class C : public virtual A {
    public:
        C(int k) : k(k) {}
        int k;
        virtual int g() { return this->i + this->k; }
};

class D : public B, public C {
    public:
        D(int i, int j, int k) : A(i), B(j), C(k) {}
        virtual int h() { return this->i + this->j + this->k; }
};

int main() {
    D d = D(1, 2, 4);
    assert(d.f() == 3);
    assert(d.g() == 5);
    assert(d.h() == 7);
}
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
источник
2
assert(A::aDefault == 0);из основной функции выдает ошибку компиляции: aDefault is not a member of Aиспользование gcc 5.4.0. Что это должно делать?
SebNag
@SebTu ах спасибо, просто что-то, что я забыл удалить из копии вставить, удалил это сейчас. Пример все еще должен быть значимым без него.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
0

Виртуальные классы не виртуальное наследование. Виртуальные классы, которые вы не можете создать, виртуальное наследование - это совсем другое.

Википедия описывает это лучше, чем я. http://en.wikipedia.org/wiki/Virtual_inheritance

bradtgmurray
источник
6
В C ++ нет такого понятия, как «виртуальные классы». Однако существуют «виртуальные базовые классы», которые являются «виртуальными» в отношении данного наследования. То, что вы ссылаетесь, это то, что официально называется «абстрактные классы».
Люк Эрмитт
@LucHermitte, в C ++ есть определённые виртуальные классы. Проверьте это: en.wikipedia.org/wiki/Virtual_class .
Рафид
msgstr "ошибка: 'виртуальная' может быть указана только для функций". Я не знаю, что это за язык. Но в C ++ нет такого понятия, как виртуальный класс .
Люк Эрмит
0

Регулярное наследование

В типичном 3-х уровневом наследовании без виртуального наследования без виртуального наследования, когда вы создаете новый самый производный объект, вызывается новый, и требуемый для объекта размер определяется компилятором из типа класса и передается новому.

новый имеет подпись:

_GLIBCXX_WEAK_DEFINITION void *
operator new (std::size_t sz) _GLIBCXX_THROW (std::bad_alloc)

И звонит malloc , возвращая пустой указатель

Затем он передается конструктору самого производного объекта, который немедленно вызовет средний конструктор, а затем средний конструктор немедленно вызовет базовый конструктор. Затем база сохраняет указатель на свою виртуальную таблицу в начале объекта, а затем его атрибуты после него. Затем он возвращается к среднему конструктору, который будет хранить свой указатель виртуальной таблицы в том же месте, а затем его атрибуты после атрибутов, которые были бы сохранены базовым конструктором. Он возвращает наиболее производный конструктор, который хранит указатель на свою виртуальную таблицу в том же месте, а затем его атрибуты после атрибутов, которые были бы сохранены средним конструктором.

Поскольку указатель виртуальной таблицы перезаписывается, указатель виртуальной таблицы всегда оказывается одним из самых производных классов. Виртуальность распространяется на самый производный класс, поэтому, если функция является средней в среднем классе, она будет виртуальной в самом производном классе, но не в базовом классе. Если вы полиморфно приводите экземпляр самого производного класса к указателю на базовый класс, то компилятор не разрешит это косвенным вызовом виртуальной таблицы и вместо этого вызовет функцию напрямую A::function(). Если функция является виртуальной для типа, к которому вы ее приводите, она преобразуется в вызов виртуальной таблицы, которая всегда будет функцией самого производного класса. Если он не является виртуальным для этого типа, он просто вызоветType::function() и передаст на него указатель объекта, приведенный к типу.

На самом деле, когда я говорю указатель на его виртуальную таблицу, на самом деле это всегда смещение 16 в виртуальной таблице.

vtable for Base:
        .quad   0
        .quad   typeinfo for Base
        .quad   Base::CommonFunction()
        .quad   Base::VirtualFunction()

pointer is typically to the first function i.e. 

        mov     edx, OFFSET FLAT:vtable for Base+16

virtualне требуется снова в более производных классах, если он виртуален в менее производном классе, потому что он распространяется. Но его можно использовать, чтобы показать, что функция действительно является виртуальной функцией, без необходимости проверять классы, которые она наследует определения типов.

override это еще одна защита компилятора, которая говорит, что эта функция что-то переопределяет, а если нет, выдает ошибку компилятора.

= 0 означает, что это абстрактная функция

final предотвращает повторную реализацию виртуальной функции в более производном классе и гарантирует, что виртуальная таблица самого производного класса содержит конечную функцию этого класса.

= default в документации четко указано, что компилятор будет использовать реализацию по умолчанию

= delete выдает ошибку компилятора при попытке вызова этого

Виртуальное Наследование

Рассматривать

class Base
  {
      int a = 1;
      int b = 2;
  public:
      void virtual CommonFunction(){} ;
      void virtual VirtualFunction(){} ;
  };


class DerivedClass1: virtual public Base
  {
      int c = 3;
  public:
    void virtual DerivedCommonFunction(){} ;
     void virtual VirtualFunction(){} ;
  };

  class DerivedClass2 : virtual public Base
 {
     int d = 4;
 public:
     //void virtual DerivedCommonFunction(){} ;    
     void virtual VirtualFunction(){} ;
     void virtual DerivedCommonFunction2(){} ;
 };

class DerivedDerivedClass :  public DerivedClass1, public DerivedClass2
 {
   int e = 5;
 public:
     void virtual DerivedDerivedCommonFunction(){} ;
     void virtual VirtualFunction(){} ;
 };

 int main () {
   DerivedDerivedClass* d = new DerivedDerivedClass;
   d->VirtualFunction();
   d->DerivedCommonFunction();
   d->DerivedCommonFunction2();
   d->DerivedDerivedCommonFunction();
   ((DerivedClass2*)d)->DerivedCommonFunction2();
   ((Base*)d)->VirtualFunction();
 }

Практически не наследуя класс баса, вы получите объект, который выглядит следующим образом:

Вместо этого:

Т.е. будет 2 базовых объекта.

В описанной выше ситуации виртуального наследования алмазов после вызова new он вызывает самый производный конструктор, и в этом конструкторе он вызывает все 3 производных конструктора, передавая смещения в таблицу виртуальных таблиц, вместо вызова только вызова DerivedClass1::DerivedClass1()иDerivedClass2::DerivedClass2() затем те , как призваниеBase::Base()

Следующее все скомпилировано в режиме отладки -O0, поэтому будет избыточная сборка

main:
.LFB8:
        push    rbp
        mov     rbp, rsp
        push    rbx
        sub     rsp, 24
        mov     edi, 48 //pass size to new
        call    operator new(unsigned long) //call new
        mov     rbx, rax  //move the address of the allocation to rbx
        mov     rdi, rbx  //move it to rdi i.e. pass to the call
        call    DerivedDerivedClass::DerivedDerivedClass() [complete object constructor] //construct on this address
        mov     QWORD PTR [rbp-24], rbx  //store the address of the object on the stack as d
DerivedDerivedClass::DerivedDerivedClass() [complete object constructor]:
.LFB20:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], rdi
.LBB5:
        mov     rax, QWORD PTR [rbp-8] // object address now in rax 
        add     rax, 32 //increment address by 32
        mov     rdi, rax // move object address+32 to rdi i.e. pass to call
        call    Base::Base() [base object constructor]
        mov     rax, QWORD PTR [rbp-8] //move object address to rax
        mov     edx, OFFSET FLAT:VTT for DerivedDerivedClass+8 //move address of VTT+8 to edx
        mov     rsi, rdx //pass VTT+8 address as 2nd parameter 
        mov     rdi, rax //object address as first
        call    DerivedClass1::DerivedClass1() [base object constructor]
        mov     rax, QWORD PTR [rbp-8] //move object address to rax
        add     rax, 16  //increment object address by 16
        mov     edx, OFFSET FLAT:VTT for DerivedDerivedClass+24  //store address of VTT+24 in edx
        mov     rsi, rdx //pass address of VTT+24 as second parameter
        mov     rdi, rax //address of object as first
        call    DerivedClass2::DerivedClass2() [base object constructor]
        mov     edx, OFFSET FLAT:vtable for DerivedDerivedClass+24 //move this to edx
        mov     rax, QWORD PTR [rbp-8] // object address now in rax
        mov     QWORD PTR [rax], rdx. //store address of vtable for DerivedDerivedClass+24 at the start of the object
        mov     rax, QWORD PTR [rbp-8] // object address now in rax
        add     rax, 32  // increment object address by 32
        mov     edx, OFFSET FLAT:vtable for DerivedDerivedClass+120 //move this to edx
        mov     QWORD PTR [rax], rdx  //store vtable for DerivedDerivedClass+120 at object+32 (Base) 
        mov     edx, OFFSET FLAT:vtable for DerivedDerivedClass+72 //store this in edx
        mov     rax, QWORD PTR [rbp-8] //move object address to rax
        mov     QWORD PTR [rax+16], rdx //store vtable for DerivedDerivedClass+72 at object+16 (DerivedClass2)
        mov     rax, QWORD PTR [rbp-8]
        mov     DWORD PTR [rax+28], 5
.LBE5:
        nop
        leave
        ret

Он вызывает Base::Base()с указателем на смещение объекта 32. Base хранит указатель на свою виртуальную таблицу по адресу, который он получает, и его членам после него.

Base::Base() [base object constructor]:
.LFB11:
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-8], rdi //stores address of object on stack (-O0)
.LBB2:
        mov     edx, OFFSET FLAT:vtable for Base+16  //puts vtable for Base+16 in edx
        mov     rax, QWORD PTR [rbp-8] //copies address of object from stack to rax
        mov     QWORD PTR [rax], rdx  //stores it address of object
        mov     rax, QWORD PTR [rbp-8] //copies address of object on stack to rax again
        mov     DWORD PTR [rax+8], 1 //stores a = 1 in the object
        mov     rax, QWORD PTR [rbp-8] //junk from -O0
        mov     DWORD PTR [rax+12], 2  //stores b = 2 in the object
.LBE2:
        nop
        pop     rbp
        ret

DerivedDerivedClass::DerivedDerivedClass()затем вызывает DerivedClass1::DerivedClass1()с указателем на объект смещения 0, а также передает адресVTT for DerivedDerivedClass+8

DerivedClass1::DerivedClass1() [base object constructor]:
.LFB14:
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-8], rdi //address of object
        mov     QWORD PTR [rbp-16], rsi  //address of VTT+8
.LBB3:
        mov     rax, QWORD PTR [rbp-16]  //address of VTT+8 now in rax
        mov     rdx, QWORD PTR [rax]     //address of DerivedClass1-in-DerivedDerivedClass+24 now in rdx
        mov     rax, QWORD PTR [rbp-8]   //address of object now in rax
        mov     QWORD PTR [rax], rdx     //store address of DerivedClass1-in-.. in the object
        mov     rax, QWORD PTR [rbp-8]  // address of object now in rax
        mov     rax, QWORD PTR [rax]    //address of DerivedClass1-in.. now implicitly in rax
        sub     rax, 24                 //address of DerivedClass1-in-DerivedDerivedClass+0 now in rax
        mov     rax, QWORD PTR [rax]    //value of 32 now in rax
        mov     rdx, rax                // now in rdx
        mov     rax, QWORD PTR [rbp-8]  //address of object now in rax
        add     rdx, rax                //address of object+32 now in rdx
        mov     rax, QWORD PTR [rbp-16]  //address of VTT+8 now in rax
        mov     rax, QWORD PTR [rax+8]   //address of DerivedClass1-in-DerivedDerivedClass+72 (Base::CommonFunction()) now in rax
        mov     QWORD PTR [rdx], rax     //store at address object+32 (offset to Base)
        mov     rax, QWORD PTR [rbp-8]  //store address of object in rax, return
        mov     DWORD PTR [rax+8], 3    //store its attribute c = 3 in the object
.LBE3:
        nop
        pop     rbp
        ret
VTT for DerivedDerivedClass:
        .quad   vtable for DerivedDerivedClass+24
        .quad   construction vtable for DerivedClass1-in-DerivedDerivedClass+24
        .quad   construction vtable for DerivedClass1-in-DerivedDerivedClass+72
        .quad   construction vtable for DerivedClass2-in-DerivedDerivedClass+24
        .quad   construction vtable for DerivedClass2-in-DerivedDerivedClass+72
        .quad   vtable for DerivedDerivedClass+120
        .quad   vtable for DerivedDerivedClass+72

construction vtable for DerivedClass1-in-DerivedDerivedClass:
        .quad   32
        .quad   0
        .quad   typeinfo for DerivedClass1
        .quad   DerivedClass1::DerivedCommonFunction()
        .quad   DerivedClass1::VirtualFunction()
        .quad   -32
        .quad   0
        .quad   -32
        .quad   typeinfo for DerivedClass1
        .quad   Base::CommonFunction()
        .quad   virtual thunk to DerivedClass1::VirtualFunction()
construction vtable for DerivedClass2-in-DerivedDerivedClass:
        .quad   16
        .quad   0
        .quad   typeinfo for DerivedClass2
        .quad   DerivedClass2::VirtualFunction()
        .quad   DerivedClass2::DerivedCommonFunction2()
        .quad   -16
        .quad   0
        .quad   -16
        .quad   typeinfo for DerivedClass2
        .quad   Base::CommonFunction()
        .quad   virtual thunk to DerivedClass2::VirtualFunction()
vtable for DerivedDerivedClass:
        .quad   32
        .quad   0
        .quad   typeinfo for DerivedDerivedClass
        .quad   DerivedClass1::DerivedCommonFunction()
        .quad   DerivedDerivedClass::VirtualFunction()
        .quad   DerivedDerivedClass::DerivedDerivedCommonFunction()
        .quad   16
        .quad   -16
        .quad   typeinfo for DerivedDerivedClass
        .quad   non-virtual thunk to DerivedDerivedClass::VirtualFunction()
        .quad   DerivedClass2::DerivedCommonFunction2()
        .quad   -32
        .quad   0
        .quad   -32
        .quad   typeinfo for DerivedDerivedClass
        .quad   Base::CommonFunction()
        .quad   virtual thunk to DerivedDerivedClass::VirtualFunction()

virtual thunk to DerivedClass1::VirtualFunction():
        mov     r10, QWORD PTR [rdi]
        add     rdi, QWORD PTR [r10-32]
        jmp     .LTHUNK0
virtual thunk to DerivedClass2::VirtualFunction():
        mov     r10, QWORD PTR [rdi]
        add     rdi, QWORD PTR [r10-32]
        jmp     .LTHUNK1
virtual thunk to DerivedDerivedClass::VirtualFunction():
        mov     r10, QWORD PTR [rdi]
        add     rdi, QWORD PTR [r10-32]
        jmp     .LTHUNK2
non-virtual thunk to DerivedDerivedClass::VirtualFunction():
        sub     rdi, 16
        jmp     .LTHUNK3

        .set    .LTHUNK0,DerivedClass1::VirtualFunction()
        .set    .LTHUNK1,DerivedClass2::VirtualFunction()
        .set    .LTHUNK2,DerivedDerivedClass::VirtualFunction()
        .set    .LTHUNK3,DerivedDerivedClass::VirtualFunction()

DerivedDerivedClass::DerivedDerivedClass()затем передает адрес объекта + 16 и адрес для VTT DerivedDerivedClass+24к DerivedClass2::DerivedClass2()которой сборка идентична DerivedClass1::DerivedClass1()за исключением того линии , mov DWORD PTR [rax+8], 3которые , очевидно , имеет 4 вместо 3 для d = 4.

После этого он заменяет все 3 указателя виртуальных таблиц в объекте указателями на смещения в DerivedDerivedClassvtable таблицы представления этого класса.

d->VirtualFunction();:

        mov     rax, QWORD PTR [rbp-24] //store pointer to virtual table in rax 
        mov     rax, QWORD PTR [rax] //dereference and store in rax
        add     rax, 8 // call the 2nd function in the table
        mov     rdx, QWORD PTR [rax] //dereference 
        mov     rax, QWORD PTR [rbp-24]
        mov     rdi, rax
        call    rdx

d->DerivedCommonFunction();:

        mov     rax, QWORD PTR [rbp-24]
        mov     rdx, QWORD PTR [rbp-24]
        mov     rdx, QWORD PTR [rdx]
        mov     rdx, QWORD PTR [rdx]
        mov     rdi, rax
        call    rdx

d->DerivedCommonFunction2();:

        mov     rax, QWORD PTR [rbp-24]
        lea     rdx, [rax+16]
        mov     rax, QWORD PTR [rbp-24]
        mov     rax, QWORD PTR [rax+16]
        add     rax, 8
        mov     rax, QWORD PTR [rax]
        mov     rdi, rdx
        call    rax

d->DerivedDerivedCommonFunction();:

        mov     rax, QWORD PTR [rbp-24]
        mov     rax, QWORD PTR [rax]
        add     rax, 16
        mov     rdx, QWORD PTR [rax]
        mov     rax, QWORD PTR [rbp-24]
        mov     rdi, rax
        call    rdx

((DerivedClass2*)d)->DerivedCommonFunction2();:

        cmp     QWORD PTR [rbp-24], 0
        je      .L14
        mov     rax, QWORD PTR [rbp-24]
        add     rax, 16
        jmp     .L15
.L14:
        mov     eax, 0
.L15:
        cmp     QWORD PTR [rbp-24], 0
        cmp     QWORD PTR [rbp-24], 0
        je      .L18
        mov     rdx, QWORD PTR [rbp-24]
        add     rdx, 16
        jmp     .L19
.L18:
        mov     edx, 0
.L19:
        mov     rdx, QWORD PTR [rdx]
        add     rdx, 8
        mov     rdx, QWORD PTR [rdx]
        mov     rdi, rax
        call    rdx

((Base*)d)->VirtualFunction();:

        cmp     QWORD PTR [rbp-24], 0
        je      .L20
        mov     rax, QWORD PTR [rbp-24]
        mov     rax, QWORD PTR [rax]
        sub     rax, 24
        mov     rax, QWORD PTR [rax]
        mov     rdx, rax
        mov     rax, QWORD PTR [rbp-24]
        add     rax, rdx
        jmp     .L21
.L20:
        mov     eax, 0
.L21:
        cmp     QWORD PTR [rbp-24], 0
        cmp     QWORD PTR [rbp-24], 0
        je      .L24
        mov     rdx, QWORD PTR [rbp-24]
        mov     rdx, QWORD PTR [rdx]
        sub     rdx, 24
        mov     rdx, QWORD PTR [rdx]
        mov     rcx, rdx
        mov     rdx, QWORD PTR [rbp-24]
        add     rdx, rcx
        jmp     .L25
.L24:
        mov     edx, 0
.L25:
        mov     rdx, QWORD PTR [rdx]
        add     rdx, 8
        mov     rdx, QWORD PTR [rdx]
        mov     rdi, rax
        call    rdx
Льюис Келси
источник