Каждый класс, который содержит одну или несколько виртуальных функций, имеет связанную с ним Vtable. Пустой указатель, называемый vptr, указывает на эту vtable. Каждый объект этого класса содержит тот vptr, который указывает на одну и ту же Vtable. Тогда почему vptr не статичен? Вместо того, чтобы связывать vptr с объектом, почему бы не связать его с классом?
Почему vptr не статичен?
Ответы (7)
Класс среды выполнения объекта является свойством самого объекта. По сути, vptr
представляет класс среды выполнения и, следовательно, не может быть static
. Однако то, на что он указывает, может совместно использоваться всеми экземплярами одного и того же класса среды выполнения.
vptr
в том, что компилятор не знает фактический тип; что фактический тип может меняться во время выполнения.
- person James Kanze; 17.12.2012
A& a
, каков класс времени выполнения этого объекта? A
? A1
? A2
? Что-то другое? Откуда вы знаете? Вызов статической функции A
никогда не сможет сказать вам класс времени выполнения (правильным термином является динамический тип) a
- person Jonathan Wakely; 17.12.2012
getType
, вам сначала понадобится указатель виртуальной таблицы (поскольку для работы он должен быть virtual
). Но чтобы получить указатель виртуальной таблицы, вы предлагаете вызвать getType
. Упс.
- person David Schwartz; 17.12.2012
vptr
(тип его класса)? Представьте, что будет на выходе, если я сделаю typeid(vptr).name()
по отношению к классу A
и его объекту A AO
?
- person sree; 21.04.2014
Ваша схема неверна. Нет единой vtable, есть одна vtable для каждого полиморфного типа. vptr для A
указывает на vtable для A
, vptr для 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();
};
Виртуальная таблица для A
содержит { &A::foo, &A::bar }
Виртуальная таблица для A1
содержит { &A1::foo, &A::bar }
Виртуальная таблица для A2
содержит { &A2::foo, &A::bar }
Виртуальная таблица для 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++. Компилятор не может получить виртуальную таблицу для 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()
обычно использует виртуальный указатель для идентификации динамического типа.
typeof
и где сказано, что его можно использовать только для полиморфных типов? Это неверно для typeof
GCC, который работает как decltype
и может использоваться для неполиморфных типов, он просто сообщает вам статический тип (который, очевидно, бесполезен для поиска динамического типа).
- person Jonathan Wakely; 17.12.2012
typeid
. И я только что увидел, что стандарт C++11 разрешает использовать его для неполиморфного типа: в этом случае вместо того, чтобы генерировать исключение, он просто возвращает статический тип. Что для наших целей означает то же самое: вы не можете найти динамический тип, если у вас нет виртуального указателя. Я обновляю свой ответ.
- person Gorpik; 17.12.2012
Как все утверждают, Vptr является свойством объекта. Давайте посмотрим, почему?
Предположим, у нас есть три объекта
Class Base{
virtual ~Base();
//Class Definition
};
Class Derived: public Base{
//Class Definition
};
Class Client: public Derived{
//Class Definition
};
удерживающее отношение Базовое‹---Производное‹----Клиентское. Клиентский класс является производным от производного класса, который, в свою очередь, является производным от базового.
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
в том, что вы точно не знаете, какой класс имеет объект во время выполнения. Если бы вы это знали, вызов виртуальной функции был бы не нужен. На самом деле это то, что происходит, когда вы не используете виртуальные функции. Но с виртуальными функциями, если у меня есть
class Sub : Parent {};
и значение типа Parent*
, во время выполнения я не знаю, действительно ли это объект типа Parent
или объект типа Sub
. vptr позволяет мне понять это.
таблица виртуальных методов предназначена для каждого класса. Объект содержит указатель на тип времени выполнения vptr.
Я не думаю, что это требование стандартного перебора, все компиляции, с которыми я работал, делают это таким образом.
Это верно даже в вашем примере.
@Harsh Maurya: Причина может заключаться в том, что статические переменные-члены должны быть определены перед основной функцией в программе. Но если мы хотим, чтобы _vptr был статическим, чья обязанность (компилятор/программист) определить _vptr в программе перед main. И как программист знает указатель VTABLE, чтобы присвоить его _vptr. Вот почему компилятор взял на себя ответственность присвоить значение указателю (_vptr). Это происходит в конструкторе класса (скрытая функциональность). И теперь, если в дело вступает Конструктор, для каждого объекта должен быть один _vptr.
void*
: компилятор прекрасно знает, на какую структуру данных указывает vptr: он указывает на соответствующую vtable. - person curiousguy   schedule 26.06.2013