Если таблица виртуальных функций одинакова для всех объектов класса, то почему указатель на эту таблицу (vfptr) не может быть статическим и использоваться совместно для всех объектов?
Vtable по существу статичен. Но вам нужен член vptr внутри объекта, чтобы выполнять виртуальную диспетчеризацию и другие операции RTTI.
В реализации vptr этот код C ++:
class Base {
public:
virtual void f();
};
class Derived : public Base {
public:
virtual void f();
};
может действовать аналогично чему-то вроде этого:
class Base {
public:
Base::Base() : m_vptr(&Base_vtab) {}
Base::Base(const Base& b) : m_vptr(&Base_vtab) {}
void f() { (m_vptr->f_impl)(this); }
protected:
struct VTable {
void (*f_impl)(Base* self);
};
const VTable* m_vptr;
static const VTable Base_vtab;
private:
static void Base_f(Base* self);
};
const Base::VTable Base::Base_vtab = { &Base::Base_f };
class Derived : public Base {
public:
Derived::Derived() : Base() { m_vtpr = &Derived_vtab; }
Derived::Derived(const Derived& d) : Base(d) { m_vptr = &Derived_vtab; }
Derived::~Derived() { m_vptr = &Base::Base_vtab; }
protected:
static const VTable Derived_vtab;
private:
static void Derived_f(Derived* self);
static void Derived_f(Base* self) { Derived_f(static_cast<Derived*>(self)); }
};
const Base::VTable Derived::Derived_vtab = { &Derived::Derived_f };
Таблица виртуальных функций [в предположении, что компилятор C ++ реализует динамическую диспетчеризацию] распределяется между всеми объектами класса. Однако каждый объект должен знать, какая таблица виртуальных функций релевантна для этого объекта. Это то, на что указывает «указатель таблицы виртуальных функций».
Основная идея заключается в том, что статический тип ссылки или указателя на объект сообщает компилятору, как выглядит часть таблицы виртуальных функций. Когда ему нужно выполнить виртуальную диспетчеризацию, он просто следует этому указателю и решает, какую функцию вызвать. Предположим, у вас есть базовый класс B
и производные классы D1
а также D2
как это:
#include <iostream>
struct B {
virtual ~B() {}
virtual void f() = 0;
};
struct D1: public B {
void f() override { std::cout << "D1::f()\n"; }
};
struct D2: public B {
void f() override { std::cout << "D2::f()\n"; }
};
Таблица виртуальных функций для D1
а также D2
будет содержать подходящий указатель на D1::f()
а также D2::f()
соответственно. Когда компилятор видит вызов через указатель или ссылку на B
в f()
во время выполнения необходимо решить, какую функцию вызывать:
void g(B* base) {
base->f();
}
Чтобы разрешить вызов, он смотрит на то, куда указывает указатель виртуальной функции, и вызывает функцию в соответствующем слоте (более или менее; содержимое таблицы виртуальных функций, как правило, является громоздким, что может также выполнять любую необходимую настройку указателя).
«Виртуальный» означает «определенный во время выполнения». «Статический» означает «определенный во время перевода».
Чтобы принимать решения во время выполнения, у вас должен быть параметр (например, vptr), значение которого можно установить динамически во время выполнения. То есть для данной ссылки на базовый объект x
нам нужно какое-то значениеx.vptr
«который содержит динамическую информацию (а именно информацию о наиболее производном классе которого x
является базовым подобъектом).
class A
{
public:
virtual void Test();
...
};
class B: public A
{
public:
virtual void Test();
...
}
если vfptr является статическим для всех объектов, при компиляции кода ниже:
void DoTest(A* pA)
{
...
}
A* pA = new B;
DoTest(pA);
A :: vfptr будет распознаваться и использоваться компилятором, но это неожиданно!