С каждым классом, который содержит одну или несколько виртуальных функций, связана Vtable. Пустой указатель vptr указывает на эту таблицу. Каждый объект этого класса содержит тот vptr, который указывает на тот же Vtable. Тогда почему vptr не статичен? Вместо того, чтобы связывать vptr с объектом, почему бы не связать его с классом?
Класс выполнения объекта является свойством самого объекта. В результате, vptr
представляет класс времени выполнения, и поэтому не может быть static
, Однако то, на что он указывает, может быть общим для всех экземпляров одного и того же класса времени выполнения.
Ваша диаграмма неверна. Нет ни одного vtable, есть один vtable для каждого полиморфного типа. Вптр для A
указывает на vtable для A
ВПТР для A1
указывает на vtable для A1
и т.п.
Дано:
class A {
public:
virtual void foo();
virtual void bar();
};
class A1 : public A {
virtual void foo();
};
class A2 : public A {
virtual void foo();
};
class A3 : public A {
virtual void bar();
virtual void baz();
};
Vtable для A
содержит { &A::foo, &A::bar }
Vtable для A1
содержит { &A1::foo, &A::bar }
Vtable для A2
содержит { &A2::foo, &A::bar }
Vtable для A3
содержит { &A::foo, &A3::bar, &A3::baz }
Поэтому, когда вы звоните a.foo()
компилятор следует vptr объекта, чтобы найти vtable, а затем вызывает первую функцию в vtable.
Предположим, компилятор использует вашу идею, и мы пишем:
A1 a1;
A2 a2;
A& a = (std::rand() % 2) ? a1 : a2;
a.foo();
Компилятор выглядит в базовом классе A
и находит vptr для класса A
который (согласно вашей идее) является static
свойство типа A
не член объекта, на который ссылается a
связан с. Это vptr указывает на vtable для A
, или же A1
или же A2
или что-то другое? Если это указало на vtable для A1
было бы неправильно 50% времени, когда a
относится к a2
, и наоборот.
Теперь предположим, что мы пишем:
A1 a1;
A2 a2;
A& a = a1;
A& aa = a2;
a.foo();
aa.foo();
a
а также aa
обе ссылки на A
, но им нужны два разных vptr, один указывает на vtable для A1
и один указывает на vtable для A2
, Если vptr является статическим членом A
как он может иметь два значения одновременно? Единственный логичный, последовательный выбор заключается в том, что статический vptr A
указывает на vtable для A
,
Но это означает, что вызов a.foo()
звонки A::foo()
когда это должно позвонить A1::foo()
и вызов aa.foo()
также звонки A::foo()
когда это должно позвонить A2::foo()
,
Очевидно, что ваша идея не может реализовать требуемую семантику, доказывая, что компилятор, использующий вашу идею, не может быть компилятором C ++. Компилятор не может получить vtable для A1
от a
не зная, что является производным типом (что вообще невозможно, ссылка на базу могла бы быть возвращена из функции, определенной в другой библиотеке, и могла бы ссылаться на производный тип, который еще даже не был написан!) или с сохранением vptr непосредственно в объекте.
Vptr должен быть другим для a1
а также a2
и должен быть доступен без знания динамического типа при доступе к ним через указатель или ссылку на базу, чтобы при получении vptr посредством ссылки на базовый класс a
, он по-прежнему указывает на правый vtable, а не на базовый класс vtable. Самый очевидный способ сделать это — сохранить vptr непосредственно в объекте. Альтернативным, более сложным решением будет сохранение карты адресов объектов для vptrs, например что-то вроде std::map<void*, vtable*>
и найти vtable для a
глядя вверх &a
, но он по-прежнему хранит один vptr на объект, а не по одному на тип, и потребует гораздо больше работы (и динамического выделения) для обновления карты каждый раз, когда создаются и уничтожаются полиморфные объекты, и увеличивает общее использование памяти, поскольку структура карты будет занимать место. Проще просто встроить vptr в сами объекты.
Виртуальная таблица (которая, кстати, представляет собой механизм реализации, не упомянутый в стандарте C ++), используется для идентификации динамического типа объекта во время выполнения. Поэтому сам объект должен содержать указатель на него. Если бы он был статическим, то он мог бы идентифицировать только статический тип, и это было бы бесполезно.
Если вы думаете о том, как typeid()
внутренне, чтобы идентифицировать динамический тип и затем вызвать статический указатель с ним, помните, что typeid()
возвращает только динамический тип для объектов, принадлежащих к типам с виртуальными функциями; в противном случае он просто возвращает статический тип (§ 5.2.8 в текущем стандарте C ++). Да, это означает, что это работает наоборот: typeid()
обычно использует виртуальный указатель для определения динамического типа.
Весь смысл vptr
потому что вы не знаете точно, какой класс у объекта во время выполнения. Если бы вы знали это, то вызов виртуальной функции был бы ненужным. Именно это и происходит, когда вы не используете виртуальные функции. Но с виртуальными функциями, если у меня есть
class Sub : Parent {};
и значение типа Parent*
Я не знаю во время выполнения, если это действительно объект типа Parent
или один из типа Sub
, Vptr позволяет мне понять это.
Как все подтверждают, Vptr является собственностью объекта.
Давайте посмотрим почему?
Предположим, у нас есть три объекта
Class Base{
virtual ~Base();
//Class Definition
};
Class Derived: public Base{
//Class Definition
};
Class Client: public Derived{
//Class Definition
};
базовые отношения<— производный<—- Клиент.
Клиентский класс является производным от производного класса, который в свою очередь является производным от Base
Base * Ob = new Base;
Derived * Od = new Derived;
Client* Oc = new Client;
Всякий раз, когда Oc уничтожается, он должен разрушать базовую часть, производную часть, а затем клиентскую часть данных. Для облегчения этой последовательности базовый деструктор должен быть виртуальным, а деструктор объекта Oc указывает на деструктор клиента. Когда базовый деструктор объекта Oc является виртуальным компилятором, он добавляет код в деструктор объекта Oc, чтобы вызвать деструктор производного и производный деструктор, чтобы вызвать деструктор базы. В этой цепочке все базовые, производные и клиентские данные разрушаются при уничтожении объекта Client.
Если этот vptr является статическим, то запись vtable в Oc по-прежнему будет указывать на деструктор Base, и уничтожается только базовая часть Oc. Vptr в Oc всегда должен указывать на деструктор большинства производных объектов, что невозможно, если vptr является статическим.
таблица виртуальных методов для каждого класса. Объект содержит указатель на тип времени выполнения vptr.
Я не думаю, что это требование в стандартном бюсте, так как все компиляции, с которыми я работал, делают это таким образом.
Это верно даже в вашем примере.
@Harsh Maurya: Причина может быть в том, что статические переменные-члены должны быть определены до функции Main в программе. Но если мы хотим, чтобы _vptr был статическим, чья ответственность (компилятор / программист) определять _vptr в программе перед main. И как программист знает указатель VTABLE, чтобы назначить его _vptr. Вот почему компилятор взял на себя эту ответственность за присвоение значения указателю (_vptr). Это происходит в конструкторе класса (скрытая функциональность). И теперь, если появляется Конструктор, для каждого объекта должен быть один _vptr.