Хотя это не является обязательным в стандарте C ++, похоже, что GCC, например, реализует родительские классы, в том числе чисто абстрактные, путем включения указателя на v-таблицу для этого абстрактного класса в каждом экземпляре рассматриваемого класса. ,
Естественно, это увеличивает размер каждого экземпляра этого класса указателем на каждый родительский класс, который у него есть.
Но я заметил, что многие классы и структуры C # имеют множество родительских интерфейсов, которые в основном являются чисто абстрактными классами. Я был бы удивлен, если бы каждый случай, скажем Decimal
, был раздут с 6 указателями на все его различные интерфейсы.
Так что, если C # делает интерфейсы по-другому, как он это делает, по крайней мере, в типичной реализации (я понимаю, что сам стандарт может не определять такую реализацию)? И есть ли у любых реализаций C ++ способ избежать раздувания размера объекта при добавлении чистых виртуальных родителей в классы?
источник
IComparer
сCompare
g++-7 -fdump-class-hierarchy
вывод.Ответы:
В реализациях C # и Java объекты обычно имеют единственный указатель на свой класс. Это возможно, потому что это языки с одним наследованием. Структура класса тогда содержит vtable для иерархии с одним наследованием. Но вызов методов интерфейса имеет все проблемы множественного наследования. Обычно это решается добавлением дополнительных vtables для всех реализованных интерфейсов в структуру класса. Это экономит пространство по сравнению с типичными реализациями виртуального наследования в C ++, но усложняет диспетчеризацию метода интерфейса - что может быть частично компенсировано кэшированием.
Например, в JVM OpenJDK каждый класс содержит массив vtables для всех реализованных интерфейсов (vtable-интерфейс называется itable ). Когда вызывается метод интерфейса, этот массив ищется в линейном поиске для этого интерфейса, а затем метод может передаваться через этот интерфейс. Кэширование используется для того, чтобы каждый сайт вызова запоминал результат отправки метода, поэтому этот поиск нужно повторять только при изменении конкретного типа объекта. Псевдокод для отправки метода:
(Сравните реальный код в интерпретаторе OpenJDK HotSpot или компиляторе x86 .)
C # (или, точнее, CLR) использует связанный подход. Однако в данном случае itables не содержат указателей на методы, а являются картами слотов: они указывают на записи в основной vtable класса. Как и в случае с Java, поиск правильного itable - это только наихудший сценарий, и ожидается, что кэширование на сайте вызовов может избежать этого поиска почти всегда. CLR использует технику, называемую Virtual Stub Dispatch, для исправления JIT-скомпилированного машинного кода с помощью различных стратегий кэширования. псевдокод:
Основное отличие от OpenJDK-псевдокода состоит в том, что в OpenJDK каждый класс имеет массив всех прямо или косвенно реализованных интерфейсов, в то время как CLR сохраняет только массив карт слотов для интерфейсов, которые были непосредственно реализованы в этом классе. Поэтому нам нужно пройти иерархию наследования вверх, пока не будет найдена карта слотов. Для глубокой иерархии наследования это приводит к экономии места. Они особенно актуальны в CLR благодаря способу реализации обобщений: для обобщенной специализации структура класса копируется, и методы в основной таблице могут быть заменены специализациями. Карты слотов продолжают указывать на правильные записи vtable и поэтому могут быть разделены между всеми общими специализациями класса.
В завершение, есть больше возможностей для реализации диспетчеризации интерфейса. Вместо размещения указателя vtable / itable в объекте или в структуре класса мы можем использовать жирные указатели на объект, которые в основном являются
(Object*, VTable*)
парой. Недостаток состоит в том, что это удваивает размер указателей и что апкастинг (от конкретного типа к типу интерфейса) не является бесплатным. Но он более гибкий, имеет меньшую косвенность, а также означает, что интерфейсы могут быть реализованы извне из класса. Связанные подходы используются интерфейсами Go, чертами Rust и классами типов Haskell.Ссылки и дальнейшее чтение:
источник
callvirt
AKACEE_CALLVIRT
в CoreCLR - это инструкция CIL, которая обрабатывает вызывающие методы интерфейса, если кто-то хочет узнать больше о том, как среда выполнения обрабатывает эту настройку.call
код операции используется дляstatic
методов, интересно,callvirt
даже если классsealed
.Если под «родительским классом» вы имеете в виду «базовый класс», то в gcc это не так (как и в любом другом компиляторе).
В случае, когда C наследуется от B, наследуется от A, где A - полиморфный класс, экземпляр C будет иметь ровно одну vtable.
Компилятор имеет всю информацию, необходимую для слияния данных из v-таблицы A в B и B в C.
Вот пример: https://godbolt.org/g/sfdtNh
Вы увидите, что существует только одна инициализация vtable.
Я скопировал вывод сборки для главной функции здесь с аннотациями:
Полный источник для справки:
источник
class Derived : public FirstBase, public SecondBase
то может быть две виртуальными таблицами. Вы можете запустить,g++ -fdump-class-hierarchy
чтобы увидеть макет класса (также показано в моем сообщении в блоге). Затем Godbolt показывает дополнительный шаг указателя перед вызовом, чтобы выбрать второй vtable.