Возможно, проще всего начать с размышлений о том, как одиночное наследование (обычно) реализовано в C ++. Рассмотрим иерархию, которая включает хотя бы одну виртуальную функцию:
struct Base {
int x;
virtual void f() {}
virtual ~Base() {}
};
struct Derived : Base {
int y;
virtual void f() {}
virtual ~Derived() {}
};
В типичном случае это будет реализовано путем создания таблицы vtable для каждого класса и создания каждого объекта с (скрытым) указателем таблицы vtable. Указатель vtable для каждого объекта (класса Base или Derived) будет иметь указатель vtable с одинаковым смещением в структуре, и каждый будет содержать указатели на виртуальную функцию (f
и dtor) с одинаковыми смещениями в виртуальный стол.
Теперь рассмотрим полиморфное использование этих типов, таких как:
void g(Base&b) {
b.f();
}
Поскольку и Base, и Derived (и любые другие производные Base) имеют виртуальную таблицу, расположенную одинаково, и указатель на виртуальную таблицу с одинаковым смещением в структуре, компилятор может сгенерировать точно такой же код для этого независимо от того, имеет ли он дело с базой, производным или чем-то еще, полученным из базы.
Однако, когда вы добавляете множественное наследование в микс, это меняется. В частности, вы не можете упорядочить все свои объекты, чтобы указатель на виртуальную таблицу всегда имел одинаковое смещение в каждом объекте, по той простой причине, что объект, производный от двух базовых классов, (потенциально) иметь указатели на две отдельные таблицы, которые явно не могут быть с одинаковым смещением в структуре (т. е. вы не можете поместить две разные вещи в одно и то же место). Чтобы приспособиться к этому, вы должны сделать какую-то явную корректировку. Каждый умноженный производный класс должен иметь какой-то способ для компилятора найти виртуальные таблицы для всех базовых классов. Рассмотрим что-то вроде этого:
struct Base1 {
virtual void f() { }
};
struct Base2 {
virtual void g() {}
};
class Derived1 : Base1, Base2 {
virtual void f() {}
virtual void g() {}
};
class Derived2 : Base2, Base1 {
virtual void f() {}
virtual void g() {}
};
В типичном случае компилятор упорядочивает указатели vtable в том же порядке, в котором вы указываете базовые классы, поэтому Derived1 будет иметь указатель на vtable Base1, за которым следует указатель на vtable Base2. Derived2 обратит порядок.
Теперь, предполагая ту же функцию, которая выполняет полиморфный вызов f()
, но будет передана ссылка на Base1, или Derived1, или Derived2. Один из них почти неизбежно будет иметь указатель на vtable Base1 с другим смещением, чем другие. Вот тут-то и появляется «this-Adjustor» (или как вы предпочитаете его называть) - он находит правильное смещение для базового класса, который вы пытаетесь использовать, поэтому, когда вы получаете доступ к членам этого класса, вы получаете правильные данные.
Обратите внимание, что хотя я использовал указатель на vtable в качестве основного примера, это не единственная возможность. На самом деле, даже если у вас нет виртуальных функций ни в одном из классов, вам все равно нужен доступ к данным для каждого базового класса, что требует такой же настройки.