Полиморфизм и динамическая отправка (сокращенная версия)
Примечание: мне не удалось уместить достаточно информации о множественном наследовании с виртуальными базами, так как в этом нет ничего простого, а подробности загромождали бы изложение (далее). Этот ответ демонстрирует механизмы, используемые для реализации динамической отправки, предполагающей только одиночное наследование.
Для интерпретации абстрактных типов и их поведения, видимого за пределами модуля, требуется общий двоичный интерфейс приложения (ABI). Стандарт C++, разумеется, не требует реализации какого-либо конкретного ABI.
ABI описывает:
- Макет таблиц диспетчеризации виртуальных методов (vtables)
- Метаданные, необходимые для проверки типов во время выполнения и операций приведения.
- Name decoration (a.k.a. mangling), calling conventions, and many other things.
Предполагается, что оба модуля в следующем примере, 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();
внешний.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...
Приложение, использующее внешнюю библиотеку
специальный.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;
}
};
основной.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.so
ptr = 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.o
ptr = 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.so
ptr = 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.so
ptr = 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.o
return 0;
}
основной.о:
__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 описывает, как функции упорядочиваются в виртуальных таблицах. Например: они могут быть упорядочены по классам, а затем лексикографически по искаженному имени (которое включает информацию о константности, параметрах и т. д.). Для одиночного наследования этот подход гарантирует, что виртуальный индекс отправки функции всегда будет одним и тем же, независимо от того, сколько различных реализаций существует.
В приведенных здесь примерах деструкторы помещаются в начало каждой виртуальной таблицы, если это применимо. Если деструктор тривиален и не виртуален (не определен или ничего не делает), компилятор может полностью исключить его и не выделять для него запись 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
имеет одинаковое смещение для всех типов в этой иерархии, поэтому логике RTTI не нужно обрабатывать какие-либо особые случаи (в некоторых реализациях VMI потребуется получить другое смещение от преобразователя типа, чтобы получить другой 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).
person
defube
schedule
13.11.2014
operator=
вызов. - person Tony Delroy   schedule 05.11.2014