Как upcasting и vtables работают вместе для обеспечения правильного динамического связывания?

Так, vtable это таблица, поддерживаемая компилятором, которая содержит указатели на функции, которые указывают на виртуальные функции в этом классе.

а также

Присвоение объекта производного класса объекту класса-предка называется повышением частоты.

Приведение вверх обрабатывает экземпляр / объект производного класса с использованием указателя или ссылки на базовый класс; объекты не «назначены», что подразумевает перезапись значения ala operator = invocation.
(Благодаря: Тони Д)

Теперь, как во время выполнения известно, «какая» виртуальная функция класса должна вызываться?

Какая запись в vtable относится к функции «определенных» производных классов, которая должна вызываться во время выполнения?

20

Решение

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

Итак, vtable — это таблица, поддерживаемая компилятором, которая содержит указатели на функции, которые указывают на виртуальные функции в этом классе.

Более точный способ сказать следующее:

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

Приведение вверх обрабатывает экземпляр / объект производного класса с использованием указателя или ссылки на базовый класс; (…)

Возможно, более поучительно:

Повышение приведения означает, что указатель или ссылка на экземпляр класса Derived рассматривается как указатель или ссылка на экземпляр класса Base, Сам экземпляр, однако, все еще является чисто экземпляром Derived,

(Когда указатель «рассматривается как указатель на Base«, это означает, что компилятор генерирует код для работы с указателем на Base, Другими словами, компилятор и сгенерированный код знают не лучше, чем то, что они имеют дело с указателем на Base, Следовательно, указатель, который «обрабатывается как», должен указывать на объект, который предлагает по крайней мере тот же интерфейс, что и экземпляры Base, Это случается для Derived из-за наследства. Посмотрим, как это работает ниже.)

На данный момент мы можем ответить на первую версию вашего вопроса.

Теперь, как во время выполнения известно, «какая» виртуальная функция класса должна вызываться?

Предположим, у нас есть указатель на экземпляр Derived, Сначала мы выгружаем его, поэтому он рассматривается как указатель на экземпляр Base, Затем мы вызываем виртуальный метод для нашего переданного указателя. Поскольку компилятор знает, что метод является виртуальным, он знает, как искать указатель виртуальной таблицы в экземпляре. Пока мы обрабатываем указатель, как будто он указывает на экземпляр Baseфактический объект не изменил значение, и указатель виртуальной таблицы в нем все еще указывает на виртуальную таблицу Derived, Таким образом, во время выполнения адрес метода берется из виртуальной таблицы Derived,

Теперь конкретный метод может быть унаследован от Base или это может быть переопределено в Derived, Это не имеет значения; если наследуется, указатель метода в виртуальной таблице Derived просто содержит тот же адрес, что и соответствующий указатель метода в виртуальной таблице Base, Другими словами, обе таблицы указывают на одну и ту же реализацию метода для этого конкретного метода. Если переопределено, указатель метода в виртуальной таблице Derived отличается от соответствующего указателя метода в виртуальной таблице Base, поэтому метод поиска на экземплярах Derived найдет переопределенный метод при поиске на экземплярах Base найдет оригинальную версию метода — несмотря на о том, рассматривается ли указатель на экземпляр как указатель на Base или указатель на Derived,

Наконец, теперь должно быть просто объяснить, почему вторая версия вашего вопроса немного ошибочна:

Какая запись в vtable относится к функции «определенных» производных классов, которая должна вызываться во время выполнения?

Этот вопрос предполагает, что поиск в vtable выполняется сначала методом, а затем классом. Это наоборот: во-первых, указатель vtable в экземпляре используется для поиска vtable для нужного класса. Затем vtable для этого класса используется для поиска правильного метода.

4

Другие решения

Вы можете себе представить (хотя в спецификации C ++ это не сказано), что vtable — это идентификатор (или некоторые другие метаданные, которые можно использовать для «поиска дополнительной информации» о самом классе) и список функций.

Итак, если у нас есть такой класс:

class Base
{
public:
virtual void func1();
virtual void func2(int x);
virtual std::string func3();
virtual ~Base();
... some other stuff we don't care about ...
};

Затем компилятор выдаст VTable что-то вроде этого:

struct VTable_Base
{
int identifier;
void (*func1)(Base* this);
void (*func2)(Base* this, int x);
std::string (*func3)(Base* this);
~Base(Base *this);
};

Затем компилятор создаст внутреннюю структуру, которая примерно такая (это невозможно скомпилировать как C ++, просто показать, что на самом деле делает компилятор — и я это называю Sbase отличать фактическое class Base)

struct SBase
{
VTable_Base* vtable;
inline void func1(Base* this) { vtable->func1(this); }
inline void func2(Base* this, int x) { vtable->func2(this, x); }
inline std::string func3(Base* this) { return vtable->func3(this); }
inline ~Base(Base* this) { vtable->~Base(this); }
};

Он также создает настоящий vtable:

VTable_Base vtable_base =
{
1234567, &Base::func1, &Base::func2, &Base::func3, &Base::~Base
};

И в конструкторе для Base, это установит vtable = vtable_base;,

Затем мы добавляем производный класс, где переопределяем одну функцию (и по умолчанию деструктор, даже если мы ее не объявляем):

class Derived : public Base
{
virtual void func2(int x) override;
};

Компилятор теперь создаст эту структуру:

struct VTable_Derived
{
int identifier;
void (*func1)(Base* this);
void (*func2)(Base* this, int x);
std::string (*func3)(Base* this);
~Base(Derived *this);
};

а затем делает то же самое «строение» здания:

struct SDerived
{
VTable_Derived* vtable;
inline void func1(Base* this) { vtable->func1(this); }
inline void func2(Base* this, int x) { vtable->func2(this, x); }
inline std::string func3(Base* this) { return vtable->func3(this); }
inline ~Derived(Derived* this) { vtable->~Derived(this); }
};

Нам нужна эта структура, когда мы используем Derived напрямую, а не через Base учебный класс.

(Мы полагаемся на цепочку компиляторов в ~Derived звонить ~Base тоже как обычные деструкторы которые наследуют)

И, наконец, мы создаем фактический vtable:

VTable_Derived vtable_derived =
{
7654339, &Base::func1, &Derived::func2, &Base::func3, &Derived::~Derived
};

И снова Derived конструктор установит Dervied::vtable = vtable_derived для всех случаев.

Изменить, чтобы ответить на вопрос в комментариях: компилятор должен аккуратно размещать различные компоненты в обоих VTable_Derived а также SDerived так, что это соответствует VTable_Base а также SBase, так что когда у нас есть указатель на Base, Base::vtable а также Base::funcN() совпадают Derived::vtable а также Derived::FuncN, Если это не совпадает, то наследование не будет работать.

Если новые виртуальные функции добавляются в Derivedзатем они должны быть размещены после наследуемых от Base,

Конец Редактировать.

Итак, когда мы делаем:

Base* p = new Derived;

p->func2();

код будет искать SBase::Func2, который будет использовать правильный Derived::func2 (потому что фактический vtable внутри p->vtable является VTable_Derived (как установлено Derived конструктор, который вызывается вместе с new Derived).

18

Какая запись в vtable относится к функции «определенного» производного
классы, которые должны вызываться во время выполнения?

Нет, это не запись в виртуальной таблице, а указатель виртуальной таблицы, являющийся частью каждого экземпляра объекта, который определяет правильный набор виртуальных функций для этого конкретного объекта. Таким образом, в зависимости от фактической таблицы, на которую указывает виртуальная таблица, вызов «первого виртуального метода» из таблицы может привести к вызову различных функций для объектов разных типов в одной и той же полиморфной иерархии.

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

Как upcasting и vtables работают вместе, чтобы обеспечить правильную динамику
связывание?

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

Понижение рейтинга необходимо, когда вы хотите получить доступ к функциональности, которая недоступна в базовом классе и объявлена ​​в производном классе. Но прежде чем пытаться сделать это, вы должны быть уверены, что конкретный объект имеет или наследует тип, который объявляет эту функциональность, где dynamic_cast приходит, когда вы динамически приводите компилятор, генерирует проверку для этой записи vtable и наследует ли он запрошенный тип от другой таблицы, сгенерированной во время компиляции, и если да, то динамическое приведение завершается успешно, в противном случае происходит сбой.

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

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

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

5

Полиморфизм и динамическая диспетчеризация (гипер Сокращенная версия)

Примечание: я не смог уместить достаточно информации о множественном наследовании с виртуальными базами, так как ничего особенного в этом нет, и детали загромождают изложение (далее). Этот ответ демонстрирует механизмы, используемые для реализации динамической диспетчеризации, предполагающей только одно наследование.

Интерпретация абстрактных типов и их поведения, видимого через границы модулей, требует общего двоичного интерфейса приложений (ABI). Стандарт C ++, конечно, не требует реализации какого-либо конкретного ABI.

ABI будет описывать:

  • Компоновка таблиц диспетчеризации виртуальных методов (vtables)
  • Метаданные, необходимые для проверки типов во время выполнения и операций приведения
  • Оформление имен (a.k.a. mangling), соглашения о вызовах и многое другое.

Оба модуля в следующем примере external.so а также main.oПредполагается, что они были связаны с одной и той же средой выполнения. Статическая и динамическая привязка отдают предпочтение символам, расположенным внутри вызывающего модуля.


Внешняя библиотека

external.h (раздается пользователям):

class Base
{
__vfptr_t __vfptr; // For exposition

public:

__attribute__((dllimport)) virtual int Helpful();
__attribute__((dllimport)) virtual ~Base();
};

class Derived : public Base
{
public:

__attribute__((dllimport)) virtual int Helpful() override;

~Derived()
{
// Visible destructor logic here.// Note: This is in the header!// __vft@Base gets treated like any other imported symbol:
// The address is resolved at load time.
//
this->__vfptr = &__vft@Base;
static_cast<Base *>(this)->~Base();
}
};

__attribute__((dllimport)) Derived *ReticulateSplines();

external.cpp:

#include "external.h" // the version in which the attributes are dllexport

__attribute__((dllexport)) int Base::Helpful()
{
return 47;
}
__attribute__((dllexport)) Base::~Base()
{
}

__attribute__((dllexport)) int Derived::Helpful()
{
return 4449;
}

__attribute__((dllexport)) Derived *ReticulateSplines()
{
return new Derived(); // __vfptr = &__vft@Derived in external.so
}

external.so (не настоящий двоичный формат):

__vft@Base:
[offset to __type_info@Base] <-- in external.so
[offset to Base::~Base] <------- in external.so
[offset to Base::Helpful] <----- in external.so

__vft@Derived:
[offset to __type_info@Derived] <-- in external.so
[offset to Derived::~Derived] <---- in external.so
[offset to Derived::Helpful] <----- in external.so

Etc...

__type_info@Base:
[null base offset field]
[offset to mangled name]

__type_info@Derived:
[offset to __type_info@Base]
[offset to mangled name]

Etc...

Приложение, использующее внешнюю библиотеку

special.hpp:

#include <iostream>
#include "external.h"
class Special : public Base
{
public:

int Helpful() override
{
return 55;
}

virtual void NotHelpful()
{
throw std::exception{"derp"};
}
};

class MoreDerived : public Derived
{
public:

int Helpful() override
{
return 21;
}

~MoreDerived()
{
// Visible destructor logic here

this->__vfptr = &__vft@Derived; // <- the version in main.o
static_cast<Derived *>(this)->~Derived();
}
};

class Related : public Base
{
public:

virtual void AlsoHelpful() = 0;
};

class RelatedImpl : public Related
{
public:

void AlsoHelpful() override
{
using namespace std;

cout << "The time for action... Is now!" << endl;
}
};

main.cpp:

#include "special.hpp"
int main(int argc, char **argv)
{
Base *ptr = new Base(); // ptr->__vfptr = &__vft@Base (in external.so)

auto r = ptr->Helpful(); // calls "Base::Helpful" in external.so
// r = 47

delete ptr; // calls "Base::~Base" in external.soptr = new Derived(); // ptr->__vfptr = &__vft@Derived (in main.o)

r = ptr->Helpful(); // calls "Derived::Helpful" in external.so
// r = 4449

delete ptr; // calls "Derived::~Derived" in main.optr = ReticulateSplines(); // ptr->__vfptr = &__vft@Derived (in external.so)

r = ptr->Helpful(); // calls "Derived::Helpful" in external.so
// r = 4449

delete ptr; // calls "Derived::~Derived" in external.soptr = new Special(); // ptr->__vfptr = &__vft@Special (in main.o)

r = ptr->Helpful(); // calls "Special::Helpful" in main.o
// r = 55

delete ptr; // calls "Base::~Base" in external.soptr = new MoreDerived(); // ptr->__vfptr = & __vft@MoreDerived (in main.o)

r = ptr->Helpful(); // calls "MoreDerived::Helpful" in main.o
// r = 21

delete ptr; // calls "MoreDerived::~MoreDerived" in main.oreturn 0;
}

main.o:

__vft@Derived:
[offset to __type_info@Derivd] <-- in main.o
[offset to Derived::~Derived] <--- in main.o
[offset to Derived::Helpful] <---- stub that jumps to import table

__vft@Special:
[offset to __type_info@Special] <-- in main.o
[offset to Base::~Base] <---------- stub that jumps to import table
[offset to Special::Helpful] <----- in main.o
[offset to Special::NotHelpful] <-- in main.o

__vft@MoreDerived:
[offset to __type_info@MoreDerived] <---- in main.o
[offset to MoreDerived::~MoreDerived] <-- in main.o
[offset to MoreDerived::Helpful] <------- in main.o

__vft@Related:
[offset to __type_info@Related] <------ in main.o
[offset to Base::~Base] <-------------- stub that jumps to import table
[offset to Base::Helpful] <------------ stub that jumps to import table
[offset to Related::AlsoHelpful] <----- stub that throws PV exception

__vft@RelatedImpl:
[offset to __type_info@RelatedImpl] <--- in main.o
[offset to Base::~Base] <--------------- stub that jumps to import table
[offset to Base::Helpful] <------------- stub that jumps to import table
[offset to RelatedImpl::AlsoHelpful] <-- in main.o

Etc...

__type_info@Base:
[null base offset field]
[offset to mangled name]

__type_info@Derived:
[offset to __type_info@Base]
[offset to mangled name]

__type_info@Special:
[offset to __type_info@Base]
[offset to mangled name]

__type_info@MoreDerived:
[offset to __type_info@Derived]
[offset to mangled name]

__type_info@Related:
[offset to __type_info@Base]
[offset to mangled name]

__type_info@RelatedImpl:
[offset to __type_info@Related]
[offset to mangled name]

Etc...

Призыв — это (или не может быть) Магия!

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

Динамический вызов виртуального метода будет читать адрес целевой функции из таблицы, на которую указывает __vfptr член.

ABI описывает порядок упорядочения функций в vtables. Например: они могут быть упорядочены по классу, а затем лексикографически по искаженному названию (которое включает в себя информацию о постоянстве, параметрах и т. Д.). Для одиночного наследования этот подход гарантирует, что индекс виртуальной диспетчеризации функции всегда будет одинаковым, независимо от того, сколько существует различных реализаций.

В приведенных здесь примерах деструкторы размещаются в начале каждой таблицы, если это применимо. Если деструктор тривиален и не виртуален (не определен или ничего не делает), компилятор может полностью исключить его и не выделять для него запись vtable.

Base *ptr = new Special{};
MoreDerived *md_ptr = new MoreDerived{};

// The cast below is checked statically, which would
// be a problem if "ptr" weren't pointing to a Special.
//
Special *sptr = static_cast<Special *>(ptr);

// In this case, it is possible to
// prove that "ptr" could point only to
// a Special, binding statically.
//
ptr->Helpful();

// Due to the cast above, a compiler might not
// care to prove that the pointed-to type
// cannot be anything but a Special.
//
// The call below might proceed as follows:
//
// reg = sptr->__vptr[__index_of@Base::Helpful] = &Special::Helpful in main.o
//
// push sptr
// call reg
// pop
//
// This will indirectly call Special::Helpful.
//
sptr->Helpful();

// No cast required: LSP is satisfied.
ptr = md_ptr;

// Once again:
//
// reg = ptr->__vfptr[__index_of@Base::Helpful] = &MoreDerived::Helpful in main.o
//
// push ptr
// call reg
// pop
//
// This will indirectly call MoreDerived::Helpful
//
ptr->Helpful();

Приведенная выше логика одинакова для любого сайта вызова, который требует динамического связывания. В приведенном выше примере не имеет значения, какой именно ptr или же sptr указать на; код просто загрузит указатель с известным смещением, а затем вызовет его вслепую.


Тип кастинга: взлеты и падения

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

Повышение в этом простом ABI может быть выполнено полностью во время компиляции. Компилятору нужно только изучить иерархию типов, чтобы определить, связаны ли типы источника и цели (на графике типов есть путь от источника к цели). Посредством принцип замещения, указатель на MoreDerived также указывает на Base и может быть истолковано как таковое. __vfptr member имеет одинаковое смещение для всех типов в этой иерархии, поэтому RTTI-логике не нужно обрабатывать какие-либо особые случаи (в некоторых реализациях VMI необходимо получить другое смещение от thunk типа, чтобы получить другой vptr и т. д.). …).

Понижение, однако, отличается. Поскольку приведение базового типа к производному типу включает в себя определение наличия совместимого двоичного макета в указанном объекте, необходимо выполнить явную проверку типа (концептуально это «доказательство» того, что дополнительная информация существует за пределами структура предполагается во время компиляции).

Обратите внимание, что существует несколько экземпляров vtable для Derived тип: один в external.so и один в main.o, Это потому, что виртуальный метод, определенный для Derived (его деструктор) появляется в каждой единице перевода, которая включает external.h,

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

Затем выполняется приведение к минимуму путем обхода графа типов (скопированного в оба изображения), начиная с исходного типа, декодированного во время выполнения, и сравнивая искаженные имена до совпадения с целевым значением времени компиляции.

Например:

Base *ptr = new MoreDerived();

// ptr->__vfptr = &__vft::MoreDerived in main.o
//
// This provides the code below with a starting point
// for dynamic cast graph traversals.

// All searches start with the type graph in the current image,
// then all other linked images, and so on...

// This example is not exhaustive!

// Starts by grabbing &__type_info@MoreDerived
// using the offset within __vft@MoreDerived resolved
// at load time.
//
// This is similar to a virtual method call: Just grab
// a pointer from a known offset within the table.
//
// Search path:
// __type_info@MoreDerived (match!)
//
auto *md_ptr = dynamic_cast<MoreDerived *>(ptr);

// Search path:
// __type_info@MoreDerived ->
// __type_info@Derived (match!)
//
auto *d_ptr = dynamic_cast<Derived *>(ptr);

// Search path:
// __type_info@MoreDerived ->
// __type_info@Derived ->
// __type_info@Base (no match)
//
// Did not find a path connecting RelatedImpl to MoreDerived.
//
// rptr will be nullptr
//
auto *rptr = dynamic_cast<RelatedImpl *>(ptr);

Ни в одной точке кода выше не было ptr->__vfptr нужно изменить. Статическая природа дедукции типов в C ++ требует, чтобы реализация удовлетворяла принципу подстановки во время компиляции, а это означает, что фактический тип объекта не может изменяться во время выполнения.


Резюме

Я понял этот вопрос как вопрос о механизмах динамической отправки.

Мне, «Какая запись в vtable относится к функции« определенных »производных классов, которая должна вызываться во время выполнения?», спрашивает, как работает vtable.

Этот ответ предназначен для демонстрации того, что приведение типов влияет только на представление данных объекта и что реализация динамической диспетчеризации в этих примерах работает независимо от нее. Тем не менее, тип литья делает влияет на динамическую диспетчеризацию в случае множественного наследования, где определение который Для использования vtable может потребоваться несколько шагов (экземпляр типа с несколькими базами может иметь несколько vptr).

4

Кастинг
Кастинг — это понятие, связанное с переменной. Таким образом, любая переменная может быть приведена. Это может быть брошено вверх или вниз.

char charVariable = 'A';
int intVariable = charVariable; // upcasting

int intVariable = 20;
char charVariale = intVariable; // downcasting

для определяемого системой типа данных преобразование Up или downcast основано на текущей переменной и в основном связано с тем, сколько памяти компилятор выделяет для обеих сравниваемых переменных.

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

Если вы присваиваете переменную, которая выделяет больше памяти, чем тип, в который преобразуется, вызывается приведения вниз.
Приведение вниз создает некоторую проблему, когда значение, которое пытается преобразовать, не может вписаться в выделенную область памяти.

Upcasting на уровне класса
Так же, как определенный системой тип данных, мы можем иметь объект базового класса и производного класса. Поэтому, если мы хотим преобразовать производный тип в базовый тип, это называется понижением частоты. Это может быть достигнуто указателем базового класса, указывающим на тип производного класса.

class Base{
public:
void display(){
cout<<"Inside Base::display()"<<endl;
}
};
class Derived:public Base{
public:
void display(){
cout<<"Inside Derived::display()"<<endl;
}
};

int main(){
Base *baseTypePointer = new Derived(); // Upcasting
baseTypePointer.display();  // because we have upcasted we want the out put as Derived::display() as output

}

выход

Внутри базы :: дисплей ()

Освобожденные

Внутри Derived :: display ()

В вышеприведенном сценарии выходные данные не были такими исключительными. Это потому, что у нас нет v-таблицы и vptr (виртуального указателя) в объекте, базовый указатель будет вызывать Base :: display (), хотя мы назначили производный тип базовому указателю.

Чтобы избежать этой проблемы, C ++ дает нам виртуальную концепцию. Теперь функцию отображения базового класса необходимо изменить на виртуальный тип.

virtual void display()

полный код:

class Base{
public:
virtual void display(){
cout<<"Inside Base::display()"<<endl;
}
};
class Derived:public Base{
public:
void display(){
cout<<"Inside Derived::display()"<<endl;
}
};

int main(){
Base *baseTypePointer = new Derived(); // Upcasting
baseTypePointer.display();  // because we have upcasted we want the out put as Derived::display() as output

}

выход

Внутри Derived :: display ()

Освобожденные

Внутри Derived :: display ()

Чтобы понять это, нам нужно понять v-table и vptr;
когда когда-либо компилятор находит виртуальное вместе с функцией, он генерирует виртуальную таблицу для каждого из классов (как базовых, так и всех производных классов).

Если присутствует виртуальная функция, то каждый объект будет содержать vptr (виртуальный указатель), указывающий на соответствующий класс vtable, а vtable будет содержать указатель на виртуальную функцию соответствующего класса. когда вы вызовете функцию через vptr, будет вызвана функция virutal, которая вызовет соответствующую функцию класса, и мы получим требуемый результат.

введите описание изображения здесь

4

Я полагаю, это лучше всего объяснить реализацией полиморфизма в C. Учитывая эти два класса C ++:

class Foo {
virtual void foo(int);
};

class Bar : public Foo {
virtual void foo(int);
virtual void bar(double);
};

определения структуры C (то есть файл заголовка) будут выглядеть так:

//For class Foo
typedef struct Foo_vtable {
void (*foo)(int);
} Foo_vtable;

typedef struct Foo {
Foo_vtable* vtable;
} Foo;

//For class Bar
typedef struct Bar_vtable {
Foo_vtable super;
void (*bar)(double);
}

typedef struct Bar {
Foo super;
} Bar;

Как видите, для каждого класса есть два определения структуры: одно для виртуальной таблицы и одно для самого класса. Отметим также, что обе структуры для class Bar включить объект базового класса в качестве первого члена, который позволяет нам апкастинг: оба (Foo*)myBarPointer а также (Foo_vtable*)myBar_vtablePointer действительны. Как таковой, учитывая Foo*, безопасно найти местоположение foo() член, делая

Foo* basePointer = ...;
(basePointer->vtable->foo)(7);

Теперь давайте посмотрим, как мы можем заполнить виртуальные таблицы. Для этого мы напишем несколько конструкторов, которые используют статически определенные экземпляры vtable, вот так может выглядеть файл foo.c

#include "..."
static void foo(int) {
printf("Foo::foo() called\n");
}

Foo_vtable vtable = {
.foo = &foo,
};

void Foo_construct(Foo* me) {
me->vtable = vtable;
}

Это гарантирует, что возможно выполнить (basePointer->vtable->foo)(7) на каждый объект, который был передан Foo_construct(), Теперь код для Bar очень похоже:

#include "..."
static void foo(int) {
printf("Bar::foo() called\n");
}

static void bar(double) {
printf("Bar::bar() called\n");
}

Bar_vtable vtable = {
.super = {
.foo = &foo
},
.bar = &bar
};

void Bar_construct(Bar* me) {
Foo_construct(&me->super);    //construct the base class.
(me->vtable->foo)(7);    //This will print Foo::foo()
me->vtable = vtable;
(me->vtable->foo)(7);    //This will print Bar::foo()
}

Я использовал статические объявления для функций-членов, чтобы избежать необходимости придумывать новое имя для каждой реализации, static void foo(int) ограничивает видимость функции для исходного файла. Тем не менее, он все еще может быть вызван из других файлов с помощью указателя функции.

Использование этих классов может выглядеть так:

#include "..."
int main() {
//First construct two objects.
Foo myFoo;
Foo_construct(&myFoo);

Bar myBar;
Bar_construct(&myBar);

//Now make some pointers.
Foo* pointer1 = &myFoo, pointer2 = (Foo*)&myBar;
Bar* pointer3 = &myBar;

//And the calls:
(pointer1->vtable->foo)(7);    //prints Foo::foo()
(pointer2->vtable->foo)(7);    //prints Bar::foo()
(pointer3->vtable->foo)(7);    //prints Bar::foo()
(pointer3->vtable->bar)(7.0);  //prints Bar::bar()
}

Как только вы узнаете, как это работает, вы узнаете, как работают C ++ vtables. Разница лишь в том, что в C ++ компилятор выполняет ту работу, которую я выполнял в приведенном выше коде.

2

Позвольте мне попытаться объяснить это на нескольких примерах:

class Base
{
public:
virtual void function1() {cout<<"Base :: function1()\n";};
virtual void function2() {cout<<"Base :: function2()\n";};
virtual ~Base(){};
};

class D1: public Base
{
public:
~D1(){};
virtual void function1() { cout<<"D1 :: function1()\n";};
};

class D2: public Base
{
public:
~D2(){};
virtual void function2() { cout<< "D2 :: function2\n";};
};

Таким образом, компилятор сгенерирует три vtables по одному для каждого класса, так как эти классы имеют виртуальные функции. (Хотя это зависит от компилятора).

ПРИМЕЧАНИЕ: — vtables содержат только указатели на виртуальные функции. Не виртуальные функции будут по-прежнему решаться во время компиляции …

Вы правы, говоря, что vtables — это не что иное, как указатели на функции. vtables для этих классов будет выглядеть примерно так:

vtable для базы: —

&Base::function1 ();
&Base::function2 ();
&Base::~Base ();

Vtable для D1: —

&D1::function1 ();
&Base::function2 ();
&D1::~D1();

Vtable для D2: —

&Base::function1 ();
&D2::function2 ();
&D2::~D2 ();

vptr — указатель, который используется для поиска в этой таблице. В каждом объекте полиморфного класса есть дополнительное выделенное пространство для vptr (хотя расположение vptr в объекте полностью зависит от реализации). Обычно vptr находится в начале объекта.

Принимая все во внимание, если я сделаю вызов func, компилятор во время выполнения проверит, на что фактически указывает b:

void func ( Base* b )
{
b->function1 ();
b->function2 ();
}

Допустим, у нас есть объект D1, переданный в func. Компилятор будет разрешать вызовы следующим образом:

Сначала он извлекает vptr из объекта, а затем использует его для получения правильного адреса вызываемой функции. Так что, в этом случае vptr даст доступ к vtable D1. и когда он ищет функцию1, он получает адрес функции1, определенной в базовом классе. В случае вызова функции 2 он получит адрес функции базы 2.

Надеюсь, я прояснил ваши сомнения к вашему удовлетворению …

2

Реализация зависит от компилятора. Здесь я собираюсь сделать несколько мыслей, которые НИЧЕГО НЕ СДЕЛАЮТ С ЛЮБЫМ АКТУАЛЬНЫМ ЗНАНИЕМ, как именно это делается в компиляторах, но только с некоторыми минимальными требованиями, которые необходимы для работы по мере необходимости. Имейте в виду, что каждый экземпляр класса с виртуальными методами во время выполнения знает, к какому классу он тоже принадлежит.

Предположим, что у нас есть цепочка базовых и производных классов длиной 10 (так что у производного класса есть гран гран … гран папа).
Мы можем назвать эти классы base0 base1 … base9, где base9 происходит от base8 и т. Д.

Каждый из этих классов определяет метод как: virtual void doit () {…}

Давайте предположим, что в базовом классе мы используем этот метод внутри метода с именем «dowith_doit», который не переопределяется ни в одном производном классе.
Семантика c ++ подразумевает, что в зависимости от базового класса имеющегося у нас экземпляра мы должны применить к этому экземпляру «doit», определенный в базовом классе рассматриваемого экземпляра.

По сути, у нас есть два возможных способа сделать это:
a) Присвойте любому такому виртуальному методу число, которое должно быть различным для каждого метода, определенного в цепочке производных классов. В этом случае число может быть также хэшем имени метода.
Каждый класс определяет таблицу с 2 столбцами, где первый столбец содержит номер метода, а второй столбец — адрес функции. В этом случае у каждого класса будет vtable с таким количеством строк, как количество виртуальных методов, определенных внутри класса.
Выполнение метода происходит путем поиска внутри класса рассматриваемого метода. Этот поиск может быть выполнен линейно (медленно) путем деления пополам (когда есть порядок, основанный на номере метода).

б) Присвойте любому такому методу прогрессивно растущее целое число (для каждого отдельного метода в цепочке классов) и для каждого класса определите таблицу только с одним столбцом. Для виртуальных методов, определенных внутри класса, адрес функции будет в необработанном виде, определяемом номером метода. Будет много строк с нулевыми указателями, потому что каждый класс не всегда переопределяет методы предыдущих классов.
Реализация может выбрать для повышения эффективности заполнение пустых строк адресом, удерживаемым в классе предка рассматриваемого класса.

По существу никаких других простых способов для эффективной работы с виртуальными методами не существует.

Я полагаю, что только второе решение (b) используется в реальных реализациях, потому что перераспределение между служебными данными пространства, используемыми для несуществующих методов, по сравнению с эффективностью выполнения случая (b) является благоприятным для случая b (принимая во внимание также, что методы ограниченное количество — может быть 10 20 50, но не 5000).

2
По вопросам рекламы [email protected]