Почему vptr не статичен?

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

введите здесь описание изображения


person Harsh Maurya    schedule 17.12.2012    source источник
comment
Если бы он был сделан статичным... какой в ​​этом был бы смысл?   -  person R. Martinho Fernandes    schedule 17.12.2012
comment
Как бы вы получили к нему доступ из объекта, чем?   -  person StoryTeller - Unslander Monica    schedule 17.12.2012
comment
Это сведет на нет всю цель. Подсказка кроется в названии: виртуальная диспетчеризация динамическая, а не статическая.   -  person Kerrek SB    schedule 17.12.2012
comment
@martinho: каждый объект несет дополнительные 4 байта памяти, когда для всего класса может быть всего 4 байта.   -  person Harsh Maurya    schedule 17.12.2012
comment
@Dima: насколько мне известно, объект может вызывать статическую функцию. (да, в .Net это невозможно.) Более того, компилятор при замене кода для получения vptr может использовать имя класса вместо объекта..   -  person Harsh Maurya    schedule 17.12.2012
comment
Но как узнать класс во время выполнения? Компилятор знает только базовый класс, поэтому он может подключить только vptr базового класса.   -  person StoryTeller - Unslander Monica    schedule 17.12.2012
comment
@Dima: Нет, компилятор никогда не подключает vptr базового класса. Он переходит в область памяти, на которую указывает переменная базового класса, и извлекает из нее производный класс vptr.   -  person Harsh Maurya    schedule 17.12.2012
comment
@HarshMaurya, точно. Итак, если адрес статический (vptr не находится в объекте), как мы узнаем во время выполнения, какая виртуальная таблица связана с каким объектом?   -  person StoryTeller - Unslander Monica    schedule 17.12.2012
comment
@DimaRudnik : верно. Наконец-то я это понял :) Но мне интересно, почему эта концепция нигде не упоминается. Это может запутать новичков.   -  person Harsh Maurya    schedule 17.12.2012
comment
Пустой указатель с именем vptr указывает на эту vtable. vptr не объявлен и не доступен в C++. У него нет типа C++, поэтому он не является пустым указателем. Даже образно говоря, его тип не void*: компилятор прекрасно знает, на какую структуру данных указывает vptr: он указывает на соответствующую vtable.   -  person curiousguy    schedule 26.06.2013


Ответы (7)


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

person NPE    schedule 17.12.2012
comment
Согласованный. Фактически, он также используется в RTTI. Но моя точка зрения такова: поскольку все объекты класса имеют один и тот же тип, vptr будет представлять один и тот же класс времени выполнения для каждого объекта. Почему бы не создать статический метод getType(), который использует статический vptr для получения своего типа. Объект может вызвать этот статический метод, чтобы узнать их типы. - person Harsh Maurya; 17.12.2012
comment
@HarshMaurya А как компилятор узнает, какую функцию вызывать? Весь смысл использования vptr в том, что компилятор не знает фактический тип; что фактический тип может меняться во время выполнения. - person James Kanze; 17.12.2012
comment
@HarshMaurya, если у вас есть A& a, каков класс времени выполнения этого объекта? A? A1? A2? Что-то другое? Откуда вы знаете? Вызов статической функции A никогда не сможет сказать вам класс времени выполнения (правильным термином является динамический тип) a - person Jonathan Wakely; 17.12.2012
comment
@HarshMaurya: Это создало бы проблему курицы и яйца. Чтобы вызвать функцию getType, вам сначала понадобится указатель виртуальной таблицы (поскольку для работы он должен быть virtual). Но чтобы получить указатель виртуальной таблицы, вы предлагаете вызвать getType. Упс. - person David Schwartz; 17.12.2012
comment
Понятно. Я отмечаю это как ответ. Хотя комментарий @DavidSchwartz заставил меня понять проблему. Спасибо. - person Harsh Maurya; 17.12.2012
comment
@NPE, каков тип 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 в сами объекты.

person Jonathan Wakely    schedule 17.12.2012
comment
Извините, но вы невнимательно прочитали схему. A1, A2 и A3 - это не классы, а объекты класса A. В любом случае, я нашел решение. Спасибо. - person Harsh Maurya; 17.12.2012
comment
О, так на вашей диаграмме нет полиморфизма. Ну, может быть, если вы подумаете о полиморфизме, который является причиной существования виртуальных функций, вы поймете, почему ваша идея не работает. Если у вас нет производных типов, ваша идея сработает, но если у вас нет производных типов, то зачем вам вообще использовать виртуальные функции? - person Jonathan Wakely; 17.12.2012
comment
Это самое понятное объяснение на эту тему, которое я видел. - person neevek; 16.08.2017

Виртуальная таблица (между прочим, механизм реализации, не упомянутый в стандарте C++) используется для определения динамического типа объекта во время выполнения. Следовательно, сам объект должен содержать указатель на него. Если бы он был статическим, то по нему можно было бы идентифицировать только статический тип и это было бы бесполезно.

Если вы думаете о том, чтобы каким-то образом использовать typeid() для внутренней идентификации динамического типа, а затем вызывать с его помощью статический указатель, имейте в виду, что typeid() возвращает динамический тип только для объектов, принадлежащих типам с виртуальными функциями; в противном случае он просто возвращает статический тип (§ 5.2.8 в текущем стандарте C++). Да, это означает, что это работает наоборот: typeid() обычно использует виртуальный указатель для идентификации динамического типа.

person Gorpik    schedule 17.12.2012
comment
Где определено typeof и где сказано, что его можно использовать только для полиморфных типов? Это неверно для typeof GCC, который работает как decltype и может использоваться для неполиморфных типов, он просто сообщает вам статический тип (который, очевидно, бесполезен для поиска динамического типа). - person Jonathan Wakely; 17.12.2012
comment
@JonathanWakely Извините, это опечатка: я имел в виду 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 является статическим.

person Guru    schedule 01.07.2014

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

class Sub : Parent {};

и значение типа Parent*, во время выполнения я не знаю, действительно ли это объект типа Parent или объект типа Sub. vptr позволяет мне понять это.

person Lily Ballard    schedule 17.12.2012

таблица виртуальных методов предназначена для каждого класса. Объект содержит указатель на тип времени выполнения vptr.

Я не думаю, что это требование стандартного перебора, все компиляции, с которыми я работал, делают это таким образом.

Это верно даже в вашем примере.

person Marius    schedule 17.12.2012
comment
-1: Не отвечает на вопрос. Вопрос был почему, а не что. - person John Dibling; 17.12.2012

@Harsh Maurya: Причина может заключаться в том, что статические переменные-члены должны быть определены перед основной функцией в программе. Но если мы хотим, чтобы _vptr был статическим, чья обязанность (компилятор/программист) определить _vptr в программе перед main. И как программист знает указатель VTABLE, чтобы присвоить его _vptr. Вот почему компилятор взял на себя ответственность присвоить значение указателю (_vptr). Это происходит в конструкторе класса (скрытая функциональность). И теперь, если в дело вступает Конструктор, для каждого объекта должен быть один _vptr.

person Rajesh Kommineni    schedule 31.03.2018