Все мы знаем, что такое виртуальные функции в C ++, но как они реализованы на глубоком уровне?
Можно ли изменить vtable или даже получить к ней прямой доступ во время выполнения?
Существует ли vtable для всех классов или только для тех, у которых есть хотя бы одна виртуальная функция?
Имеют ли абстрактные классы просто NULL для указателя функции хотя бы одной записи?
Имеет ли одна виртуальная функция замедление работы всего класса? Или только вызов виртуальной функции? И влияет ли скорость на скорость, если виртуальная функция действительно перезаписывается или нет, или это не имеет никакого эффекта, пока она виртуальная.
c++
polymorphism
virtual-functions
vtable
Брайан Р. Бонди
источник
источник
Inside the C++ Object Model
автораStanley B. Lippman
. (Раздел 4.2, страницы 124-131)Ответы:
Как виртуальные функции реализованы на глубоком уровне?
Из «Виртуальных функций в C ++» :
Можно ли изменить vtable или даже получить к ней прямой доступ во время выполнения?
В целом, я считаю, что ответ - «нет». Вы можете немного изменить память, чтобы найти vtable, но вы все равно не знаете, как выглядит сигнатура функции для ее вызова. Все, что вы хотели бы достичь с помощью этой возможности (поддерживаемой языком), должно быть возможно без прямого доступа к vtable или ее изменения во время выполнения. Также обратите внимание, что спецификация языка C ++ не указывает, что требуются vtables, однако именно так большинство компиляторов реализуют виртуальные функции.
Существует ли vtable для всех объектов или только для тех, у которых есть хотя бы одна виртуальная функция?
Я считаю, что ответ здесь - «это зависит от реализации», так как спецификация вообще не требует vtables. Однако на практике я считаю, что все современные компиляторы создают виртуальную таблицу только в том случае, если в классе есть хотя бы 1 виртуальная функция. Существуют накладные расходы на пространство, связанные с vtable, и накладные расходы времени, связанные с вызовом виртуальной функции по сравнению с невиртуальной функцией.
Имеют ли абстрактные классы просто NULL для указателя функции хотя бы одной записи?
Ответ в том, что он не определен спецификацией языка, поэтому это зависит от реализации. Вызов чистой виртуальной функции приводит к неопределенному поведению, если она не определена (что обычно не так) (ISO / IEC 14882: 2003 10.4-2). На практике он выделяет слот в vtable для функции, но не назначает ей адрес. Это оставляет vtable незавершенной, что требует, чтобы производные классы реализовали функцию и завершили vtable. Некоторые реализации просто помещают NULL-указатель в запись vtable; другие реализации помещают указатель на фиктивный метод, который делает что-то похожее на утверждение.
Обратите внимание, что абстрактный класс может определять реализацию для чистой виртуальной функции, но эта функция может быть вызвана только с синтаксисом квалифицированного идентификатора (т. Е. С полным указанием класса в имени метода, аналогично вызову метода базового класса из производный класс). Это сделано для обеспечения простой в использовании реализации по умолчанию, но при этом требует, чтобы производный класс предоставлял переопределение.
Наличие одной виртуальной функции замедляет работу всего класса или только вызов виртуальной функции?
Это уже почти мои знания, так что, пожалуйста, помогите мне, если я ошибаюсь!
Я считаю, что только функции, которые являются виртуальными в классе, испытывают снижение производительности по времени, связанное с вызовом виртуальной функции по сравнению с невиртуальной функцией. В любом случае накладные расходы на пространство для класса есть. Обратите внимание: если есть виртуальная таблица, то ее будет только одна на класс , а не одна на объект .
Влияет ли на скорость, если виртуальная функция фактически переопределена или нет, или это не имеет никакого эффекта, пока она виртуальная?
Я не верю, что время выполнения переопределенной виртуальной функции уменьшается по сравнению с вызовом базовой виртуальной функции. Однако существует дополнительное пространство для класса, связанного с определением другой vtable для производного класса по сравнению с базовым классом.
Дополнительные ресурсы:
http://www.codersource.net/published/view/325/virtual_functions_in.aspx (через машину обратной связи)
http://en.wikipedia.org/wiki/Virtual_table
http://www.codesourcery.com/public/ cxx-abi / abi.html # vtable
источник
Непереносимо, но если вы не против грязных трюков, конечно!
В большинстве компиляторов, которые я видел, vtbl * - это первые 4 байта объекта, а содержимое vtbl представляет собой просто массив указателей на элементы (обычно в том порядке, в котором они были объявлены, с первым из базового класса). Конечно, есть и другие возможные варианты расположения, но я обычно наблюдал именно это.
А теперь потянем за махинациями ...
Смена класса во время выполнения:
Замена метода для всех экземпляров (патч-обезьяна класса)
Это немного сложнее, поскольку сам vtbl, вероятно, находится в постоянной памяти.
Последнее, скорее всего, заставит антивирусные программы и ссылку активировать и обратить внимание из-за манипуляций с mprotect. В процессе, использующем бит NX, он вполне может выйти из строя.
источник
Имеет ли одна виртуальная функция замедление работы всего класса?
Наличие виртуальных функций замедляет работу всего класса, поскольку необходимо инициализировать, скопировать еще один элемент данных ... при работе с объектом такого класса. Для класса с полдюжиной членов или около того разница должна быть незначительной. Для класса, который содержит только один
char
член или вообще не содержит членов, разница может быть заметной.Кроме того, важно отметить, что не каждый вызов виртуальной функции является вызовом виртуальной функции. Если у вас есть объект известного типа, компилятор может выдать код для обычного вызова функции и даже может встроить указанную функцию, если сочтет нужным. Только когда вы выполняете полиморфные вызовы через указатель или ссылку, которые могут указывать на объект базового класса или на объект какого-либо производного класса, вам понадобится косвенное обращение к vtable и заплатите за это с точки зрения производительности.
Действия, которые должно предпринять оборудование, по существу одинаковы, независимо от того, перезаписана функция или нет. Адрес vtable считывается из объекта, указатель функции извлекается из соответствующего слота, а функция вызывается указателем. С точки зрения реальной производительности предсказания ветвлений могут иметь некоторое влияние. Так, например, если большинство ваших объектов относятся к одной и той же реализации данной виртуальной функции, то есть некоторая вероятность того, что предсказатель ветвления правильно предсказывает, какую функцию вызывать, даже до того, как указатель был получен. Но не имеет значения, какая функция является общей: это может быть большинство объектов, делегирующих неперезаписываемый базовый вариант, или большинство объектов, принадлежащих одному подклассу и, следовательно, делегируемых одному и тому же перезаписанному случаю.
как они реализованы на глубоком уровне?
Мне нравится идея jheriko продемонстрировать это с помощью фиктивной реализации. Но я бы использовал C для реализации чего-то похожего на приведенный выше код, чтобы было легче увидеть низкий уровень.
родительский класс Foo
производный класс Bar
функция f, выполняющая вызов виртуальной функции
Итак, вы можете видеть, что vtable - это просто статический блок в памяти, в основном содержащий указатели на функции. Каждый объект полиморфного класса будет указывать на vtable, соответствующую его динамическому типу. Это также делает связь между RTTI и виртуальными функциями более ясной: вы можете проверить, к какому типу относится класс, просто посмотрев, на какую vtable он указывает. Вышеизложенное упрощено во многих отношениях, например, с множественным наследованием, но общая концепция разумна.
Если
arg
имеет типFoo*
и вы беретеarg->vtable
, но на самом деле это объект типаBar
, то вы все равно получите правильный адресvtable
. Это потому, чтоvtable
всегда является первым элементом по адресу объекта, независимо от того, вызывается онvtable
илиbase.vtable
в правильно набранном выражении.источник
Обычно с VTable - это массив указателей на функции.
источник
Этот ответ был включен в ответ сообщества Wiki
Ответ на этот вопрос заключается в том, что она не указана - вызов чистой виртуальной функции приводит к неопределенному поведению, если она не определена (что обычно не так) (ISO / IEC 14882: 2003 10.4-2). Некоторые реализации просто помещают NULL-указатель в запись vtable; другие реализации помещают указатель на фиктивный метод, который делает что-то похожее на утверждение.
Обратите внимание, что абстрактный класс может определять реализацию для чистой виртуальной функции, но эта функция может быть вызвана только с синтаксисом квалифицированного идентификатора (т. Е. С полным указанием класса в имени метода, аналогично вызову метода базового класса из производный класс). Это сделано для того, чтобы обеспечить простую в использовании реализацию по умолчанию, но при этом требуется, чтобы производный класс предоставлял переопределение.
источник
Вы можете воссоздать функциональность виртуальных функций в C ++, используя указатели функций как члены класса и статические функции как реализации, или используя указатель на функции-члены и функции-члены для реализаций. Между этими двумя методами есть только преимущества в обозначениях ... на самом деле вызовы виртуальных функций сами по себе являются просто удобством записи. На самом деле наследование - это просто удобство записи ... все это может быть реализовано без использования языковых функций для наследования. :)
Приведенный ниже дерьмовый непроверенный код, вероятно, содержит ошибки, но, надеюсь, демонстрирует идею.
например
источник
void(*)(Foo*) MyFunc;
это какой-то синтаксис Java?Постараюсь сделать проще :)
Все мы знаем, что такое виртуальные функции в C ++, но как они реализованы на глубоком уровне?
Это массив с указателями на функции, которые являются реализациями конкретной виртуальной функции. Индекс в этом массиве представляет собой конкретный индекс виртуальной функции, определенной для класса. Это включает чисто виртуальные функции.
Когда полиморфный класс является производным от другого полиморфного класса, у нас могут быть следующие ситуации:
Можно ли изменить vtable или даже получить к ней прямой доступ во время выполнения?
Нестандартный способ - нет API для доступа к ним. У компиляторов могут быть некоторые расширения или частные API для доступа к ним, но это может быть только расширение.
Существует ли vtable для всех классов или только для тех, у которых есть хотя бы одна виртуальная функция?
Только те, которые имеют хотя бы одну виртуальную функцию (будь то даже деструктор) или производят хотя бы один класс, имеющий свою vtable («полиморфен»).
Имеют ли абстрактные классы просто NULL для указателя функции хотя бы одной записи?
Это возможная реализация, но не практикуется. Вместо этого обычно есть функция, которая выводит что-то вроде «вызвана чистая виртуальная функция» и выполняет
abort()
. Вызов этого может произойти, если вы попытаетесь вызвать абстрактный метод в конструкторе или деструкторе.Имеет ли одна виртуальная функция замедление работы всего класса? Или только вызов виртуальной функции? И влияет ли скорость на скорость, если виртуальная функция действительно перезаписывается или нет, или это не имеет никакого эффекта, пока она виртуальная.
Замедление зависит только от того, разрешен ли вызов как прямой вызов или как виртуальный вызов. И все остальное не имеет значения. :)
Если вы вызываете виртуальную функцию через указатель или ссылку на объект, тогда она всегда будет реализована как виртуальный вызов, потому что компилятор никогда не может знать, какой тип объекта будет назначен этому указателю во время выполнения и имеет ли он класс, в котором этот метод переопределяется или нет. Только в двух случаях компилятор может разрешить вызов виртуальной функции как прямой вызов:
final
в классе, на который у вас есть указатель или ссылка, через которую вы его вызываете ( только в C ++ 11 ). В этом случае компилятор знает, что этот метод не может подвергаться дальнейшему переопределению и может быть только методом из этого класса.Обратите внимание, что виртуальные вызовы имеют только накладные расходы на разыменование двух указателей. Использование RTTI (хотя и доступно только для полиморфных классов) медленнее, чем вызов виртуальных методов, если вам удастся реализовать одно и то же двумя способами. Например, определение,
virtual bool HasHoof() { return false; }
а затем переопределение только для того,bool Horse::HasHoof() { return true; }
чтобы предоставить вам возможность вызывать,if (anim->HasHoof())
который будет быстрее, чем попыткиif(dynamic_cast<Horse*>(anim))
. Это связано с тем, чтоdynamic_cast
в некоторых случаях приходится проходить по иерархии классов даже рекурсивно, чтобы увидеть, можно ли построить путь из фактического типа указателя и желаемого типа класса. В то время как виртуальный вызов всегда один и тот же - разыменование двух указателей.источник
Вот работающая ручная реализация виртуальной таблицы на современном C ++. У него четко определенная семантика, никаких хаков и нет
void*
.Примечание:
.*
и->*
разные операторы , чем*
и->
. Указатели на функции-члены работают иначе.источник
У каждого объекта есть указатель vtable, который указывает на массив функций-членов.
источник
Во всех этих ответах здесь не упоминается то, что в случае множественного наследования, когда все базовые классы имеют виртуальные методы. У наследующего класса есть несколько указателей на vmt. В результате размер каждого экземпляра такого объекта больше. Всем известно, что класс с виртуальными методами имеет 4 дополнительных байта для vmt, но в случае множественного наследования для каждого базового класса виртуальные методы умножены на 4. 4 - это размер указателя.
источник
Ответы Берли здесь верны, за исключением вопроса:
Имеют ли абстрактные классы просто NULL для указателя функции хотя бы одной записи?
Ответ заключается в том, что для абстрактных классов не создается вообще никакой виртуальной таблицы. В этом нет необходимости, поскольку нельзя создавать объекты этих классов!
Другими словами, если у нас есть:
Указатель vtbl, доступ к которому осуществляется через pB, будет vtbl класса D. Именно так и реализован полиморфизм. То есть, как доступ к методам D осуществляется через pB. Для класса B. нет необходимости в vtbl.
В ответ на комментарий Майка ниже ...
Если у класса B в моем описании есть виртуальный метод foo (), который не переопределяется D, и виртуальный метод bar (), который переопределяется, тогда vtbl D будет иметь указатель на foo () B и на его собственный bar () . Для B. все еще не создан vtbl.
источник
B
не должно быть необходимо. Тот факт, что некоторые из его методов имеют (по умолчанию) реализации, не означает, что они должны храниться в vtable. Но я просто прогнал ваш код (по модулю некоторых исправлений, чтобы он скомпилировался),gcc -S
а затем,c++filt
очевидно, есть vtable дляB
включения туда. Я предполагаю, что это может быть потому, что vtable также хранит данные RTTI, такие как имена классов и наследование. Это может потребоваться дляdynamic_cast<B*>
. Даже-fno-rtti
не убирает vtable. Сclang -O3
вместоgcc
это вдруг исчезло.очень симпатичное доказательство концепции, которое я сделал немного раньше (чтобы посмотреть, имеет ли значение порядок наследования); дайте мне знать, если ваша реализация C ++ действительно отклоняет его (моя версия gcc выдает предупреждение только о назначении анонимных структур, но это ошибка), мне любопытно.
CCPolite.h :
CCPolite_constructor.h :
main.c :
вывод:
обратите внимание, поскольку я никогда не выделяю свой фальшивый объект, нет необходимости производить какое-либо уничтожение; деструкторы автоматически помещаются в конец области динамически выделяемых объектов, чтобы освободить память самого литерала объекта и указателя vtable.
источник