Подтвердить что ты не робот

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

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

и

Назначение объекта производного класса объекту класса предка называется up-casting.

Up-casting обрабатывает экземпляр/объект производного класса с использованием указателя или ссылки базового класса; объекты не "назначены", что подразумевает перезапись значения ala operator = invocation.
(Благодаря: Tony D)

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

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

4b9b3361

Ответ 1

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

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

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

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

Up-casting обрабатывает экземпляр/объект производного класса с использованием указателя или ссылки базового класса; (...)

Возможно, более просветляющее:

Up-casting означает, что указатель или ссылка на экземпляр класса 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 для этого класса используется для поиска правильного метода.

Ответ 2

Вы можете представить (хотя в спецификации С++ этого не сказано), что 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);
};

Затем компилятор создаст внутреннюю структуру, что-то вроде этого (это невозможно скомпилировать как С++, просто показать, что делает на самом деле компилятор), и я называю это 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).

Ответ 3

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

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

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

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

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

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

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

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

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

Ответ 4

литье кастинг - это понятие, связанное с переменной. Таким образом, любая переменная может быть выполнена. Он может быть сброшен или опущен.

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

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

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

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

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

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

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

}

Выход

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

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

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

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

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

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; когда когда-либо компилятор найдет виртуальный вместе с функцией, он будет генерировать виртуальную таблицу для каждого из классов (как Base, так и всех производных классов).

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

enter image description here

Ответ 5

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

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

Интерпретация абстрактных типов и их поведения, видимых на границах модулей, требует использования обычного двоичного интерфейса приложения (ABI). Конечно, стандарт С++ не требует реализации какого-либо конкретного 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!


        // [email protected] gets treated like any other imported symbol:
        // The address is resolved at load time.
        //
        this->__vfptr = &[email protected];
        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 = &[email protected] in external.so
}

external.so(не реальный двоичный макет):

[email protected]:
[offset to [email protected]] <-- in external.so
[offset to Base::~Base] <------- in external.so
[offset to Base::Helpful] <----- in external.so

[email protected]:
[offset to [email protected]] <-- in external.so
[offset to Derived::~Derived] <---- in external.so
[offset to Derived::Helpful] <----- in external.so

Etc...

[email protected]:
[null base offset field]
[offset to mangled name]

[email protected]:
[offset to [email protected]]
[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 = &[email protected]; // <- 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 = &[email protected] (in external.so)

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

    delete ptr; // calls "Base::~Base" in external.so



    ptr = new Derived(); // ptr->__vfptr = &[email protected] (in main.o)

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

    delete ptr; // calls "Derived::~Derived" in main.o



    ptr = ReticulateSplines(); // ptr->__vfptr = &[email protected] (in external.so)

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

    delete ptr; // calls "Derived::~Derived" in external.so



    ptr = new Special(); // ptr->__vfptr = &[email protected] (in main.o)

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

    delete ptr; // calls "Base::~Base" in external.so



    ptr = new MoreDerived(); // ptr->__vfptr = & [email protected] (in main.o)

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

    delete ptr; // calls "MoreDerived::~MoreDerived" in main.o


    return 0;
}

main.o:

[email protected]:
[offset to [email protected]] <-- in main.o
[offset to Derived::~Derived] <--- in main.o
[offset to Derived::Helpful] <---- stub that jumps to import table

[email protected]:
[offset to [email protected]] <-- 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

[email protected]:
[offset to [email protected]] <---- in main.o
[offset to MoreDerived::~MoreDerived] <-- in main.o
[offset to MoreDerived::Helpful] <------- in main.o

[email protected]:
[offset to [email protected]] <------ 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

[email protected]:
[offset to [email protected]] <--- 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...

[email protected]Base:
[null base offset field]
[offset to mangled name]

[email protected]:
[offset to [email protected]]
[offset to mangled name]

[email protected]:
[offset to [email protected]]
[offset to mangled name]

[email protected]:
[offset to [email protected]]
[offset to mangled name]

[email protected]:
[offset to [email protected]]
[offset to mangled name]

[email protected]:
[offset to [email protected]]
[offset to mangled name]

Etc...

Вызов (возможно, не будет) Magic!

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

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

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

В приведенных здесь примерах деструкторы размещаются в начале каждой таблицы vtable, если это применимо. Если деструктор тривиальный и не виртуальный (не определен или ничего не делает), компилятор может полностью его отодвинуть и не выделять для него запись 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[[email protected]::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[[email protected]::Helpful] = &MoreDerived::Helpful in main.o
//
// push ptr
// call reg
// pop
//
// This will indirectly call MoreDerived::Helpful
//
ptr->Helpful();

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


Тип литья: Ups и Downs

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

Ускорение в этом простом ABI может быть выполнено полностью во время компиляции. Компилятору необходимо только изучить иерархию типов, чтобы определить, связаны ли исходный и целевой типы (есть путь от источника к цели в графе типов). По принцип подстановки указатель на MoreDerived также указывает на Base и может быть интерпретирован как таковой. Элемент __vfptr имеет одинаковое смещение для всех типов в этой иерархии, поэтому логике 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 &[email protected]
// using the offset within [email protected] 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:
// [email protected] (match!)
//
auto *md_ptr = dynamic_cast<MoreDerived *>(ptr);

// Search path:
// [email protected] ->
// [email protected] (match!)
//
auto *d_ptr = dynamic_cast<Derived *>(ptr);

// Search path:
// [email protected] ->
// [email protected] ->
// [email protected] (no match)
//
// Did not find a path connecting RelatedImpl to MoreDerived.
//
// rptr will be nullptr
//
auto *rptr = dynamic_cast<RelatedImpl *>(ptr);

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


Резюме

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

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

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

Ответ 6

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

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: -

&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 из объекта, а затем он будет использовать его для получения правильного адреса функции для вызова. SO, в этом случае vptr предоставит доступ к D1 vtable. и когда он ищет функцию1, он получит адрес функции1, определенный в базовом классе. В случае вызова функции2 он получит адрес базовой функции2.

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

Ответ 7

Полагаю, это лучше всего объяснить внедрением полиморфизма в 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;

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

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

Теперь давайте посмотрим, как мы можем фактически заполнить vtables. Для этого мы пишем некоторые конструкторы, которые используют некоторые статически определенные экземпляры 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()
}

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

Ответ 8

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

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

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

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

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

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

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

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

Ответ 9

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

class Base {
hidden: // not part of the language, just to illustrate.
  static VDT baseVDT; // per class VDT for base
  VDT *vTable; // per object instance
private:
  ...
public:
  virtual int base1();
  virtual int base2();
  ...
};

vTable содержит указатели на все функции в Base.

В качестве скрытой части базового конструктора vTable присваивается baseVDT.

VDT Base::baseVDT[] = { 
  Base::base1, 
  Base::base2 
};

class Derived : public Base {
hidden:
  static VDT derivedVDT; // per class VDT for derived
private:
  ...
public:
  virtual int base2();
  ...
};

vTable for Derived содержит указатели на все функции, определенные в Base, за которыми следуют функции, определенные в Derived. Когда объекты типа Derived создаются, vTable устанавливается в производный VDT.

VDT derived::derivedVDT[] = { 
  // functions first defined in Base
  Base::base1, 
  Derived::base2, // override
  // functions first defined in Derived are appended
  Derived::derived3 
}; // function 2 has an override in derived.

Теперь, если мы имеем

Base *bd    = new Derived;
Derived *dd = new Derived;
Base *bb    = new Base;

bd указывает на объект типа, который vTable указывает на Derived

Таким образом, функция вызывает

x = bd->base2();
y = bb->base2();

на самом деле

// "base2" here is the index into vTable for base2.
x = bd->vTable["base2"]();  // vTable points to derivedVDT
y = bb->vTable["base2"]();  // vTable points to baseVDT

Индекс тот же, что и из-за конструкции VDT. Это также означает, что компилятор знает индекс в момент компиляции.

Это также может быть реализовано как

// call absolute address to virtual dispatch function which calls the right base2.
x = Base::base2Dispatch(bd->vTable["base2"]); 

inline Base::base2Dispatch(void *call) {
  return call(); // call through function pointer.
}

Что с O2 или O3 будет одинаковым.


Существуют некоторые особые случаи:

dd указывает на производный или более производный объект, а base2 объявляется final, затем

z = dd->base2();

на самом деле

z = Derived::base2();  // absolute call to final method.

Если dd указал на объект Base или что-то еще, ваше поведение в режиме w90 > и компилятор все еще может это сделать.

В другом случае, если компилятор видит только несколько производных классов из Base, он может создать интерфейс Oracle для base2. [бесплатно после компилятора MS или Intel на какой-либо конференции С++ в 2012 или 2013 году? показывая, что (~ 500%?) больше кода дает (2+ раз?) ускорение в среднем]

inline Base::base2Dispatch(void *call) {
  if (call == Derived::base2)  // most likely from compilers static analysis or profiling.
    return Derived::base2(); // call absolute address
  if (call == Base::base2)
    return Base::base2(); // call absolute address

  //  Backup catch all solution in case of more derived classes
  return call(); // call through function pointer.
}

Почему вы хотите сделать это как компилятор??? больше кода плохо, ненужные ветки уменьшают производительность!

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

Получить адрес из памяти, 3+ цикла. Отложенный конвейер, ожидая значения ip, 10 циклов, на некоторых процессорах 19+ циклов.

Если самый сложный современный процессор может предсказать фактический адрес перехода [BTB], а также предсказание ветвления, это может быть потерей. В дополнение к 8 дополнительным инструкциям легко сэкономить 4 * (3 + 10) инструкции, потерянные из-за конвейерных ларьков (если частота отказов предсказания меньше 10-20%).

Если ветки в двух, если оба прогнозируются (т.е. оцениваются как ложные), потерянные 2 цикла хорошо покрываются латентностью памяти, чтобы получить адрес вызова, и мы не хуже.
Если один из вариантов if неверно предсказал, BTB, скорее всего, также ошибается. Тогда стоимость ошибочных прогнозов составляет около 8 циклов, из которых 3 оплачивается задержка памяти, а правильное не принимается или второе, если может сэкономить день, или мы заплатим полный 10+ конвейер. Если существует только 2 варианта, один из них будет, и мы сохраняем конвейерную стойку из вызова указателя функции, и мы будем макс. получить один неверный прогноз, в результате чего нет (значительного) худшего результата, чем прямого вызова. Если задержка памяти больше и результат правильно предсказан, эффект намного больше.