Это может быть выражено в облегченном C ++, если вы находите его более читабельным, чем ассемблер (я так понимаю). Я ограничусь C (в основном) и просто добавлю наследование, чтобы избежать большого количества приведения.
Для ясности детали реализации будут иметь префикс __
. Обратите внимание, что в целом эти идентификаторы зарезервированы для реализации, поэтому обычно вы не должны использовать их в своих программах.
Тип безопасной виртуальной диспетчеризации .
Примечание: ограничено простым наследованием (одно базовое, виртуальное наследование отсутствует)
Давайте создадим класс Animal
.
struct __AnimalTableT;
struct Animal { __AnimalTableT const * const __vptr; int height; }
void AnimalInit(Animal* a, int height) {
a->height = height;
}
Мы резервируем место для указателя на виртуальную таблицу в Animal и выражаем метод как внешнюю функцию, чтобы сделать this
явным.
Далее мы «создаем» виртуальную таблицу. Обратите внимание, что массив в C должен состоять из аналогичных элементов, поэтому здесь мы будем использовать подход более высокого уровня.
struct __AnimalTableT {
typedef void (*InitFunction)(int);
InitFunction Init;
};
static __AnimalTableT const __AnimalTable = { &AnimalInit };
Теперь давайте создадим корову:
struct Cow: Animal {};
void CowInit(Animal* a, int height) {
Cow* c = static_cast<Cow*>(a);
c->height = height;
}
И связанная таблица:
// Note: we could have new functions here (that only Cow has)
// they would be appended after the "Animal" part
struct __CowTableT: __AnimalTableT {};
static __CowTableT const __CowTable = { &CowInit };
И использование:
typedef void (*__AnimalInitT)(Animal*,int);
int main() {
Cow cow = { &__CowTable, 0 };
__AnimalInitT const __ai = cow.__vptr->Init;
(*__ai)(&cow, 5);
}
А настоящий?
Реальное использование немного сложнее, но основывается на той же идее.
Как вы можете заметить, странно, что CowInit
принимает указатель Animal*
в качестве первого аргумента. Проблема в том, что вам нужен совместимый тип указателя на функцию с изначально перегруженным методом. В случае линейного наследования это не имело бы большого значения, но в случае множественного наследования или виртуального наследования все становится довольно беспокойным, и подраздел Animal
в Cow
может не планироваться в самом начале, что приводит к настройка указателя.
В реальной жизни у нас есть множество:
Ну, мы можем изменить подпись CowInit
на более естественную:
void CowInit(Cow* cow, int height);
И затем, мы "ликвидируем" разрыв, создавая "толчок", чтобы сделать адаптацию:
void __CowInit(Animal* a, int height) {
CowInit(static_cast<Cow*>(a), height);
}
static __CowTableT const __CowTable = { &__CowInit };
В реальной жизни у нас есть таблицы:
Еще одно замечание: использование структуры очень хорошо, но мы говорим здесь о деталях реализации, поэтому излишняя тщательность не требуется. В общем, поэтому компиляторы используют простой массив, а не структуру:
typedef (void)(*__GenericFunction)();
static __GenericFunction const __AnimalTable[] = {
__GenericFunction(&AnimalInit)
};
static __GenericFunction const __CowTable[] = {
__GenericFunction(&__CowInit)
};
Это немного меняет вызов: вы используете индекс вместо имени атрибута, и вам нужно привести обратно к соответствующему типу функции.
typedef void (*__AnimalInitT)(Animal*,int);
int main() {
Cow cow = { &__CowTable, 0 };
// old line: __AnimalInitT const __ai = cow.__vptr->Init;
__AnimalInit const __ai = __AnimalInit(cow.__vptr[0]);
(*__ai)(&cow, 5);
}
Как видите, использование таблиц - это действительно детали реализации.
Действительно важным моментом здесь является введение thunk для адаптации сигнатуры функции. Обратите внимание, что thunk введен при создании таблицы класса Derived (Cow здесь). В нашем случае это не нужно, поскольку на низком уровне оба объекта имеют один и тот же адрес, поэтому мы могли бы обойтись без него, и умный компилятор не сгенерирует его и непосредственно примет &CowInit
.